- 423 Objekte in 19 deutschen Kategorien (Panzer, Radfahrzeuge, Flugzeuge, Gebaeude, Zelte, Tuerme, Mauern, Sperren, Kisten, Technik, Felsen, Boden, Decals ...); Gruppen "Bauwerke" und "Kisten & Barrieren" samt Test-Asset-Namenslisten entfernt - LibraryBrowser: Gruppen/Namen aus dem Manifest (kategorie, name_de), unbekannte Kategorien bekommen automatisch eine eigene Gruppe - Thumbs neu, library/ synchronisiert (inkl. neuer Kollision) - Neue Identitaetsprobe: Kollision ALLER Objekte Editor = Client = Server - Tests auf Pack-Objekte umgestellt Co-Authored-By: Claude <noreply@anthropic.com>
168 lines
7.5 KiB
GDScript
168 lines
7.5 KiB
GDScript
extends SceneTree
|
||
## Editor-Tests (headless): Karte t_5badf371 — svmap laden/speichern.
|
||
## Ausführen: godot --headless --path . --script res://tests/test_editor_svmap.gd
|
||
## Exit 0 = alles grün. Läuft OHNE UI (nur SvmapIO/SvmapDocument).
|
||
|
||
var failures := 0
|
||
var checks := 0
|
||
|
||
|
||
func _init() -> void:
|
||
# user:// persistiert über Godot-Läufe — vor UND nach dem Test aufräumen
|
||
# (Lektion aus t_74976e9f: Stale-Reste maskierten sonst Verifikationen).
|
||
_cleanup()
|
||
_run_tests()
|
||
_cleanup()
|
||
if failures == 0:
|
||
print("ALL OK: %d checks" % checks)
|
||
quit(0)
|
||
else:
|
||
print("FAILED: %d/%d checks" % [failures, checks])
|
||
quit(1)
|
||
|
||
|
||
func _cleanup() -> void:
|
||
for path in ["user://test_roundtrip.svmap", "user://test_roundtrip2.svmap"]:
|
||
if FileAccess.file_exists(path):
|
||
DirAccess.remove_absolute(ProjectSettings.globalize_path(path))
|
||
|
||
|
||
func _check(cond: bool, label: String) -> void:
|
||
checks += 1
|
||
if cond:
|
||
print(" ok: %s" % label)
|
||
else:
|
||
failures += 1
|
||
print(" FAIL: %s" % label)
|
||
|
||
|
||
func _errors_contain(errors: Array, needle: String) -> bool:
|
||
for e in errors:
|
||
if str(e).contains(needle):
|
||
return true
|
||
return false
|
||
|
||
|
||
func _run_tests() -> void:
|
||
print("== Neue Map: default-Dokument ==")
|
||
var doc := SvmapDocument.create_new("Testmap", 1024.0, "abend")
|
||
_check(int(doc["format_version"]) == 2, "format_version 2 (Material-Schicht-Format)")
|
||
_check(str(doc["info"]["name"]) == "Testmap", "Name übernommen")
|
||
_check(str(doc["weather"]) == "abend", "Wetter übernommen")
|
||
_check((doc["objects"] as Array).is_empty(), "leere Objektliste")
|
||
_check((doc["spawn_zones"]["0"]["points"] as Array).size() == 4, "Team 0: 4 Default-Spawns")
|
||
_check((doc["spawn_zones"]["1"]["points"] as Array).size() == 4, "Team 1: 4 Default-Spawns")
|
||
var terrain: Dictionary = doc["terrain"]
|
||
_check(int(terrain["resolution"]) == 129, "Default-Auflösung 129 (Editor-MVP-Perf-Limit, Karte t_9aba04f1)")
|
||
var raw := Marshalls.base64_to_raw(str(terrain["heightmap_r16_b64"]))
|
||
_check(raw.size() == 129 * 129 * 2, "Höhenfeld-Länge = resolution² × 2")
|
||
var flat := true
|
||
for i in raw.size():
|
||
if raw[i] != 0:
|
||
flat = false
|
||
break
|
||
_check(flat, "Default-Höhenfeld flach (alles 0)")
|
||
|
||
print("== Speichern (Validierung vor dem Schreiben) ==")
|
||
var save1 := SvmapIO.save_to_file("user://test_roundtrip.svmap", doc)
|
||
_check(save1["ok"], "save_to_file ok: %s" % str(save1["errors"]))
|
||
|
||
print("== Roundtrip: speichern → laden → speichern → byte-identisch ==")
|
||
var load1 := SvmapIO.load_from_file("user://test_roundtrip.svmap")
|
||
_check(load1["ok"], "load_from_file ok: %s" % str(load1["errors"]))
|
||
var doc2: Dictionary = load1["data"]
|
||
_check(str(doc2["info"]["name"]) == "Testmap", "Name im Roundtrip erhalten")
|
||
_check(str(doc2["weather"]) == "abend", "Wetter im Roundtrip erhalten")
|
||
_check((doc2["spawn_zones"]["1"]["points"] as Array).size() == 4, "Spawns im Roundtrip erhalten")
|
||
var save2 := SvmapIO.save_to_file("user://test_roundtrip2.svmap", doc2)
|
||
_check(save2["ok"], "Re-Save ok")
|
||
var text1 := FileAccess.get_file_as_string("user://test_roundtrip.svmap")
|
||
var text2 := FileAccess.get_file_as_string("user://test_roundtrip2.svmap")
|
||
_check(text1 == text2, "Dateien byte-identisch (Prüfsumme stabil über save/load/save)")
|
||
|
||
print("== Server-Kompatibilität: Editor-Prüfsumme == Loader-Kanonisierung ==")
|
||
# Der Loader im Match-Server nutzt exakt: kanonisieren → stringify(„ ") →
|
||
# sha256_text. Der Editor muss denselben Hash erzeugen (geprüft an der
|
||
# geladenen Map, die ein checksum-Feld trägt):
|
||
var doc_signed: Dictionary = _deep_copy(doc2)
|
||
SvmapIO.sign_map(doc_signed)
|
||
var payload: Dictionary = SvmapIO._canonicalize(doc_signed)
|
||
payload.erase("checksum")
|
||
var expected: String = JSON.stringify(payload, " ").sha256_text()
|
||
_check(str(doc_signed["checksum"]["value"]) == expected, "sign_map-Hash = Kanonisierungs-Hash")
|
||
|
||
print("== Negative Fälle (Ablehnung mit verständlicher Meldung) ==")
|
||
var res: Dictionary
|
||
|
||
res = SvmapIO.parse_and_validate("{{{definitiv kaputt")
|
||
_check(not res["ok"] and _errors_contain(res["errors"], "V2"), "kaputtes JSON → V2")
|
||
|
||
res = SvmapIO.parse_and_validate('{"format_version": 3}')
|
||
_check(not res["ok"] and _errors_contain(res["errors"], "V3"), "falsche Version → V3")
|
||
|
||
var bad: Dictionary = _deep_copy(doc2)
|
||
bad["weather"] = "sturm"
|
||
res = SvmapIO.parse_and_validate(JSON.stringify(bad))
|
||
_check(not res["ok"] and _errors_contain(res["errors"], "V12"), "unbekanntes Wetter → V12")
|
||
|
||
bad = _deep_copy(doc2)
|
||
(bad["spawn_zones"]["0"]["points"] as Array)[1]["id"] = "A1"
|
||
res = SvmapIO.parse_and_validate(JSON.stringify(bad))
|
||
_check(not res["ok"] and _errors_contain(res["errors"], "V11"), "Spawn-ID-Dublette → V11")
|
||
|
||
bad = _deep_copy(doc2)
|
||
(bad["spawn_zones"]["1"]["points"] as Array).resize(3)
|
||
res = SvmapIO.parse_and_validate(JSON.stringify(bad))
|
||
_check(not res["ok"] and _errors_contain(res["errors"], "V11"), "zu wenige Spawns → V11")
|
||
|
||
bad = _deep_copy(doc2)
|
||
(bad["objects"] as Array).append({"id": "taube_aus_dem_wald", "pos": [0.0, 1.0, 0.0], "rot_y_deg": 0.0, "scale": 1.0})
|
||
res = SvmapIO.parse_and_validate(JSON.stringify(bad))
|
||
_check(not res["ok"] and _errors_contain(res["errors"], "V7"), "unbekannte Objekt-ID → V7")
|
||
|
||
bad = _deep_copy(doc2)
|
||
(bad["objects"] as Array).append({"id": "box_001", "pos": [99999.0, 0.0, 0.0], "rot_y_deg": 0.0, "scale": 1.0})
|
||
res = SvmapIO.parse_and_validate(JSON.stringify(bad))
|
||
_check(not res["ok"] and _errors_contain(res["errors"], "V9"), "Objekt außerhalb der Grenzen → V9")
|
||
|
||
bad = _deep_copy(doc2)
|
||
bad["terrain"]["resolution"] = 700
|
||
res = SvmapIO.parse_and_validate(JSON.stringify(bad))
|
||
_check(not res["ok"] and _errors_contain(res["errors"], "V6"), "Auflösung zu groß → V6")
|
||
|
||
bad = _deep_copy(doc2)
|
||
bad["terrain"]["heightmap_r16_b64"] = "AAAA"
|
||
res = SvmapIO.parse_and_validate(JSON.stringify(bad))
|
||
_check(not res["ok"] and _errors_contain(res["errors"], "V5"), "Höhenfeld falsche Länge → V5")
|
||
|
||
bad = _deep_copy(doc2)
|
||
bad["checksum"]["value"] = "0".repeat(64)
|
||
res = SvmapIO.parse_and_validate(JSON.stringify(bad))
|
||
_check(not res["ok"] and _errors_contain(res["errors"], "V13"), "manipulierte Prüfsumme → V13")
|
||
|
||
bad = _deep_copy(doc2)
|
||
bad["info"]["author"] = ""
|
||
res = SvmapIO.parse_and_validate(JSON.stringify(bad))
|
||
_check(not res["ok"] and _errors_contain(res["errors"], "V4"), "leerer Autor → V4")
|
||
|
||
print("== Poly-Budget (injiziertes Manifest) ==")
|
||
var mega_manifest: Dictionary = {"objects": {"mega": {"poly": 2_000_000, "collision": {"shape": "box", "size": [1, 1, 1]}}}}
|
||
var budget_doc := SvmapDocument.create_new("Budget", 512.0, "mittag")
|
||
(budget_doc["objects"] as Array).append({"id": "mega", "pos": [0.0, 1.0, 0.0], "rot_y_deg": 0.0, "scale": 1.0})
|
||
res = SvmapIO.parse_and_validate(JSON.stringify(budget_doc), mega_manifest)
|
||
_check(not res["ok"] and _errors_contain(res["errors"], "V8"), "Poly-Budget überschritten → V8")
|
||
|
||
print("== Poly-Nutzung (Editor-Budgetanzeige) ==")
|
||
var usage_doc := SvmapDocument.create_new("Usage", 512.0, "mittag")
|
||
(usage_doc["objects"] as Array).append({"id": "box_001", "pos": [0.0, 0.6, 0.0], "rot_y_deg": 0.0, "scale": 2.0})
|
||
# poly(box_001) aus der Registry, scale²=4
|
||
var box_poly := int((SvmapIO._manifest_or({})["objects"] as Dictionary)["box_001"]["poly"])
|
||
_check(int(SvmapDocument.poly_usage(usage_doc, SvmapIO._manifest_or({}))) == box_poly * 4, "poly_usage = poly × scale² (%d)" % (box_poly * 4))
|
||
|
||
# Sentinel: falls ein Script-Error einen Abschnitt abbricht, fehlt dieser
|
||
# Check → kein falsches „ALL OK".
|
||
_check(true, "Alle Testabschnitte liefen bis zum Ende durch")
|
||
|
||
|
||
func _deep_copy(value: Variant) -> Variant:
|
||
return SvmapIO._canonicalize(value)
|