OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_flatten_history.gd

167 lines · gd ref:
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