extends Node
## GATE — FLATTEN HISTORY for one map (2026-10-05: "we need a way to purge history
## permanently" — the user's Home had 2460 level records, 1983 from one accidental
## Subtract, and every fold and the History panel scan them all).
##
## LevelPersistence.flatten_map(): the map's journal is replaced by the MINIMUM records
## that rebuild what is there now, written as non-undoable BASE records (`base: true`:
## History and undo skip them — an undo must never reach into a flattened map), and
## every old record of the map is erased from the room's state. A backup of the old
## records is written first.
##
## 1. the record count drops
## 2. the live GEOMETRY (planes, material, texture, clearance — names aside) is
## identical after a refold
## 3. every object's type, transform and props are identical
## 4. History is empty, and Ctrl+Z changes nothing
## 5. the backup holds every old record
##
## RUN AS A SCENE: "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_flatten_history.tscn
var fails := 0
var checks := 0
func _ok(what: String, cond: bool, note: String = "") -> void:
checks += 1
if not cond:
fails += 1
print(" %-62s %s %s" % [what, "OK" if cond else "**FAIL**", note])
func _box(lo: Vector3, hi: Vector3) -> Array:
return [1, 0, 0, hi.x, -1, 0, 0, -lo.x, 0, 1, 0, hi.y, 0, -1, 0, -lo.y, 0, 0, 1, hi.z, 0, 0, -1, -lo.z]
## Live brushes as a sorted multiset of their content, names aside.
static func geometry(level: Node) -> PackedStringArray:
var out := PackedStringArray()
for id: int in (level.get_live_brush_ids() as PackedInt32Array):
var b: Dictionary = level.get_brush(id)
var pl: PackedFloat32Array = b.get("planes", PackedFloat32Array())
var parts := PackedStringArray()
for k in range(0, pl.size(), 4):
parts.append("%.4f,%.4f,%.4f,%.4f" % [pl[k], pl[k + 1], pl[k + 2], pl[k + 3]])
parts.sort()
out.append("%s|m%d|c%s|s%s|t%s" % [";".join(parts), int(b.get("material_id", 0)),
str(b.get("clearance", false)), str(b.get("sky", false)), var_to_str(b.get("face_tex", []))])
out.sort()
return out
static func objects_of(objs: Node) -> PackedStringArray:
var out := PackedStringArray()
for e: Dictionary in (objs.entries() as Array):
var n := e["node"] as Node3D
var t: Transform3D = objs.scene_xform(n)
var p: Dictionary = objs.props(String(e["dot"]))
var keys := p.keys()
keys.sort()
var ps := PackedStringArray()
for k: Variant in keys:
ps.append("%s=%s" % [k, var_to_str(p[k])])
out.append("%s@%.3f,%.3f,%.3f|%s|%s" % [e["type"], t.origin.x, t.origin.y, t.origin.z,
"%.3f,%.3f,%.3f" % [t.basis.x.x, t.basis.y.y, t.basis.z.z], ",".join(ps)])
out.sort()
return out
func _ready() -> void:
var gs: Node = get_tree().root.get_node_or_null("/root/GameState")
gs.install_routes()
var keet: Node = get_tree().root.get_node_or_null("/root/KeetService")
if keet != null:
keet.current_member_id = "gate-author"
keet.current_role = "admin"
var map: Node3D = (load("res://scenes/maps/map_dev_test.tscn") as PackedScene).instantiate()
add_child(map)
for i in 6:
await get_tree().process_frame
_run(map)
if checks < 9:
print(" **FAIL** only %d checks ran -- setup died before the assertions" % checks)
fails += 1
print("\nGATE flatten_history: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
get_tree().quit(0 if fails == 0 else 1)
func _run(map: Node3D) -> void:
var level: Node = map.get("level")
var persist: Node = map.get("_persist")
var objs: Node = map.get_node("SceneObjects")
# ---- a real history ----
var made: Array = []
for i in 6:
made.append(persist.record("brush.create", {"planes": _box(Vector3(60 + i * 3, 0, 0), Vector3(62 + i * 3, 2, 2)),
"proj": [], "material_id": i % 3, "level_id": 0, "sky": false}))
persist.refold()
map.delete_brushes([level.brush_by_key(made[0])]) # a created brush deleted
map.select_brushes(PackedInt32Array([level.brush_by_key("i:1:3")])) # Platform1 (a BASE brush)
map.translate_selection(Vector3(0, 0, 2)) # ... moved
map.scale_brushes([level.brush_by_key("i:1:5")], Vector3(1, 2, 1), Vector3(10, 3, 5)) # Platform3 scaled
map.delete_brushes([level.brush_by_key("i:1:7")]) # Wall1 (base) deleted
persist.record("face.tex", {"brush_key": made[1], "face_keys": ["i:0", "i:1"],
"tex": {"tex": "kenney-prototype/dark_01", "mode": 1}, "mask": 0}) # a retexture
var sdot: String = persist.record("shape.create", {"type": "stairs", "gv": 1, "params": {"steps": 4},
"frame": SceneObjects.xform_to(Transform3D(Basis.IDENTITY, Vector3(0, 0, 70))), "size": [4.0, 2.0, 2.0],
"material_id": 0, "level_id": 0})
persist.record("shape.set", {"target": sdot, "params": {"steps": 6}}) # a shape edited
persist.refold()
var od: String = map.create_object("marker", Transform3D(Basis.IDENTITY, Vector3(1, 1, 1)))
map.move_object(od, Transform3D(Basis.IDENTITY, Vector3(2, 1, 1)))
map.set_object_props(od, {"tint": [0.1, 0.2, 0.3]})
map.delete_objects(["scene:TeleporterToFlat"]) # a base object deleted
map.move_object("scene:SpawnPoint1", Transform3D(Basis.IDENTITY, Vector3(-5, 1, -9))) # a base object moved
persist.refold()
persist.undo_last() # some history is undone
persist.refold()
# A level-source key (world:level:<a>:source) is not history: it must survive. map_test
# imports nothing, so the gate plants one.
gs_set_source()
var src0: int = _source_keys()
var n0: int = persist.records().size()
var g0 := geometry(level)
var o0 := objects_of(objs)
_ok("setup: a history to flatten (%d records)" % n0, n0 >= 15)
var r: Dictionary = persist.flatten_map()
persist.refold()
var n1: int = persist.records().size()
_ok("1. the record count drops (%d -> %d)" % [n0, n1], n1 < n0, str(r))
var g1 := geometry(level)
_ok("2. the live geometry is identical (%d brushes)" % g0.size(), g1 == g0, "%d vs %d" % [g0.size(), g1.size()])
var o1 := objects_of(objs)
_ok("3. every object is identical (%d)" % o0.size(), o1 == o0, "%d vs %d" % [o0.size(), o1.size()])
_ok("4. History is empty", (persist.gestures() as Array).is_empty(), "%d gestures" % (persist.gestures() as Array).size())
persist.undo_last()
persist.refold()
_ok("4. Ctrl+Z changes nothing", geometry(level) == g0 and objects_of(objs) == o0)
var bp := String(r.get("backup", ""))
var bk: Variant = JSON.parse_string(FileAccess.get_file_as_string(bp)) if bp != "" else null
_ok("5. a backup was written", bk is Dictionary, bp)
_ok("5. it holds every old record", bk is Dictionary and (bk.get("entries", {}) as Dictionary).size() == n0,
"%d vs %d" % [(bk.get("entries", {}) as Dictionary).size() if bk is Dictionary else -1, n0])
_ok("the level-source key survives (it is not history)", src0 == 1 and _source_keys() == 1,
"%d -> %d" % [src0, _source_keys()])
func gs_set_source() -> void:
var persist: Node = get_node("MapDevTest/LevelPersistence") if has_node("MapDevTest/LevelPersistence") else null
if persist == null:
for c in get_children():
persist = c.get_node_or_null("LevelPersistence")
if persist != null:
break
if persist != null:
persist.persist_source("a".repeat(64), "gate.map", 1)
func _source_keys() -> int:
var gs: Node = get_tree().root.get_node_or_null("/root/GameState")
var n := 0
for k: String in gs.keys_with_prefix("world:level:"):
if k.ends_with(":source"):
n += 1
return n