svmap: Prüfsummen-Repair-Dialog „Trotzdem laden“ (t_9814f18f)

This commit is contained in:
Hermes (scopedvr-dev) 2026-10-20 22:05:53 +00:00
parent 65b33a207a
commit 35a1d80d3e
4 changed files with 194 additions and 1 deletions

View file

@ -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

View file

@ -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")
_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()

View file

@ -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)")

View file

@ -0,0 +1 @@
uid://cw5pjgvecdew0