Files
gta6/tools/extract/tests/test_overview.py
T

183 lines
7.7 KiB
Python

import hashlib
import json
from pathlib import Path
from PIL import Image
import overview
from leonida.coords import SLICE
def _ring(x0, z0, x1, z1):
return [[x0, z0], [x1, z0], [x1, z1], [x0, z1], [x0, z0]]
def _fixture(tmp_path: Path) -> Path:
"""Ein einziger Chunk A1 (Nordwest): Land, eine Straße, ein Gebäude, eine Palme.
Chunk-lokale Koordinaten von chunk_0_0 fallen bei scale 1 genau auf die Kartenpixel,
weil der Chunk-Ursprung die Nordwest-Ecke des Ausschnitts ist.
"""
chunk = {
"chunk": [0, 0],
"origin": [SLICE["x0"], -SLICE["y1"]],
"land": [[_ring(0, 0, 250, 250)]],
"surfaces": [],
"roads": [{"poly": [_ring(100, 0, 120, 250)], "bridge": False}],
"centerlines": [],
"buildings": [{"poly": [_ring(20, 20, 60, 60)], "height": 12.0, "source": "district", "speculative": False}],
"palms": [[200.0, 200.0]],
}
(tmp_path / "chunk_0_0.json").write_text(json.dumps(chunk))
(tmp_path / "manifest.json").write_text('{"slice": "vice-a"}\n')
return tmp_path
def test_map_picture_has_one_pixel_per_metre_and_no_palms(tmp_path):
src = tmp_path / "chunks"
src.mkdir()
_fixture(src)
dst = tmp_path / "out"
overview.render_map(src, dst)
picture = Image.open(dst / "map.png")
assert picture.size == (3000, 3000)
assert picture.getpixel((110, 125)) == overview.ROAD
assert picture.getpixel((40, 40)) == overview._building_shade(12.0)
assert picture.getpixel((200, 200)) == overview.LAND # Palme wird nicht gezeichnet
assert picture.getpixel((1500, 1500)) == overview.WATER
def test_walkable_mask_blocks_buildings_and_water(tmp_path):
src = tmp_path / "chunks"
src.mkdir()
_fixture(src)
dst = tmp_path / "out"
overview.render_map(src, dst)
mask = Image.open(dst / "map-walkable.png")
assert mask.mode == "L"
assert mask.size == (750, 750)
assert mask.getpixel((10, 10)) == overview.BLOCKED # Gebäude bei 40 m / 40 m
assert mask.getpixel((27, 31)) == overview.WALKABLE # Straße bei 110 m / 125 m
assert mask.getpixel((50, 50)) == overview.WALKABLE # Land bei 200 m / 200 m
assert mask.getpixel((375, 375)) == overview.BLOCKED # Wasser
def test_map_json_carries_scale_slice_and_manifest_fingerprint(tmp_path):
src = tmp_path / "chunks"
src.mkdir()
_fixture(src)
dst = tmp_path / "out"
overview.render_map(src, dst)
info = json.loads((dst / "map.json").read_text())
assert info["scale"] == 1.0
assert info["walkable_scale"] == 0.25
assert info["slice"] == SLICE
assert info["manifest_sha256"] == hashlib.sha256((src / "manifest.json").read_bytes()).hexdigest()
def test_debug_overview_still_draws_palms(tmp_path):
src = tmp_path / "chunks"
src.mkdir()
_fixture(src)
dst = tmp_path / "debug.png"
stats = overview.render(src, dst)
assert stats["palmen"] == 1
assert Image.open(dst).getpixel((100, 100)) == overview.PALM # 200 m bei 0,5 px/m
def test_walkable_mask_counts_only_ground_with_collision(tmp_path):
"""Straßen und Beläge sind reine Auflagen: ohne Landplatte darunter trägt nur eine Brücke."""
src = tmp_path / "chunks"
src.mkdir()
chunk = {
"chunk": [0, 0],
"origin": [SLICE["x0"], -SLICE["y1"]],
"land": [],
"surfaces": [{"poly": [_ring(0, 0, 40, 40)], "kind": "sand"}],
"roads": [{"poly": [_ring(100, 0, 120, 40)], "bridge": False},
{"poly": [_ring(200, 0, 220, 40)], "bridge": True}],
"centerlines": [],
"buildings": [],
"palms": [],
}
(src / "chunk_0_0.json").write_text(json.dumps(chunk))
(src / "manifest.json").write_text('{"slice": "vice-a"}\n')
dst = tmp_path / "out"
overview.render_map(src, dst)
mask = Image.open(dst / "map-walkable.png")
assert mask.getpixel((5, 5)) == overview.BLOCKED # Sand ohne Land bei 20 m / 20 m: keine Kollision
assert mask.getpixel((27, 5)) == overview.BLOCKED # Straße ohne Land bei 110 m / 20 m: keine Kollision
assert mask.getpixel((52, 5)) == overview.WALKABLE # Brücke bei 210 m / 20 m
assert mask.getpixel((40, 5)) == overview.BLOCKED # Wasser bei 160 m / 20 m
def test_walkable_mask_blocks_pixels_that_are_only_partly_free(tmp_path):
"""Ein 4-m-Pixel, den Wasser oder ein Gebäude auch nur anschneidet, ist kein Landeplatz."""
src = tmp_path / "chunks"
src.mkdir()
chunk = {
"chunk": [0, 0],
"origin": [SLICE["x0"], -SLICE["y1"]],
"land": [[_ring(0, 0, 102, 250)]], # Küste bei x = 102 m, mitten in Pixel 25
"surfaces": [],
"roads": [],
"centerlines": [],
"buildings": [{"poly": [_ring(38, 40, 50, 50)], "height": 8.0, "source": "district", "speculative": False}],
"palms": [],
}
(src / "chunk_0_0.json").write_text(json.dumps(chunk))
(src / "manifest.json").write_text('{"slice": "vice-a"}\n')
dst = tmp_path / "out"
overview.render_map(src, dst)
mask = Image.open(dst / "map-walkable.png")
assert mask.getpixel((24, 30)) == overview.WALKABLE # 96..100 m: ganz Land
assert mask.getpixel((25, 30)) == overview.BLOCKED # 100..104 m: halb Wasser
assert mask.getpixel((9, 11)) == overview.BLOCKED # 36..40 m: das Gebäude beginnt bei 38 m
assert mask.getpixel((8, 11)) == overview.WALKABLE # 32..36 m: frei
def test_committed_map_files_match_the_committed_chunks(tmp_path):
"""Kartenbild, Maske und map.json müssen aus genau den ausgelieferten Chunks stammen.
Ändert ein Pipeline-Lauf die Chunks, ohne dass overview.py läuft, zeigte die Minimap eine alte
Welt und landete nach einer alten Maske. Die Laufzeitprüfung über manifest_sha256 bemerkt das
erst im Browser, dieser Test schon beim Testlauf.
"""
shipped = Path(overview.__file__).resolve().parents[2] / "public" / "data" / "vice-a"
overview.render_map(shipped, tmp_path)
for name in ("map.png", "map-walkable.png"):
fresh, committed = Image.open(tmp_path / name), Image.open(shipped / name)
assert (fresh.size, fresh.mode) == (committed.size, committed.mode), name
assert fresh.tobytes() == committed.tobytes(), f"{name} ist veraltet: cd tools/extract && uv run python overview.py"
assert json.loads((tmp_path / "map.json").read_text()) == json.loads((shipped / "map.json").read_text())
def test_map_picture_draws_paths_and_the_new_surface_kinds(tmp_path):
src = tmp_path / "chunks"
src.mkdir()
chunk = {
"chunk": [0, 0],
"origin": [SLICE["x0"], -SLICE["y1"]],
"land": [[_ring(0, 0, 250, 250)]],
"surfaces": [{"poly": [_ring(0, 0, 40, 40)], "kind": "field"},
{"poly": [_ring(50, 0, 90, 40)], "kind": "court"},
{"poly": [_ring(0, 50, 40, 90)], "kind": "pool"},
{"poly": [_ring(50, 50, 90, 90)], "kind": "parking"}],
"roads": [],
"centerlines": [],
"buildings": [],
"palms": [],
"paths": [{"poly": [_ring(100, 0, 104, 250)]}],
}
(src / "chunk_0_0.json").write_text(json.dumps(chunk))
(src / "manifest.json").write_text('{"slice": "vice-a"}\n')
dst = tmp_path / "out"
overview.render_map(src, dst)
picture = Image.open(dst / "map.png")
assert picture.getpixel((20, 20)) == overview.SURFACE["field"]
assert picture.getpixel((70, 20)) == overview.SURFACE["court"]
assert picture.getpixel((20, 70)) == overview.SURFACE["pool"]
assert picture.getpixel((70, 70)) == overview.ROAD # Parkplatz in Straßenfarbe
assert picture.getpixel((102, 125)) == overview.PATH
assert Image.open(dst / "map-walkable.png").getpixel((25, 31)) == overview.WALKABLE # Weg auf Land