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():
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -2677,3 +2696,27 @@ 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")
|
||||
|
||||
|
||||
# -------------------------------- 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