diff --git a/src/svmap/svmap_io.gd b/src/svmap/svmap_io.gd index 4121242..308c9d1 100644 --- a/src/svmap/svmap_io.gd +++ b/src/svmap/svmap_io.gd @@ -547,3 +547,51 @@ static func encode_materials(mats: PackedByteArray) -> String: for i in mats.size(): bytes[i] = clampi(mats[i], 0, MATERIAL_IDS.size() - 1) return Marshalls.raw_to_base64(bytes) + + +# ------------------------------------- Fremdformat-Dialog (t_9814f18f) ------ + +## true, wenn ein Load NUR an der Pruefsumme haengt (V13) — die Kern-Validierung +## (V3–V16) ist gruen. Kriterium fuer den „Trotzdem laden (Pruefsumme erneuern)“- +## Dialog im Editor: die Datei kommt aus einem fremden Writer (FirstFight-Fall +## 20.10.: Python-repr-Floats), der Inhalt selbst ist intakt. +static func checksum_only_failure(errors: Array) -> bool: + if errors.is_empty(): + return false + for e in errors: + if not str(e).begins_with("V13:"): + return false + return true + + +## Laden mit Pruefsummen-Erneuerung (t_9814f18f, „Trotzdem laden“-Weg): +## Datei lesen + JSON parsen, dann NUR die Kern-Validierung (V3–V16 ohne +## Pruefsumme). Ist die gruen, traegt sign_map() die v2-Pruefsumme ins Dokument +## ein — beim naechsten Speichern landet sie in der Datei. Schlaegt die +## Kern-Validierung durch, bleibt der harte Abbruch (echter Schaden). +## Rueckgabe wie load_from_file: {ok, errors, data}. +static func load_from_file_resign(path: String) -> Dictionary: + var result := {"ok": false, "errors": [], "data": {}} + if not FileAccess.file_exists(path): + _add(result, "Datei nicht gefunden: %s" % path) + return result + var fa := FileAccess.open(path, FileAccess.READ) + if fa == null: + _add(result, "Datei nicht lesbar: %s (Fehler %d)" % [path, FileAccess.get_open_error()]) + return result + if fa.get_length() > MAX_FILE_BYTES: + _add(result, "V1: Map-Datei zu gross (%d Bytes, Limit %d)." % [fa.get_length(), MAX_FILE_BYTES]) + return result + var parsed: Variant = JSON.parse_string(fa.get_as_text()) + if parsed == null or not (parsed is Dictionary): + _add(result, "V2: Kein gueltiges JSON-Objekt (Datei beschaedigt oder keine .svmap?).") + return result + var data: Dictionary = parsed + var errors := _validate_core(data) + if not errors.is_empty(): + result["errors"] = Array(errors) + return result + sign_map(data) + result["ok"] = true + result["data"] = data + return result diff --git a/src/ui/main.gd b/src/ui/main.gd index 7157009..bd93ee1 100644 --- a/src/ui/main.gd +++ b/src/ui/main.gd @@ -150,6 +150,8 @@ var world3d: Node3D # 3D-Wurzel im SubViewport (Spieler lebt hier) # Karte 3/3 (t_11b5c25b): Dirty-Schutz + Kurzhilfe + Werkzeug-Statuszeile. var dirty_dialog: ConfirmationDialog # „Ungespeicherte Änderungen verwerfen?“ +var resign_dialog: ConfirmationDialog # „Trotzdem laden (Prüfsumme erneuern)“ (t_9814f18f) +var _resign_pending_path: String = "" var _pending_after_dirty := Callable() # was nach „Verwerfen“ ausgeführt wird var help_overlay: PanelContainer # H-Kurzhilfe (nicht-blockierend) var help_overlay_title: Label @@ -499,6 +501,17 @@ func _build_dialogs_and_overlays() -> void: dirty_dialog.confirmed.connect(_on_dirty_confirmed) add_child(dirty_dialog) + # Fremdformat-Dialog (t_9814f18f): Lade-Fehler NUR an der Prüfsumme (V13), + # Kern-Validierung grün → „Trotzdem laden (Prüfsumme erneuern)“ statt hartem + # Abbruch; der Editor signiert das Dokument dann mit dem v2-Schema neu. + resign_dialog = ConfirmationDialog.new() + resign_dialog.title = "Prüfsumme stimmt nicht" + resign_dialog.dialog_text = "Der Inhalt der Map ist intakt, aber die Prüfsumme stimmt nicht (die Datei wurde von einem fremden Programm geschrieben oder anders formatiert).\n\nTrotzdem laden? Die Prüfsumme wird beim nächsten Speichern erneuert." + resign_dialog.ok_button_text = "Trotzdem laden (Prüfsumme erneuern)" + resign_dialog.cancel_button_text = "Abbrechen" + resign_dialog.confirmed.connect(_on_resign_confirmed) + add_child(resign_dialog) + # Kurzhilfe-Overlay (Karte 3/3): H schaltet es an, H/Esc aus. Kein modal # Dialog — der Editor bleibt bedienbar, das Panel liegt nur obenauf. help_overlay = PanelContainer.new() @@ -1188,6 +1201,12 @@ func _on_new_map_confirmed() -> void: func _on_open_selected(path: String) -> void: var result := SvmapIO.load_from_file(path) if not result["ok"]: + # t_9814f18f: NUR die Prüfsumme (V13) schlägt fehl, Kern (V3–V16) grün → + # Dialog „Trotzdem laden (Prüfsumme erneuern)“ statt hartem Abbruch. + if SvmapIO.checksum_only_failure(result["errors"]): + _resign_pending_path = path + resign_dialog.popup_centered() + return _show_errors("Map konnte NICHT geladen werden:\n(Falls sie in einem anderen Programm geändert wurde, prüfe die Datei.)", result["errors"]) _set_status("Laden fehlgeschlagen: %s — versuche eine .svmap-Datei aus dem Map-Editor oder vom Server." % path.get_file(), true) return @@ -2676,4 +2695,28 @@ func _sync_test_start_panel_visibility() -> void: ## Leisten-Knopf „🎯 Test-Start" (registry.gd ACTIONS) — schaltet das Werkzeug um. func _on_test_start_tool_pressed() -> void: - _set_tool_mode("objects" if tool_mode == "teststart" else "teststart") \ No newline at end of file + _set_tool_mode("objects" if tool_mode == "teststart" else "teststart") + + +# -------------------------------- Fremdformat-Dialog (t_9814f18f) ----------- + +## „Trotzdem laden (Prüfsumme erneuern)“ bestätigt: Datei NUR gegen die +## Kern-Validierung (V3–V16) prüfen, v2-Prüfsumme ins Dokument schreiben und +## normal öffnen. dirty=true: das Dokument weicht von der Datei ab — der +## nächste Speicher-Vorgang schreibt das v2-Schema in die Datei. +func _on_resign_confirmed() -> void: + var result := SvmapIO.load_from_file_resign(_resign_pending_path) + if not result["ok"]: + _show_errors("Map konnte NICHT geladen werden (auch ohne Prüfsummen-Prüfung):", result["errors"]) + _set_status("Laden fehlgeschlagen: %s" % _resign_pending_path.get_file(), true) + return + doc = result["data"] + current_path = _resign_pending_path + dirty = true + history.begin(doc) + _clear_edit_state() + _refresh_all() + _test_start_reload() + var map_name := str(doc.get("info", {}).get("name", "?")) + _set_status("Map „%s“ geladen — Prüfsumme erneuert (v2). Speichere die Map, um die neue Prüfsumme zu übernehmen." % map_name) + _refresh_file_label() diff --git a/tests/test_svmap_resign_dialog.gd b/tests/test_svmap_resign_dialog.gd new file mode 100644 index 0000000..f795afc --- /dev/null +++ b/tests/test_svmap_resign_dialog.gd @@ -0,0 +1,101 @@ +extends SceneTree +## Suite: Prüfsummen-Fremdformat-Dialog (t_9814f18f). +## Ausführen: godot4 --headless --path . --script res://tests/test_svmap_resign_dialog.gd +## Exit 0 = alles grün. Läuft OHNE UI (nur SvmapIO/SvmapDocument). +## +## Deckt den Sly-Fall ab: Datei von fremdem Writer (V13 schlägt fehl, +## Kern V3–V16 grün) → checksum_only_failure → load_from_file_resign +## akzeptiert + signiert v2 + Roundtrip bleibt stabil. + +var failures := 0 +var checks := 0 + + +func _init() -> void: + _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_resign.svmap", "user://test_resign2.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("== Fremdformat-Dialog: Erkennung + Resign-Weg (t_9814f18f) ==") + var doc := SvmapDocument.create_new("Resign-Test", 512.0, "mittag") + _check(doc.has("format_version"), "Test-Dokument erzeugt") + + # 1) Normaler Save + Load. + var save := SvmapIO.save_to_file("user://test_resign.svmap", doc) + _check(save["ok"], "Baseline-Map gespeichert (v2)") + + # 2) Prüfsumme manipulieren (64 Nullen) → V13-only-Fehler. + var text := FileAccess.get_file_as_string("user://test_resign.svmap") + var parsed: Variant = JSON.parse_string(text) + var bad: Dictionary = parsed + bad["checksum"]["value"] = "0".repeat(64) + var fa := FileAccess.open("user://test_resign.svmap", FileAccess.WRITE) + fa.store_string(JSON.stringify(bad, " ")) + fa.close() + var broken := SvmapIO.load_from_file("user://test_resign.svmap") + _check(not broken["ok"], "manipulierte Prüfsumme → Load schlägt fehl") + _check(_errors_contain(broken["errors"], "V13"), "Fehler ist V13 (Prüfsumme)") + _check(SvmapIO.checksum_only_failure(broken["errors"]), "checksum_only_failure erkennt V13-only-Fall") + + # 3) Kern-defekte Map (Name leer) → NICHT als V13-only erkennbar. + var core_bad: Dictionary = JSON.parse_string(text) + core_bad["info"]["name"] = "" + var fa2 := FileAccess.open("user://test_resign2.svmap", FileAccess.WRITE) + fa2.store_string(JSON.stringify(core_bad, " ")) + fa2.close() + var core := SvmapIO.load_from_file("user://test_resign2.svmap") + _check(not core["ok"], "Kern-defekte Map (Name leer) schlägt fehl") + _check(not SvmapIO.checksum_only_failure(core["errors"]), "Kern-Fehler ist KEIN V13-only-Fall (harter Abbruch bleibt)") + + # 4) Resign-Weg: Datei trotz manipulierter Prüfsumme laden. + var resign := SvmapIO.load_from_file_resign("user://test_resign.svmap") + _check(resign["ok"], "load_from_file_resign akzeptiert V13-only-Datei") + _check(str(resign["data"]["checksum"]["schema"]) == "v2", "Resign trägt schema v2") + + # 5) Resign-Dokument speichern + laden → stabiler v2-Roundtrip. + var resave := SvmapIO.save_to_file("user://test_resign.svmap", resign["data"]) + _check(resave["ok"], "Resign-Dokument gespeichert") + var reload := SvmapIO.load_from_file("user://test_resign.svmap") + _check(reload["ok"], "Resign-Map lädt normal (Roundtrip)") + _check(str(reload["data"]["checksum"]["schema"]) == "v2", "Datei trägt v2 nach Resave") + + # 6) Adversarial-Float im Sly-Fall: 15-stelliger Wert überlebt Resign+Save+Load. + var adv: Dictionary = JSON.parse_string(FileAccess.get_file_as_string("user://test_resign.svmap")) + if (adv["objects"] as Array).size() > 0: + adv["objects"][0]["pos"][1] = 0.000457770656900891 + var adv_save := SvmapIO.save_to_file("user://test_resign.svmap", adv) + _check(adv_save["ok"], "Map mit adversarial Float gespeichert (Quantisierung)") + var adv_load := SvmapIO.load_from_file("user://test_resign.svmap") + _check(adv_load["ok"], "Map mit adversarial Float lädt (v2-Hash formatunabhängig)") + else: + _check(true, "kein Objekt im Default-Dokument — Float-Check entfällt (v2-Suite deckt adversarial ab)") diff --git a/tests/test_svmap_resign_dialog.gd.uid b/tests/test_svmap_resign_dialog.gd.uid new file mode 100644 index 0000000..2cc8ee9 --- /dev/null +++ b/tests/test_svmap_resign_dialog.gd.uid @@ -0,0 +1 @@ +uid://cw5pjgvecdew0