132 lines
No EOL
4.2 KiB
GDScript
132 lines
No EOL
4.2 KiB
GDScript
extends RefCounted
|
|
class_name ActionHistory
|
|
## Zentrale Undo/Redo-Historie des Map-Editors (M2, Karte t_40d855d0).
|
|
##
|
|
## Die Map IST das svmap-Dictionary (1:1-Format, keine Kopie). Jede Aktion
|
|
## sichert die betroffenen OBER-Blöcke (z. B. "objects", "terrain",
|
|
## "spawn_zones") als tiefe Kopie VOR und NACH der Änderung. Undo setze die
|
|
## Blöcke zurück, Redo wendet "after" erneut an.
|
|
##
|
|
## So melden spätere Werkzeuge ihre Aktionen an (Terrain-Strokes, Spawn-Zonen):
|
|
## var before := history.snapshot(["terrain"]) # VOR der Änderung
|
|
## doc["terrain"][...] = ... # Änderung machen
|
|
## history.commit("Hügel geformt", before, history.snapshot(["terrain"]))
|
|
##
|
|
## Ober-Blöcke sind klein (Objekt-Liste/Spawn-Punkte) oder Base64-Strings
|
|
## (heightmap) — tiefe Kopien sind hier billig und bombenfest gegen
|
|
## Alias-Verwechslung, weil Godot-Arrays/Dicts sonst gegenseitig referenzieren.
|
|
|
|
const MAX_STEPS := 100
|
|
|
|
|
|
var doc: Dictionary = {}
|
|
var entries: Array = [] # Eintrag = {desc:String, merge:String, before:{Blk:Variante}, after:{Blk:Variante}}
|
|
var index := -1 # zuletzt angewandter Eintrag; -1 = nichts
|
|
|
|
|
|
func begin(document: Dictionary) -> void:
|
|
doc = document # Referenz — wir ändern dieselbe Map, keine Kopie
|
|
clear()
|
|
|
|
|
|
func clear() -> void:
|
|
entries.clear()
|
|
index = -1
|
|
|
|
|
|
## Tiefe Kopie der angegebenen Blöcke (VOR oder NACH einer Änderung).
|
|
## Skalare Ober-Keys (format_version, weather, …) werden als Wert kopiert —
|
|
## so kann ein Snapshot auch den GANZEN doc abdecken (Resize, R3).
|
|
func snapshot(keys: Array) -> Dictionary:
|
|
var out := {}
|
|
for key in keys:
|
|
if doc.has(key):
|
|
var value: Variant = doc[key]
|
|
if value is Array or value is Dictionary:
|
|
out[key] = value.duplicate(true)
|
|
else:
|
|
out[key] = value
|
|
return out
|
|
|
|
|
|
func can_undo() -> bool:
|
|
return index >= 0
|
|
|
|
|
|
func can_redo() -> bool:
|
|
return index + 1 < entries.size()
|
|
|
|
|
|
func undo_desc() -> String:
|
|
if not can_undo():
|
|
return ""
|
|
return str(entries[index].get("desc", ""))
|
|
|
|
|
|
func redo_desc() -> String:
|
|
if not can_redo():
|
|
return ""
|
|
return str(entries[index + 1].get("desc", ""))
|
|
|
|
|
|
## Eine fertige Änderung in die History einhängen.
|
|
## merge_tag: ungleich "" → identisch getagte Folge-Aufrufe (z. B. Drehen
|
|
## per Mausrad in schnellen Ticks) überschreiben den letzten Eintrag,
|
|
## statt die History zu fluten. commit immer direkt NACH der Doc-Änderung aufrufen.
|
|
func commit(desc: String, before: Dictionary, after: Dictionary, merge_tag: String = "") -> void:
|
|
if merge_tag != "" and index >= 0 and str(entries[index].get("merge", "")) == merge_tag:
|
|
entries[index]["after"] = after
|
|
return
|
|
entries.resize(index + 1) # redo-Zweig verwerfen (Standard-Undo-Semantik)
|
|
entries.append({"desc": desc, "merge": merge_tag, "before": before, "after": after})
|
|
if entries.size() > MAX_STEPS:
|
|
entries.remove_at(0) # älteste Aktion verdrängen
|
|
index = entries.size() - 1
|
|
|
|
|
|
## Letzte Aktion zurücknehmen. Rückgabe: Beschreibung ("" = nichts da).
|
|
## Restore läuft IN-PLACE: doc["objects"] etc. behalten ihre Identität (Array/
|
|
## Dict-Objekt wird geleert und neu befüllt) — bezogene Referenzen (Tests,
|
|
## Ansichten) werden nicht zu Stale-Schnappschüssen.
|
|
func undo() -> String:
|
|
if not can_undo():
|
|
return ""
|
|
var entry: Dictionary = entries[index]
|
|
for key in entry["before"]:
|
|
_restore_block(key, entry["before"][key])
|
|
index -= 1
|
|
return str(entry.get("desc", ""))
|
|
|
|
|
|
## Zurückgenommenes wiederherstellen. Rückgabe: Beschreibung ("" = nichts da).
|
|
func redo() -> String:
|
|
if not can_redo():
|
|
return ""
|
|
index += 1
|
|
var entry: Dictionary = entries[index]
|
|
for key in entry["after"]:
|
|
_restore_block(key, entry["after"][key])
|
|
return str(entry.get("desc", ""))
|
|
|
|
|
|
func _restore_block(key: String, value: Variant) -> void:
|
|
if value == null:
|
|
doc.erase(key)
|
|
return
|
|
# Ganzen Block (Array/Dict) in-place neu befüllen — Identität bleibt erhalten.
|
|
# Skalare (format_version, weather, …) werden als Wert gesetzt.
|
|
var source: Variant = value
|
|
if source is Array or source is Dictionary:
|
|
source = value.duplicate(true)
|
|
if source is Array:
|
|
var arr: Array = doc.get(key, [])
|
|
arr.clear()
|
|
arr.append_array(source)
|
|
doc[key] = arr
|
|
elif source is Dictionary:
|
|
var dict: Dictionary = doc.get(key, {})
|
|
dict.clear()
|
|
dict.merge(source)
|
|
doc[key] = dict
|
|
else:
|
|
doc[key] = source |