extends SceneTree
## HEADLESS GATE — the journal fold, in the engine, with the pocket cache watching.
##
## hyper-games #22 §5 items 2/5. The offline oracles (dc-terrain
## tests/test_journal_fold.cpp, test_brush_identity.cpp, test_carve_identity.cpp)
## prove the fold is pure, convergent and correctly ordered. They cannot see the
## thing that decides whether undo is USABLE: DCLevel's pocket cache.
##
## The cache is keyed by the identity of a pocket's inputs, and undo is now "mark
## the record undone and re-fold" rather than a flag flip. So a re-fold that
## reproduces the same state MUST reproduce the same keys, or every undo re-runs
## Dual Contouring over every pocket in the level — the 5.9ms -> 422ms regression
## the cache was built to kill, landing on the one interaction that has to feel
## instant. `pockets_rebuilt` in get_stats() counts exactly that, so it is the
## number this gate reads.
##
## Run:
## cd /storage/projects/code/godot/hyper-tps
## "$HYPER_TPS_GODOT" --headless --script res://scenes/test/dc_level_journal_gate.gd
##
## Synthetic brushes on purpose: no START.MAP, no WAD, no absolute dev paths, so
## this runs anywhere the extension builds.
var fails := 0
func _ok(what: String, cond: bool) -> void:
if not cond:
fails += 1
print(" %-56s %s" % [what, "OK" if cond else "**FAIL**"])
func _eqi(what: String, got: int, want: int) -> void:
var c := got == want
if not c:
fails += 1
print(" %-56s %-6d want %-6d %s" % [what, got, want, "OK" if c else "**FAIL**"])
## An axis-aligned box as six half-spaces, the flat (nx,ny,nz,d) layout add_brush takes.
func _box(lo: Vector3, hi: Vector3) -> PackedFloat32Array:
return PackedFloat32Array([
-1, 0, 0, -lo.x, 1, 0, 0, hi.x,
0, -1, 0, -lo.y, 0, 1, 0, hi.y,
0, 0, -1, -lo.z, 0, 0, 1, hi.z,
])
func _carve_rec(author: String, seq: int, pos: Vector3) -> Dictionary:
return {
"author": author, "seq": seq, "kind": "carve", "undone": false,
"carve": {"type": "sphere", "pos": pos, "radius": 0.6},
}
## ON THE FIRST FRAME, NOT IN _init. A SceneTree script's _init runs before the
## root window is in the tree, so a node added there never gets _ready — DCLevel's
## MeshInstance3D children are created in _ready, and _build_pockets early-returns
## without a pocket node. Measured: is_inside_tree() false, ms_pocket 0.012, and
## pockets_rebuilt stuck at 0 for a build that never happened. The gate would have
## passed on nothing.
var _done := false
func _process(_delta: float) -> bool:
if _done:
return true
_done = true
_run()
return true
func _run() -> void:
print("=== DCLevel journal gate (headless) ===\n")
if not ClassDB.class_exists("DCLevel"):
printerr("DCLevel is not registered — build dc-terrain first")
quit(1)
return
var lvl: Node3D = ClassDB.instantiate("DCLevel")
get_root().add_child(lvl)
# THREE WELL-SEPARATED BRUSHES, one carve each, so the clusters are independent
# and locality is observable. Sharing a cluster would make "only the touched
# pocket rebuilt" unmeasurable.
var keys: Array[String] = []
for i in 3:
var x := float(i) * 20.0
var id: int = lvl.add_brush(_box(Vector3(x, -2, -2), Vector3(x + 4, 2, 2)), 1,
PackedInt32Array(), 0, false, "")
keys.append(lvl.get_brush_key(id))
print("-- the import is named deterministically --")
var want_keys: Array[String] = ["i:0:0", "i:0:1", "i:0:2"]
_ok("imported brushes take i:<level>:<n>", keys == want_keys)
var journal: Array = [
_carve_rec("alice", 100, Vector3(1.0, 0.0, 0.0)),
_carve_rec("alice", 101, Vector3(21.0, 0.0, 0.0)),
_carve_rec("bob", 102, Vector3(41.0, 0.0, 0.0)),
]
print("\n-- folding the journal builds the pockets --")
var st: Dictionary = lvl.apply_journal(journal)
lvl.rebuild()
_eqi("records applied", int(st["applied"]), 3)
_eqi("nothing skipped", int(st["skipped"]), 0)
var s: Dictionary = lvl.get_stats()
_eqi("three carves", int(s["carves"]), 3)
_eqi("three pockets", int(s["pockets"]), 3)
_eqi("all three built", int(s["pockets_rebuilt"]), 3)
var fp_all: int = lvl.fingerprint()
print("\n-- THE GATE: re-folding the same journal rebuilds NOTHING --")
# Local handles, generations and carve ids must all come back identical, or the
# cache misses on geometry that did not change.
st = lvl.apply_journal(journal)
lvl.rebuild()
s = lvl.get_stats()
_eqi("pockets still three", int(s["pockets"]), 3)
_eqi("pockets REBUILT", int(s["pockets_rebuilt"]), 0)
_ok("and the state is identical", lvl.fingerprint() == fp_all)
print("\n-- undo is LOCAL: one cluster goes, the others are reused --")
# The interactive case. Undoing the FIRST carve used to renumber every later
# carve id and (separately) every pocket depended on every carve, so this was a
# full-level DC pass.
var undone: Array = journal.duplicate(true)
(undone[0] as Dictionary)["undone"] = true
st = lvl.apply_journal(undone)
lvl.rebuild()
s = lvl.get_stats()
_eqi("the undone record is counted as such", int(st["undone"]), 1)
_eqi("two carves left", int(s["carves"]), 2)
_eqi("two pockets left", int(s["pockets"]), 2)
_eqi("neither survivor was rebuilt", int(s["pockets_rebuilt"]), 0)
print("\n-- redo restores the exact prior state --")
st = lvl.apply_journal(journal)
lvl.rebuild()
s = lvl.get_stats()
_eqi("three pockets again", int(s["pockets"]), 3)
_eqi("only the returning one was rebuilt", int(s["pockets_rebuilt"]), 1)
_ok("the state matches before the undo", lvl.fingerprint() == fp_all)
print("\n-- a created brush survives a re-fold with the same handle --")
var create: Dictionary = {
"author": "carol", "seq": 200, "kind": "brush.create", "undone": false,
"planes": _box(Vector3(60, -2, -2), Vector3(64, 2, 2)),
"proj": PackedInt32Array(), "material_id": 2, "level_id": 0, "sky": false,
}
var j2: Array = journal.duplicate(true)
j2.append(create)
lvl.apply_journal(j2)
lvl.rebuild()
var made: int = lvl.brush_by_key("carol:200")
_ok("the create is named by the record's dot", made != 0)
lvl.apply_journal(j2)
lvl.rebuild()
_eqi("and keeps its local handle across a re-fold", lvl.brush_by_key("carol:200"), made)
print("\n-- undoing the create tombstones it, without forgetting it --")
var j3: Array = j2.duplicate(true)
(j3[3] as Dictionary)["undone"] = true
lvl.apply_journal(j3)
lvl.rebuild()
_ok("the brush is gone from the live set",
bool((lvl.get_brush(made) as Dictionary).get("tombstoned", false)))
_eqi("but its name still resolves", lvl.brush_by_key("carol:200"), made)
lvl.apply_journal(j2)
lvl.rebuild()
_ok("and redo brings it back live",
not bool((lvl.get_brush(made) as Dictionary).get("tombstoned", true)))
print("\n%s" % ("FAILED" if fails else "PASS"))
quit(1 if fails else 0)