extends Node
## HEADLESS GATE — texture intent through the REAL journal path (hyper-games #94 P1
## step 2, design gate 5): record -> GameState -> LevelPersistence._to_record ->
## DCLevel._record_from -> fold -> get_brush / the derived projection table.
##
## #41 §4's trap is the reason this exists: a record kind or field missing from
## `_to_record` becomes {} (or is silently dropped) and VANISHES on the next fold,
## while the C++ oracle -- which never goes through GDScript -- stays green.
## That trap had already caught two fields: brush.create's `clearance` and
## `group` were written by the box operator, read by the C++ decode, and dropped by
## _to_record, so every refold rebuilt a clearance volume as a SOLID brush.
##
## RUN AS A SCENE (level_persistence.gd names GameState at compile time):
## "$HYPER_TPS_GODOT" --headless res://scenes/test/journal_texture_gate.tscn
var fails := 0
var checks := 0
func _ok(what: String, cond: bool, note: String = "") -> void:
checks += 1
if not cond:
fails += 1
print(" %-64s %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]
func _tex(id: String, mode: int = 1) -> Dictionary:
return {"tex": id, "mode": mode}
func _face_tex(level: Node, bkey: String, fkey: String) -> Dictionary:
var id: int = level.brush_by_key(bkey)
var b: Dictionary = level.get_brush(id)
var ft: Array = b.get("face_tex", [])
var f: int = level.face_by_key(id, fkey)
return ft[f] if f >= 0 and f < ft.size() else {}
func _ready() -> void:
await get_tree().process_frame
_run()
if checks < 15:
print(" **FAIL** only %d checks ran -- setup died before the assertions" % checks)
fails += 1
print("\nGATE journal_texture: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
get_tree().quit(0 if fails == 0 else 1)
func _run() -> void:
if not ClassDB.class_exists("DCLevel"):
_ok("DCLevel registered", false)
return
var gs: Node = get_tree().root.get_node_or_null("/root/GameState")
gs.route("world:level:*", 1, "peer") # 1 == Channel.DURABLE
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)
print("-- brush.create: face_tex, clearance and group survive the fold --")
var ft: Array = []
for i in 6:
ft.append(_tex("p/a%d" % i))
var bdot: String = persist.record("brush.create", {
"planes": _box(Vector3(0, 0, 0), Vector3(2, 2, 2)), "proj": [], "material_id": 1,
"level_id": 0, "sky": false, "clearance": true, "group": "g1", "face_tex": ft})
_ok("brush.create recorded", bdot != "")
var st: Dictionary = persist.refold()
_ok("applied, nothing skipped", int(st.get("applied", 0)) == 1 and int(st.get("skipped", 0)) == 0, str(st))
var b: Dictionary = level.get_brush(level.brush_by_key(bdot))
_ok("clearance survives the fold (was dropped by _to_record)", bool(b.get("clearance", false)))
_ok("group survives the fold (was dropped by _to_record)", String(b.get("group", "")) == "g1", String(b.get("group", "")))
var fts: Array = b.get("face_tex", [])
_ok("face_tex survives the fold", fts.size() == 6 and String((fts[0] as Dictionary).get("tex", "")) == "p/a0",
str(fts.size()))
print("\n-- face.add carries its texture --")
var adot: String = persist.record("face.add", {
"brush_key": bdot, "n": [0.7071068, 0.7071068, 0.0], "d": 2.5, "tex": _tex("p/bevel", 3)})
persist.refold()
var nt: Dictionary = _face_tex(level, bdot, adot)
_ok("the new face has its texture and mode", String(nt.get("tex", "")) == "p/bevel" and int(nt.get("mode", 0)) == 3, str(nt))
print("\n-- face.tex with a field mask --")
var c1: String = persist.record("face.tex", {
"brush_key": bdot, "face_keys": ["i:0", "i:2"], "tex": _tex("p/brick"), "mask": 1})
var c2: String = persist.record("face.tex", {
"brush_key": bdot, "face_keys": ["i:0"], "tex": {"off_u": 0.25, "rot": 15.0}, "mask": 4 | 8})
persist.refold()
var t0: Dictionary = _face_tex(level, bdot, "i:0")
_ok("texture change landed on both faces",
String(t0.get("tex", "")) == "p/brick" and String(_face_tex(level, bdot, "i:2").get("tex", "")) == "p/brick")
_ok("the later nudge did not clobber the texture", String(t0.get("tex", "")) == "p/brick", String(t0.get("tex", "")))
_ok("the nudge landed (offset, rotation)", is_equal_approx(float(t0.get("off_u", 0)), 0.25) and is_equal_approx(float(t0.get("rot", 0)), 15.0), str(t0))
_ok("an unnamed face is untouched", String(_face_tex(level, bdot, "i:1").get("tex", "")) == "p/a1")
_ok("undo the texture change", persist.undo_op(c1))
persist.refold()
_ok("after undo the face is back to its create-time texture", String(_face_tex(level, bdot, "i:0").get("tex", "")) == "p/a0")
_ok("and the nudge (still live) is kept", is_equal_approx(float(_face_tex(level, bdot, "i:0").get("off_u", 0)), 0.25))
print("\n-- the derived projection table --")
_ok("redo the texture change (p/brick back on two faces)", persist.redo_op(c1))
persist.refold()
var imp := PackedFloat32Array()
imp.resize(2 * 16) # two import rows (START.MAP's table)
for k in imp.size():
imp[k] = 100.0 + k
level.set_import_projection_table(imp)
var info := {}
for i in 6:
info["p/a%d" % i] = {"layer": 7, "tile_u": 2.0, "tile_v": 2.0}
info["p/bevel"] = {"layer": 9, "tile_u": 1.0, "tile_v": 1.0}
level.set_texture_info(info, 31) # 31 = the "missing" layer
level.rebuild()
var tab: PackedFloat32Array = level.get_projection_table()
var rows: int = tab.size() / 16
_ok("the import rows come first, unchanged", rows > 2 and tab[0] == 100.0 and tab[31] == 131.0, "%d rows" % rows)
var layers := {}
var u_len := -1.0
for r in range(2, rows):
var lay := int(tab[r * 16])
layers[lay] = true
if lay == 7:
u_len = Vector3(tab[r * 16 + 4], tab[r * 16 + 5], tab[r * 16 + 6]).length()
_ok("derived rows carry the pack layer", layers.has(7) and layers.has(9), str(layers.keys()))
_ok("p/brick (no info) falls back to the missing layer", layers.has(31))
_ok("tile size is honoured (2 m tile -> |U| = 0.5)", is_equal_approx(u_len, 0.5), str(u_len))
# DEDUP: a second brush elsewhere with IDENTICAL intent adds no rows -- offsets are
# in tiles and axes depend only on the normal, so its faces map to the same rows.
# (The FIRST brush no longer matches `ft` -- two faces were retextured -- so the
# comparison is between two brushes that both carry `ft` exactly.)
persist.record("brush.create", {
"planes": _box(Vector3(10, 0, 0), Vector3(12, 2, 2)), "proj": [], "material_id": 1,
"level_id": 0, "sky": false, "face_tex": ft})
persist.refold()
level.rebuild()
var rows2: int = (level.get_projection_table() as PackedFloat32Array).size() / 16
persist.record("brush.create", {
"planes": _box(Vector3(20, 0, 0), Vector3(22, 2, 2)), "proj": [], "material_id": 1,
"level_id": 0, "sky": false, "face_tex": ft})
persist.refold()
level.rebuild()
var rows3: int = (level.get_projection_table() as PackedFloat32Array).size() / 16
_ok("dedup: a third brush identical to the second adds no rows", rows3 == rows2, "%d -> %d" % [rows2, rows3])
_ok("the version bumped when the table changed, not otherwise", level.get_projection_table_version() >= 1)
print("\n-- texture lock rides brush.translate / brush.scale through _to_record (gate 4) --")
# #41 §4's trap again: a tex_lock that _to_record drops refolds as an UNLOCKED
# move — the texture slides, silently, on every peer and on every refold.
var lk: String = persist.record("brush.create", {
"planes": _box(Vector3(40, 0, 0), Vector3(42, 2, 2)), "proj": [], "material_id": 1,
"level_id": 0, "sky": false, "face_tex": ft})
persist.refold()
var off0 := float(_face_tex(level, lk, "i:2").get("off_u", 0.0))
persist.record("brush.translate", {"brush_key": lk, "delta": [0.5, 0, 0.25], "tex_lock": true})
persist.refold()
var t2: Dictionary = _face_tex(level, lk, "i:2")
_ok("a locked translate moved the texture offset", absf(float(t2.get("off_u", 0.0)) - off0) > 1e-4,
"%.4f -> %.4f" % [off0, float(t2.get("off_u", 0.0))])
persist.record("brush.scale", {"brush_key": lk, "scale": [2, 1, 1], "pivot": [40, 0, 0], "tex_lock": true})
persist.refold()
_ok("a locked non-uniform scale froze the faces to explicit", int(_face_tex(level, lk, "i:2").get("mode", 0)) == 2,
str(_face_tex(level, lk, "i:2").get("mode")))
var un: String = persist.record("brush.create", {
"planes": _box(Vector3(50, 0, 0), Vector3(52, 2, 2)), "proj": [], "material_id": 1,
"level_id": 0, "sky": false, "face_tex": ft})
persist.record("brush.translate", {"brush_key": un, "delta": [0.5, 0, 0.25]})
persist.refold()
_ok("without tex_lock the texture stays in world space", is_equal_approx(float(_face_tex(level, un, "i:2").get("off_u", 0.0)), 0.0))