extends Node
## HEADLESS GATE — LevelPersistence end to end: record -> GameState -> fold -> geometry.
##
## hyper-games #22 §5 items 1/2/4/5. The C++ oracles prove the fold; the DCLevel
## gate proves the engine binding and the pocket cache. NEITHER touches the layer
## the shipped defects actually lived in: the GDScript that mints names, writes
## records and drives undo. That is where `local_id` was used as a cross-peer
## identity, and where `prev` was used as an inverse op.
##
## No keet daemon is needed. GameState.set_state writes its local mirror before
## _send_durable, and _send_durable only reports a rejection when the daemon is
## absent — so the record round-trip through GameState is exercised for real, while
## replication is not. That split is honest: replication is what the two-machine
## test in #22 §5 is for; this covers everything up to the wire.
##
## RUN AS A SCENE, not with --script. A --script main loop is COMPILED before
## autoload singletons are registered, and level_persistence.gd names GameState at
## compile time, so the whole dependency chain fails to compile ("Identifier not
## found: GameState") even though the singleton is fine by the first frame. A scene
## loads after registration, which is also how the game itself loads it.
##
## Run:
## cd /storage/projects/code/godot/hyper-tps
## "$HYPER_TPS_GODOT" --headless res://scenes/test/level_journal_roundtrip_gate.tscn
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 _eqf(what: String, got: float, want: float) -> void:
var c: bool = absf(got - want) < 1e-4
if not c:
fails += 1
print(" %-56s %-9.4f want %-9.4f %s" % [what, got, want, "OK" if c 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**"])
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,
])
## The d of one face of one brush, by durable name — "where the wall is".
func _plane_d(level: Node, bkey: String, fkey: String) -> float:
var id: int = level.brush_by_key(bkey)
var f: int = level.face_by_key(id, fkey)
if f < 0:
return -9999.0
var pl: PackedFloat32Array = (level.get_brush(id) as Dictionary).get("planes", PackedFloat32Array())
return pl[f * 4 + 3]
func _ready() -> void:
# One frame, so DCLevel's _ready has created its mesh/pocket children and the
# physics space exists — dc_level_gate.gd documents the same ordering trap.
await get_tree().process_frame
_run()
func _run() -> void:
print("=== LevelPersistence journal round-trip (headless) ===\n")
if not ClassDB.class_exists("DCLevel"):
printerr("DCLevel is not registered — build dc-terrain first")
get_tree().quit(1)
return
# FETCHED FROM THE TREE, not named as the autoload identifier. A --script main
# loop is COMPILED before autoload singletons are registered, so writing
# `GameState` here is a compile error ("Identifier not found") even though the
# singleton exists perfectly well by the first frame — which is when this runs.
var gs: Node = get_tree().root.get_node_or_null("/root/GameState")
if gs == null:
printerr("GameState autoload is missing — cannot exercise the record path")
get_tree().quit(1)
return
# The route game.gd installs. Without it world:level:* falls through to
# _DEFAULT_ROUTE = REALTIME and never reaches _send_durable at all — which is
# precisely the bug that made every level edit non-durable until 0.843.0.
# 1 == GameState.Channel.DURABLE (the enum cannot be named for the same reason).
gs.route("world:level:*", 1, "peer")
# AN IDENTITY, because the authorship gate needs one and there is no daemon here
# to supply it. Author-scoped keys embed the writer's memberId, and can_write
# compares that against KeetService.current_member_id — which stays "" without a
# room, while LevelPersistence._author() falls back to "p<peer>". Those disagree,
# so every write this harness makes is REJECTED and nothing is stored: the first
# run of this gate failed 9 assertions for exactly that reason and none of them
# were about the journal. In a real session the member id is always set by the
# time durable writes mean anything (they need KeetService.game_state at all), so
# this is the harness supplying what the game supplies, not a workaround.
var keet: Node = get_tree().root.get_node_or_null("/root/KeetService")
if keet != null:
keet.current_member_id = "gate-author"
var level: Node3D = ClassDB.instantiate("DCLevel")
add_child(level)
var persist := LevelPersistence.new()
persist.level = level
add_child(persist)
level.add_brush(_box(Vector3(0, -2, -2), Vector3(4, 2, 2)), 1, PackedInt32Array(), 0, false, "")
level.rebuild()
print("-- the base --")
_eqf("the +x face of i:0:0 is imported at", _plane_d(level, "i:0:0", "i:1"), 4.0)
print("\n-- a carve round-trips through GameState and folds back --")
var cdot: String = persist.record("carve", {"type": "sphere", "pos": [1.0, 0.0, 0.0], "radius": 0.6})
_ok("the record has a dot", cdot != "")
level.apply_carve_sphere(Vector3(1, 0, 0), 0.6, cdot)
var st: Dictionary = persist.refold()
_eqi("one record", int(st.get("records", 0)), 1)
_eqi("applied", int(st.get("applied", 0)), 1)
_eqi("skipped nothing", int(st.get("skipped", 0)), 0)
_eqi("the carve survives the fold", int((level.get_stats() as Dictionary)["carves"]), 1)
print("\n-- undo by DOT rolls back that carve, on a fold that never saw its id --")
_ok("undo writes the tombstone", persist.undo_op(cdot))
persist.refold()
_eqi("no active carve", int((level.get_stats() as Dictionary)["carves"]), 0)
_ok("redo brings it back", persist.redo_op(cdot))
persist.refold()
_eqi("active again", int((level.get_stats() as Dictionary)["carves"]), 1)
print("\n-- THE SHIPPED BUG: two nudges, undo the first --")
# The face moves 4.0 -> 4.5 -> 5.0. Undoing the FIRST must leave 5.0, because
# the second record is still live and says 5.0. The `prev`-based scheme wrote
# record 1's previous plane (4.0) over it — a value neither op asked for.
var bkey: String = level.get_brush_key(level.brush_by_key("i:0:0"))
var d1: String = persist.record("face.set", {
"brush_key": bkey, "face_key": "i:1", "n": [1.0, 0.0, 0.0], "d": 4.5})
var d2: String = persist.record("face.set", {
"brush_key": bkey, "face_key": "i:1", "n": [1.0, 0.0, 0.0], "d": 5.0})
persist.refold()
_eqf("both applied, later wins", _plane_d(level, "i:0:0", "i:1"), 5.0)
_ok("undo the FIRST", persist.undo_op(d1))
persist.refold()
_eqf("the face is where the LIVE record says", _plane_d(level, "i:0:0", "i:1"), 5.0)
_ok("undo the second too", persist.undo_op(d2))
persist.refold()
_eqf("with both gone it returns to the imported plane", _plane_d(level, "i:0:0", "i:1"), 4.0)
print("\n-- a created brush is durable, named by its dot, and reversible --")
var bdot: String = persist.reserve_dot()
var planes := _box(Vector3(10, -2, -2), Vector3(14, 2, 2))
var plist: Array = []
for f: float in planes:
plist.append(f)
var nid: int = level.add_brush(planes, 2, PackedInt32Array(), 0, false, bdot)
_ok("the local create took", nid > 0)
_ok("and the record was written", persist.record_at(bdot, "brush.create", {
"planes": plist, "proj": [], "material_id": 2, "level_id": 0, "sky": false}))
persist.refold()
_eqi("the fold resolves the dot to the same local handle", level.brush_by_key(bdot), nid)
_ok("it is live", not bool((level.get_brush(nid) as Dictionary).get("tombstoned", true)))
_ok("undo the create", persist.undo_op(bdot))
persist.refold()
_ok("the brush is tombstoned", bool((level.get_brush(nid) as Dictionary).get("tombstoned", false)))
_eqi("but its name is not forgotten", level.brush_by_key(bdot), nid)
print("\n-- a fold is idempotent, however many times state_changed fires --")
var before: int = level.fingerprint()
persist.refold()
persist.refold()
_ok("the state does not drift", level.fingerprint() == before)
_eqi("and nothing is skipped", int(persist.last_fold().get("skipped", 0)), 0)
print("\n-- UNIVERSAL undo: every kind, and across a restart --")
# The reported bug: Shift+K only undid carves made in THIS session, because the
# undo stack was an Array filled in by the carve keypress. A face move or a fill
# was never undoable, and reloading the map made your own carves unreachable.
# Undo now reads the journal, so this is a query and there is nothing to lose.
var fresh := LevelPersistence.new() # a NEW node: knows nothing, like a restart
fresh.level = level
add_child(fresh)
fresh.refold()
var live_before: int = fresh.undo_depth()
_ok("a brand-new node can see the edits it never made", live_before > 0)
# Three fresh edits of THREE DIFFERENT KINDS, so undo is not carve-shaped.
var u_face: String = fresh.record("face.set", {
"brush_key": bkey, "face_key": "i:1", "n": [1.0, 0.0, 0.0], "d": 4.75})
var u_carve: String = fresh.record("carve", {
"type": "sphere", "pos": [3.0, 0.0, 0.0], "radius": 0.5})
var u_dot: String = fresh.reserve_dot()
var u_planes := _box(Vector3(20, -2, -2), Vector3(24, 2, 2))
var u_list: Array = []
for f: float in u_planes:
u_list.append(f)
level.add_brush(u_planes, 3, PackedInt32Array(), 0, false, u_dot)
fresh.record_at(u_dot, "brush.create", {
"planes": u_list, "proj": [], "material_id": 3, "level_id": 0, "sky": false})
fresh.refold()
_eqi("three more undoable", fresh.undo_depth(), live_before + 3)
_eqf("the face moved", _plane_d(level, "i:0:0", "i:1"), 4.75)
# LIFO: newest first, whatever kind it is.
_ok("undo 1 takes the create", fresh.undo_last() == u_dot)
fresh.refold()
_ok("the created brush is gone",
bool((level.get_brush(level.brush_by_key(u_dot)) as Dictionary).get("tombstoned", false)))
_ok("undo 2 takes the carve", fresh.undo_last() == u_carve)
_ok("undo 3 takes the FACE MOVE — never undoable before", fresh.undo_last() == u_face)
fresh.refold()
_eqf("so the face is back where the older record put it", _plane_d(level, "i:0:0", "i:1"), 4.0)
# Redo restores last-undone-first, which is the inverse of that run.
_ok("redo 1 gives back the face move", fresh.redo_last() == u_face)
_ok("redo 2 gives back the carve", fresh.redo_last() == u_carve)
_ok("redo 3 gives back the create", fresh.redo_last() == u_dot)
fresh.refold()
_eqf("the face is moved again", _plane_d(level, "i:0:0", "i:1"), 4.75)
_eqi("and we are back where we started", fresh.undo_depth(), live_before + 3)
# Another peer's edits are not mine to undo — the authorship gate would reject
# the write anyway, so offering it would only fail silently.
var mine_before: int = fresh.undo_depth()
GameState.set_state("world:level:someone-else:op:99", {
"v": 2, "kind": "carve", "undone": false, "src": "",
"type": "sphere", "pos": [9.0, 0.0, 0.0], "radius": 0.4})
_eqi("a foreign record does not enter my undo history", fresh.undo_depth(), mine_before)
fresh.queue_free()
print("\n-- A DOORWAY: split, split, delete the middle, then undo it all --")
# The parity gap this closes (docs/trenchbroom-parity.md step 1). Every op here is
# a journal record, so the doorway is durable, replicated and reversible — which is
# the whole argument for having done the journal first.
var wall_dot: String = fresh.reserve_dot()
var wall_planes := _box(Vector3(30, -2, -2), Vector3(40, 2, -1))
var wl: Array = []
for f: float in wall_planes:
wl.append(f)
var wall: int = level.add_brush(wall_planes, 4, PackedInt32Array(), 0, false, wall_dot)
fresh.record_at(wall_dot, "brush.create", {
"planes": wl, "proj": [], "material_id": 4, "level_id": 0, "sky": false})
_ok("a wall to cut", wall > 0)
var cut1: String = fresh.reserve_dot()
var mid: int = level.split_brush(wall, Vector3(1, 0, 0), 34.0, cut1)
fresh.record_at(cut1, "brush.split", {
"brush_key": wall_dot, "n": [1.0, 0.0, 0.0], "d": 34.0})
var cut2: String = fresh.reserve_dot()
var right: int = level.split_brush(mid, Vector3(1, 0, 0), 36.0, cut2)
fresh.record_at(cut2, "brush.split", {
"brush_key": cut1, "n": [1.0, 0.0, 0.0], "d": 36.0})
_ok("two cuts made two new brushes", mid > 0 and right > 0)
level.remove_brush(mid)
fresh.record("brush.remove", {"brush_key": cut1})
var st2: Dictionary = fresh.refold()
_eqi("the whole doorway folded with nothing skipped", int(st2.get("skipped", 0)), 0)
_ok("the middle piece is gone",
bool((level.get_brush(level.brush_by_key(cut1)) as Dictionary).get("tombstoned", false)))
_ok("the two jambs are still there",
not bool((level.get_brush(level.brush_by_key(wall_dot)) as Dictionary).get("tombstoned", true))
and not bool((level.get_brush(level.brush_by_key(cut2)) as Dictionary).get("tombstoned", true)))
# And the whole thing rewinds, one press at a time, because it is only records.
fresh.undo_last() # the delete
fresh.refold()
_ok("undo puts the middle back",
not bool((level.get_brush(level.brush_by_key(cut1)) as Dictionary).get("tombstoned", true)))
fresh.undo_last() # the second cut
fresh.undo_last() # the first cut
fresh.refold()
_eqi("undoing both cuts leaves the wall whole again",
int((level.get_brush(level.brush_by_key(wall_dot)) as Dictionary).get("planes",
PackedFloat32Array()).size()) / 4, 6)
_ok("and the cut-off halves are hidden with it",
bool((level.get_brush(level.brush_by_key(cut1)) as Dictionary).get("tombstoned", false)))
print("\n-- an edit against a DIFFERENT level source is refused --")
persist.persist_source("a".repeat(64), "other.map", 1)
var stale: String = persist.record("face.set", {
"brush_key": bkey, "face_key": "i:1", "n": [1.0, 0.0, 0.0], "d": 6.0})
_ok("the record exists", stale != "")
# Now claim a different source: every record written under the old hash must
# stop applying rather than move a wall that is no longer the same wall.
persist.persist_source("b".repeat(64), "another.map", 1)
persist.refold()
# 4.75 is where the live face.set from the undo section above left it. The claim
# under test is that the STALE record (d = 6.0, written against the old source
# hash) did not apply — not that the face is at its imported value.
_eqf("the stale record did not move the face", _plane_d(level, "i:0:0", "i:1"), 4.75)
print("\n%s" % ("FAILED" if fails else "PASS"))
get_tree().quit(1 if fails else 0)