svmap: Prüfsummen-Repair-Dialog „Trotzdem laden“ (t_9814f18f)
This commit is contained in:
parent
65b33a207a
commit
35a1d80d3e
4 changed files with 194 additions and 1 deletions
|
|
@ -547,3 +547,51 @@ static func encode_materials(mats: PackedByteArray) -> String:
|
||||||
for i in mats.size():
|
for i in mats.size():
|
||||||
bytes[i] = clampi(mats[i], 0, MATERIAL_IDS.size() - 1)
|
bytes[i] = clampi(mats[i], 0, MATERIAL_IDS.size() - 1)
|
||||||
return Marshalls.raw_to_base64(bytes)
|
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
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,8 @@ var world3d: Node3D # 3D-Wurzel im SubViewport (Spieler lebt hier)
|
||||||
|
|
||||||
# Karte 3/3 (t_11b5c25b): Dirty-Schutz + Kurzhilfe + Werkzeug-Statuszeile.
|
# Karte 3/3 (t_11b5c25b): Dirty-Schutz + Kurzhilfe + Werkzeug-Statuszeile.
|
||||||
var dirty_dialog: ConfirmationDialog # „Ungespeicherte Änderungen verwerfen?“
|
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 _pending_after_dirty := Callable() # was nach „Verwerfen“ ausgeführt wird
|
||||||
var help_overlay: PanelContainer # H-Kurzhilfe (nicht-blockierend)
|
var help_overlay: PanelContainer # H-Kurzhilfe (nicht-blockierend)
|
||||||
var help_overlay_title: Label
|
var help_overlay_title: Label
|
||||||
|
|
@ -499,6 +501,17 @@ func _build_dialogs_and_overlays() -> void:
|
||||||
dirty_dialog.confirmed.connect(_on_dirty_confirmed)
|
dirty_dialog.confirmed.connect(_on_dirty_confirmed)
|
||||||
add_child(dirty_dialog)
|
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
|
# Kurzhilfe-Overlay (Karte 3/3): H schaltet es an, H/Esc aus. Kein modal
|
||||||
# Dialog — der Editor bleibt bedienbar, das Panel liegt nur obenauf.
|
# Dialog — der Editor bleibt bedienbar, das Panel liegt nur obenauf.
|
||||||
help_overlay = PanelContainer.new()
|
help_overlay = PanelContainer.new()
|
||||||
|
|
@ -1188,6 +1201,12 @@ func _on_new_map_confirmed() -> void:
|
||||||
func _on_open_selected(path: String) -> void:
|
func _on_open_selected(path: String) -> void:
|
||||||
var result := SvmapIO.load_from_file(path)
|
var result := SvmapIO.load_from_file(path)
|
||||||
if not result["ok"]:
|
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"])
|
_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)
|
_set_status("Laden fehlgeschlagen: %s — versuche eine .svmap-Datei aus dem Map-Editor oder vom Server." % path.get_file(), true)
|
||||||
return
|
return
|
||||||
|
|
@ -2676,4 +2695,28 @@ func _sync_test_start_panel_visibility() -> void:
|
||||||
|
|
||||||
## Leisten-Knopf „🎯 Test-Start" (registry.gd ACTIONS) — schaltet das Werkzeug um.
|
## Leisten-Knopf „🎯 Test-Start" (registry.gd ACTIONS) — schaltet das Werkzeug um.
|
||||||
func _on_test_start_tool_pressed() -> void:
|
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()
|
||||||
|
|
|
||||||
101
tests/test_svmap_resign_dialog.gd
Normal file
101
tests/test_svmap_resign_dialog.gd
Normal 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)")
|
||||||
1
tests/test_svmap_resign_dialog.gd.uid
Normal file
1
tests/test_svmap_resign_dialog.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cw5pjgvecdew0
|
||||||
Loading…
Reference in a new issue