Laden: gelöschte Objekte (Pilze) beim Öffnen ersetzen/überspringen statt Abbruch (test.svmap wieder ladbar)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
fa2feee780
commit
075735f426
6 changed files with 203 additions and 2 deletions
|
|
@ -770,6 +770,51 @@ static func checksum_only_failure(errors: Array) -> bool:
|
|||
return true
|
||||
|
||||
|
||||
## true, wenn ein Load NUR an unbekannten Objekt-IDs (V7) und/oder der Prüfsumme
|
||||
## (V13) hängt — Kriterium für die Migration beim Öffnen (SvmapMigration).
|
||||
static func migrierbar(errors: Array) -> bool:
|
||||
if errors.is_empty():
|
||||
return false
|
||||
var v7 := false
|
||||
for e in errors:
|
||||
if str(e).begins_with("V7:"):
|
||||
v7 = true
|
||||
elif not str(e).begins_with("V13:"):
|
||||
return false
|
||||
return v7
|
||||
|
||||
|
||||
## Laden mit Migration veralteter Objekt-IDs: JSON parsen, unbekannte IDs per
|
||||
## SvmapMigration ersetzen/überspringen, Kern-Validierung, v2-Prüfsumme erneuern.
|
||||
## Rückgabe wie load_from_file plus "migration" (Bericht von migriere_objekte).
|
||||
static func load_from_file_migriert(path: String) -> Dictionary:
|
||||
var result := {"ok": false, "errors": [], "data": {}, "migration": {}}
|
||||
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
|
||||
result["migration"] = SvmapMigration.migriere_objekte(data, _manifest_or({}))
|
||||
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
|
||||
|
||||
|
||||
## 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
|
||||
|
|
|
|||
67
src/svmap/svmap_migration.gd
Normal file
67
src/svmap/svmap_migration.gd
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
extends RefCounted
|
||||
class_name SvmapMigration
|
||||
## Migration veralteter Objekt-IDs beim Laden (Karte 25.10.: test.svmap mit
|
||||
## gelöschten Pilz-Objekten ließ sich nicht mehr öffnen, V7).
|
||||
##
|
||||
## Regel: Eine ID, die nicht (mehr) in der Bibliothek steht, wird per Ersatz-Tabelle
|
||||
## durch ein passendes Pack-Objekt ersetzt (Position, Drehung, Skalierung, Gruppe
|
||||
## bleiben). Gibt es keinen Ersatz, wird das Objekt übersprungen und gemeldet —
|
||||
## nie ein Absturz, nie stilles Verschwinden. Die Datei selbst ändert sich erst
|
||||
## beim Speichern (Undo/Neu-Laden bleibt möglich).
|
||||
|
||||
## Präfix → Ersatz-ID (Pilze waren kleine kollisionslose Bodendeko → Gras 003).
|
||||
const ERSATZ_PRAEFIX := {
|
||||
"pilz_": "grass_003",
|
||||
}
|
||||
|
||||
|
||||
## Ersatz-ID für eine unbekannte ID ("" = keiner).
|
||||
static func ersatz_fuer(oid: String, lib: Dictionary) -> String:
|
||||
for praefix in ERSATZ_PRAEFIX:
|
||||
if oid.begins_with(praefix):
|
||||
var neu: String = ERSATZ_PRAEFIX[praefix]
|
||||
return neu if lib.has(neu) else ""
|
||||
return ""
|
||||
|
||||
|
||||
## Migriert data["objects"] in place. Rückgabe:
|
||||
## {"ersetzt": {alt_id: {"neu": id, "anzahl": n}}, "entfernt": {alt_id: n}, "gesamt": n}
|
||||
static func migriere_objekte(data: Dictionary, manifest: Dictionary) -> Dictionary:
|
||||
var bericht := {"ersetzt": {}, "entfernt": {}, "gesamt": 0}
|
||||
var objects: Variant = data.get("objects", null)
|
||||
if not (objects is Array):
|
||||
return bericht
|
||||
var lib: Dictionary = manifest.get("objects", {})
|
||||
var neu_liste: Array = []
|
||||
for obj in (objects as Array):
|
||||
if not (obj is Dictionary) or not (obj.get("id", null) is String) \
|
||||
or lib.has(obj["id"]):
|
||||
neu_liste.append(obj)
|
||||
continue
|
||||
var alt: String = obj["id"]
|
||||
var ersatz := ersatz_fuer(alt, lib)
|
||||
bericht["gesamt"] += 1
|
||||
if ersatz.is_empty():
|
||||
bericht["entfernt"][alt] = int(bericht["entfernt"].get(alt, 0)) + 1
|
||||
continue
|
||||
var kopie: Dictionary = (obj as Dictionary).duplicate(true)
|
||||
kopie["id"] = ersatz
|
||||
neu_liste.append(kopie)
|
||||
var eintrag: Dictionary = bericht["ersetzt"].get(alt, {"neu": ersatz, "anzahl": 0})
|
||||
eintrag["anzahl"] = int(eintrag["anzahl"]) + 1
|
||||
bericht["ersetzt"][alt] = eintrag
|
||||
if int(bericht["gesamt"]) > 0:
|
||||
data["objects"] = neu_liste
|
||||
return bericht
|
||||
|
||||
|
||||
## Menschenlesbare Zusammenfassung (deutsch) für die Statuszeile.
|
||||
static func zusammenfassung(bericht: Dictionary, lib: Dictionary) -> String:
|
||||
var teile: PackedStringArray = []
|
||||
for alt in bericht.get("ersetzt", {}):
|
||||
var e: Dictionary = bericht["ersetzt"][alt]
|
||||
var neu_name := str(lib.get(e["neu"], {}).get("name_de", e["neu"]))
|
||||
teile.append("%d× „%s“ durch „%s“ ersetzt" % [e["anzahl"], alt, neu_name])
|
||||
for alt in bericht.get("entfernt", {}):
|
||||
teile.append("%d× „%s“ übersprungen (kein Ersatz)" % [bericht["entfernt"][alt], alt])
|
||||
return ", ".join(teile)
|
||||
1
src/svmap/svmap_migration.gd.uid
Normal file
1
src/svmap/svmap_migration.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dhfmxbbnonapa
|
||||
|
|
@ -1279,6 +1279,13 @@ func _on_new_map_confirmed() -> void:
|
|||
|
||||
func _on_open_selected(path: String) -> void:
|
||||
var result := SvmapIO.load_from_file(path)
|
||||
var migration := {}
|
||||
if not result["ok"] and SvmapIO.migrierbar(result["errors"]):
|
||||
# Gelöschte Pack-Objekte (z. B. Pilze): ersetzen/überspringen statt Abbruch.
|
||||
var migriert := SvmapIO.load_from_file_migriert(path)
|
||||
if migriert["ok"]:
|
||||
result = migriert
|
||||
migration = migriert["migration"]
|
||||
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.
|
||||
|
|
@ -1297,13 +1304,16 @@ func _on_open_selected(path: String) -> void:
|
|||
# Alte Bau-Sektion „gebaeude“ (vor 25.10.) → normale Bauteil-Objekte in einer Gruppe.
|
||||
var uebernommen := BauOps.migriere_gebaeude(doc)
|
||||
current_path = path
|
||||
dirty = uebernommen > 0
|
||||
dirty = uebernommen > 0 or not migration.is_empty()
|
||||
history.begin(doc)
|
||||
_clear_edit_state()
|
||||
_refresh_all()
|
||||
_test_start_reload() # Karte t_96e3c4ee: Sidecar der geladenen Map lesen
|
||||
var map_name := str(doc.get("info", {}).get("name", "?"))
|
||||
if uebernommen > 0:
|
||||
if not migration.is_empty() and int(migration.get("gesamt", 0)) > 0:
|
||||
_set_status("Map „%s“ geladen. Veraltete Objekte angepasst: %s. Bitte prüfen und speichern — vorher bleibt die Datei unverändert." % [
|
||||
map_name, SvmapMigration.zusammenfassung(migration, SvmapIO._manifest_or({}).get("objects", {}))])
|
||||
elif uebernommen > 0:
|
||||
_set_status("Map „%s“ geladen. %d alte Bauteile wurden in das neue Bausystem übernommen (Gruppe „Gebäude“) — Wandhöhe ist jetzt 3 m, bitte kurz prüfen und speichern." % [map_name, uebernommen])
|
||||
else:
|
||||
_set_status("Map „%s“ geladen (%d Objekte). Klicke ein Objekt in der Karte an, um es anzupassen." % [map_name, SvmapDocument.object_count(doc)])
|
||||
|
|
|
|||
77
tests/test_migration.gd
Normal file
77
tests/test_migration.gd
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
extends SceneTree
|
||||
## Suite: Migration veralteter Objekt-IDs beim Laden (test.svmap-Fall, Pilze).
|
||||
## Ausführen: godot4 --headless --path . --script res://tests/test_migration.gd
|
||||
|
||||
var failures := 0
|
||||
var checks := 0
|
||||
const PFAD := "user://test_migration.svmap"
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
_run_tests()
|
||||
if FileAccess.file_exists(PFAD):
|
||||
DirAccess.remove_absolute(ProjectSettings.globalize_path(PFAD))
|
||||
if failures == 0:
|
||||
print("ALL OK: %d checks" % checks)
|
||||
quit(0)
|
||||
else:
|
||||
print("FAILED: %d/%d checks" % [failures, checks])
|
||||
quit(1)
|
||||
|
||||
|
||||
func _check(cond: bool, label: String) -> void:
|
||||
checks += 1
|
||||
if cond:
|
||||
print(" ok: %s" % label)
|
||||
else:
|
||||
failures += 1
|
||||
print(" FAIL: %s" % label)
|
||||
|
||||
|
||||
func _run_tests() -> void:
|
||||
var lib: Dictionary = SvmapIO._manifest_or({})
|
||||
_check(lib.get("objects", {}).has("grass_003"), "Ersatz-Objekt grass_003 steht in der Bibliothek")
|
||||
_check(not lib["objects"].has("pilz_austernseitling"), "Pilze sind aus der Bibliothek verschwunden")
|
||||
|
||||
# Map mit Pilz (ersetzbar), unbekanntem Objekt (kein Ersatz) und gültigem Objekt
|
||||
var doc := SvmapDocument.create_new("Migration", 256.0, "mittag")
|
||||
doc["objects"] = [
|
||||
{"id": "barrier_001", "pos": [1.0, 0.0, 1.0], "scale": 1.0, "rot_y_deg": 0.0},
|
||||
{"id": "pilz_austernseitling", "pos": [5.0, 0.0, 6.0], "scale": 2.0, "rot_y_deg": 33.0, "group": "g1"},
|
||||
{"id": "pilz_steinpilz", "pos": [7.0, 0.0, 8.0], "scale": 1.0, "rot_y_deg": 0.0},
|
||||
{"id": "gibt_es_nicht", "pos": [9.0, 0.0, 9.0], "scale": 1.0, "rot_y_deg": 0.0},
|
||||
]
|
||||
SvmapIO.sign_map(doc)
|
||||
var f := FileAccess.open(PFAD, FileAccess.WRITE)
|
||||
f.store_string(JSON.stringify(doc))
|
||||
f.close()
|
||||
|
||||
var normal := SvmapIO.load_from_file(PFAD)
|
||||
_check(not normal["ok"], "normales Laden scheitert an V7")
|
||||
_check(SvmapIO.migrierbar(normal["errors"]), "Fehler gelten als migrierbar (nur V7)")
|
||||
|
||||
var r := SvmapIO.load_from_file_migriert(PFAD)
|
||||
_check(r["ok"], "Laden mit Migration klappt: %s" % str(r["errors"]))
|
||||
var objs: Array = r["data"]["objects"]
|
||||
_check(objs.size() == 3, "3 Objekte (1 gültig + 2 Pilze ersetzt, 1 unbekanntes übersprungen) – ist %d" % objs.size())
|
||||
_check(objs[0]["id"] == "barrier_001", "gültiges Objekt unverändert, Reihenfolge bleibt")
|
||||
_check(objs[1]["id"] == "grass_003" and objs[2]["id"] == "grass_003", "Pilze → grass_003")
|
||||
_check(objs[1]["pos"] == [5.0, 0.0, 6.0] and float(objs[1]["scale"]) == 2.0
|
||||
and float(objs[1]["rot_y_deg"]) == 33.0 and objs[1]["group"] == "g1",
|
||||
"Position, Skalierung, Drehung und Gruppe bleiben erhalten")
|
||||
var m: Dictionary = r["migration"]
|
||||
_check(int(m["gesamt"]) == 3 and m["entfernt"].get("gibt_es_nicht", 0) == 1
|
||||
and m["ersetzt"]["pilz_steinpilz"]["anzahl"] == 1, "Bericht zählt Ersetzte und Übersprungene")
|
||||
_check(SvmapMigration.zusammenfassung(m, lib["objects"]).contains("übersprungen"), "Zusammenfassung nennt Übersprungene")
|
||||
_check(SvmapIO.validate_map(r["data"]).is_empty(), "migrierte Map besteht die volle Validierung (inkl. Prüfsumme)")
|
||||
|
||||
# Roundtrip: speichern → laden ohne Migration
|
||||
var s := SvmapIO.save_to_file(PFAD, r["data"])
|
||||
_check(s["ok"], "migrierte Map lässt sich speichern")
|
||||
_check(SvmapIO.load_from_file(PFAD)["ok"], "gespeicherte Datei lädt danach normal")
|
||||
|
||||
# Echter Fall: lokale test.svmap (falls vorhanden)
|
||||
if FileAccess.file_exists("res://test.svmap"):
|
||||
var t := SvmapIO.load_from_file_migriert(ProjectSettings.globalize_path("res://test.svmap"))
|
||||
_check(t["ok"], "test.svmap lädt mit Migration: %s" % str(t["errors"]))
|
||||
_check(int(t["migration"]["gesamt"]) == 6, "6 Pilze ersetzt")
|
||||
1
tests/test_migration.gd.uid
Normal file
1
tests/test_migration.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://pogsae4ri7b0
|
||||
Loading…
Reference in a new issue