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"]) == 1, "format_version 1") _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": 2}') _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": "crate_wood", "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": "crate_wood", "pos": [0.0, 0.6, 0.0], "rot_y_deg": 0.0, "scale": 2.0}) # crate_wood poly=12, scale²=4 → 48 _check(int(SvmapDocument.poly_usage(usage_doc, SvmapIO._manifest_or({}))) == 48, "poly_usage = poly × scale²") # 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)