extends Node
## GATE — THE ASSET DOCUMENT (#120 L4 step 1, docs/node-tree-and-stacks.md §15.1, asset_doc.gd).
## A room built through the map's own paths, one collection "Kit" holding: a box A with a
## mirror, cut by K (a cutter in its own "Cutters" collection, as Box Cutter files it); a
## cutter K2 that only cuts a box O OUTSIDE Kit; a cable on its curve; a plain brush object;
## a scene object; a sub-collection "Sub" with a box B.
## D1 encode -> decode -> encode: byte-identical; D1b key order does not matter
## D2 two writes of the same room: the same oid
## D3 nothing in the records names the room (no "probe" dot or group survives)
## D4 every reference (target, cutter, targets, link, curve, profile, parent, members)
## resolves to a local dot the doc defines (a record, or a brush object's group)
## D5 contents: A, B, K (brought in), the cable shape; its curve; the mirror; ONE cut
## (A by K) with only A as target; the plain brushes under one local group; the
## object; the sub-collection with B
## D6 K2 and its cut are left out and REPORTED; K's arrival is reported
## D7 pivot = the bounds' base centre; the bounds hold every brush the contents fold to
## D8 decode refuses: a blob with an Object, a record of another kind, a non-local dot,
## a wrong version, garbage
## D9 the room changes (A moved) -> a different oid (D2 is not vacuous)
##
## RUN AS A SCENE: CI=true HYPERTPS_DAEMON_TCP=127.0.0.1:1 "$HYPER_TPS_GODOT" --headless --path . res://scenes/test/gate_asset_doc.tscn
const AssetDoc := preload("res://scenes/maps/asset_doc.gd")
var fails := 0
var checks := 0
var map: Node3D
var persist: Node
func _ok(what: String, cond: bool, note: Variant = "") -> void:
checks += 1
if not cond:
fails += 1
print(" %-64s %s %s" % [what, "OK" if cond else "**FAIL**", str(note)])
func _ready() -> void:
var gs: Node = get_tree().root.get_node_or_null("/root/GameState")
gs.route("world:level:*", 1, "peer")
var keet: Node = get_tree().root.get_node_or_null("/root/KeetService")
if keet != null:
keet.current_member_id = "probe"
get_tree().root.get_node("/root/GameMode").set_mode("build")
map = (load("res://scenes/maps/map_dev_test.tscn") as PackedScene).instantiate()
add_child(map)
for i in 6:
await get_tree().process_frame
persist = map.get("_persist")
_run()
if checks < 20:
print(" **FAIL** only %d checks ran -- setup died before the assertions" % checks)
fails += 1
print("\nGATE asset_doc: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
get_tree().quit(0 if fails == 0 else 1)
func _box(at: Vector3, size: Vector3) -> String:
return persist.record("shape.create", {"type": "box", "gv": 1, "params": {},
"frame": SceneObjects.xform_to(Transform3D(Basis.IDENTITY, at)), "size": [size.x, size.y, size.z],
"material_id": 2, "level_id": 0})
static func _box_planes(lo: Vector3, hi: Vector3) -> Array:
return [-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]
## Every String anywhere in `v`.
func _strings(v: Variant, out: Array) -> void:
if v is String or v is StringName:
out.append(String(v))
elif v is Dictionary:
for k: Variant in (v as Dictionary):
_strings(k, out)
_strings((v as Dictionary)[k], out)
elif v is Array:
for x: Variant in (v as Array):
_strings(x, out)
func _run() -> void:
var O0 := Vector3(500, 0, 500)
persist.begin_gesture("asset room")
var a := _box(O0, Vector3(2, 2, 2))
var b := _box(O0 + Vector3(6, 0, 0), Vector3(1, 3, 1))
var k := _box(O0 + Vector3(0.5, 0.5, 0), Vector3(1, 1, 3))
var o := _box(O0 + Vector3(-20, 0, 0), Vector3(2, 2, 2))
var k2 := _box(O0 + Vector3(-20, 0.5, 0), Vector3(1, 1, 3))
persist.record("mod.create", {"target": a, "mod": "mirror", "params": {"axis": "x", "offset": 0.0}})
persist.record("op.create", {"op": "subtract", "cutter": k, "targets": [a]})
persist.record("op.create", {"op": "subtract", "cutter": k2, "targets": [o]})
var grp := "probe:grp1"
for i in 2:
var lo := O0 + Vector3(0, 0, 6 + i * 2)
persist.record("brush.create", {"planes": _box_planes(lo, lo + Vector3(1, 1, 1)), "proj": [],
"material_id": 1, "level_id": 0, "sky": false, "clearance": false, "group": grp, "face_tex": []})
persist.end_gesture()
persist.refold()
var cable: String = map.add_cable([CurveMath.point(O0 + Vector3(0, 1, -4)), CurveMath.point(O0 + Vector3(6, 1, -4))],
false, {"radius": 0.05})
var obj: String = map.create_object(PrefabCatalogue.ids()[0], Transform3D(Basis.IDENTITY, O0 + Vector3(3, 0, 3)))
var cable_curve := String((persist.shape_state(cable)["params"] as Dictionary).get("curve", ""))
persist.begin_gesture("asset collections")
var kit: String = persist.record("col.create", {"name": "Kit", "parent": ""})
var sub: String = persist.record("col.create", {"name": "Sub", "parent": kit})
var cuts: String = persist.record("col.create", {"name": "Cutters", "parent": ""})
persist.record("col.link", {"target": kit, "members": ["g:" + a, "g:" + k2, "g:" + cable, "g:" + grp, "obj:" + obj]})
persist.record("col.link", {"target": sub, "members": ["g:" + b]})
persist.record("col.link", {"target": cuts, "members": ["g:" + k]})
persist.end_gesture()
persist.refold()
_ok("setup: the room has A, B, K, the cable, the brush object, the object",
(persist.live_shapes() as Dictionary).has(a) and (persist.live_shapes() as Dictionary).has(cable)
and cable_curve != "" and obj != "" and not (map.level.get_group_brush_ids(grp) as PackedInt32Array).is_empty(),
[a, cable, cable_curve, obj])
var w1: Dictionary = AssetDoc.write(persist, kit)
var doc: Dictionary = w1["doc"]
var bytes := AssetDoc.encode(doc)
_ok("setup: a doc was written", not doc.is_empty() and bytes.size() > 0, bytes.size())
if doc.is_empty():
return
var recs: Array = doc["records"]
# ---- D1 / D2 ----
var back := AssetDoc.decode(bytes)
_ok("D1 encode -> decode -> encode is byte-identical", not back.is_empty() and AssetDoc.encode(back) == bytes)
# D1b: another peer may build the same Dictionaries in another key order.
_ok("D1b the same doc with every key inserted in reverse order: the same bytes",
AssetDoc.encode(_reversed(doc)) == bytes)
var oid := AssetDoc.hash_of(bytes)
_ok("D2 a second write of the same room: the same oid",
AssetDoc.hash_of(AssetDoc.encode(AssetDoc.write(persist, kit)["doc"])) == oid, oid.substr(0, 12))
# ---- D3 / D4 ----
var strs: Array = []
_strings(recs, strs)
var leaks := strs.filter(func(s: String) -> bool: return s.contains("probe"))
_ok("D3 no room dot or group survives in the records", leaks.is_empty(), leaks)
# Defined: every record's dot, and every brush object's group (a name its brush.create
# records share — not a record of its own).
var defined := {}
for r: Dictionary in recs:
defined[String(r["dot"])] = true
if String(r["kind"]) == "brush.create":
defined[String(r["group"])] = true
var refs: Array = []
for r2: Dictionary in recs:
for f: String in ["target", "cutter", "link"]:
if r2.has(f) and String(r2[f]) != "":
refs.append(String(r2[f]))
if r2.has("parent") and String(r2["parent"]) != "":
refs.append(String(r2["parent"]))
for t: Variant in (r2.get("targets", []) as Array):
refs.append(String(t))
for m: Variant in (r2.get("members", []) as Array):
refs.append(String(m).substr(String(m).find(":") + 1))
var p: Dictionary = r2.get("params", {})
for pk: String in ["curve", "profile_curve"]:
if String(p.get(pk, "")) != "":
refs.append(String(p[pk]))
var dangling := refs.filter(func(x: String) -> bool: return not defined.has(x))
_ok("D4 every reference names a local dot the doc defines (%d refs)" % refs.size(),
refs.size() >= 5 and dangling.is_empty(), [refs, dangling])
# ---- D5 ----
var by_kind := {}
for r3: Dictionary in recs:
(by_kind.get_or_add(String(r3["kind"]), []) as Array).append(r3)
var shapes: Array = by_kind.get("shape.create", [])
_ok("D5 shapes: A, B, K and the cable (4); K2 left out", shapes.size() == 4,
shapes.map(func(r4: Dictionary) -> String: return String(r4["type"])))
_ok("D5 the cable's curve is in it", (by_kind.get("curve.create", []) as Array).size() == 1)
var mods: Array = by_kind.get("mod.create", [])
_ok("D5 the mirror is in it", mods.size() == 1 and String(mods[0]["mod"]) == "mirror")
var ops: Array = by_kind.get("op.create", [])
_ok("D5 ONE cut, A by K, with only A as its target", ops.size() == 1 and (ops[0]["targets"] as Array).size() == 1
and String(ops[0]["targets"][0]) == String(mods[0]["target"]) if not mods.is_empty() and ops.size() == 1 else false, ops)
var bc: Array = by_kind.get("brush.create", [])
var bgroups := {}
for r5: Dictionary in bc:
bgroups[String(r5["group"])] = true
_ok("D5 the brush object: 2 brushes, one local group", bc.size() == 2 and bgroups.size() == 1, bgroups.keys())
_ok("D5 the object is in it", (by_kind.get("object.create", []) as Array).size() == 1)
var cols: Array = by_kind.get("col.create", [])
_ok("D5 the sub-collection with B (its one member)", cols.size() == 1 and String(cols[0]["name"]) == "Sub"
and (cols[0]["members"] as Array).size() == 1 if cols.size() == 1 else false, cols)
# ---- D6 ----
var rep: Array = w1["report"]
_ok("D6 K2 (cuts only outside) is reported left out", rep.any(func(s: String) -> bool: return s.contains(k2) and s.contains("left out")), rep)
_ok("D6 K (from Cutters) is reported brought in", rep.any(func(s: String) -> bool: return s.contains(k) and s.contains("brought in")), rep)
# ---- D7 ----
var ids := PackedInt32Array()
for g: String in [a, b, k, cable, grp]:
ids.append_array(map.level.get_group_brush_ids(g) as PackedInt32Array)
var bb: Dictionary = map.level.get_brushes_bounds(ids)
var lo2: Vector3 = (bb["aabb_min"] as Vector3).min(SceneObjects.xform_from(by_kind["object.create"][0]["xform"]).origin)
var hi2: Vector3 = (bb["aabb_max"] as Vector3).max(SceneObjects.xform_from(by_kind["object.create"][0]["xform"]).origin)
var pv: Vector3 = doc["pivot"]
_ok("D7 the bounds = every brush the contents fold to + the object", (doc["bounds"][0] as Vector3).is_equal_approx(lo2)
and (doc["bounds"][1] as Vector3).is_equal_approx(hi2), [doc["bounds"], lo2, hi2])
_ok("D7 the pivot = the bounds' base centre", pv.is_equal_approx(Vector3((lo2.x + hi2.x) * 0.5, lo2.y, (lo2.z + hi2.z) * 0.5)), pv)
# ---- D8 ----
var with_obj := doc.duplicate(true)
with_obj["evil"] = Resource.new()
_ok("D8 refuses a blob carrying an Object", AssetDoc.decode(var_to_bytes_with_objects(with_obj)).is_empty())
var bad_kind := doc.duplicate(true)
(bad_kind["records"] as Array).append({"kind": "brush.remove", "dot": "L:999", "brush_key": "x"})
_ok("D8 refuses a record of another kind", AssetDoc.decode(AssetDoc.encode(bad_kind)).is_empty())
var bad_dot := doc.duplicate(true)
(bad_dot["records"][0] as Dictionary)["dot"] = "probe:1"
_ok("D8 refuses a non-local dot", AssetDoc.decode(AssetDoc.encode(bad_dot)).is_empty())
var bad_v := doc.duplicate(true)
bad_v["v"] = 99
_ok("D8 refuses another version", AssetDoc.decode(AssetDoc.encode(bad_v)).is_empty())
_ok("D8 refuses garbage", AssetDoc.decode(PackedByteArray([1, 2, 3, 4, 5, 6, 7, 8])).is_empty())
# ---- D9 ----
persist.begin_gesture("move A")
persist.record("shape.xform", {"target": a, "frame": SceneObjects.xform_to(Transform3D(Basis.IDENTITY, O0 + Vector3(0, 0, 1)))})
persist.end_gesture()
persist.refold()
_ok("D9 A moved -> a different oid", AssetDoc.hash_of(AssetDoc.encode(AssetDoc.write(persist, kit)["doc"])) != oid)
## `v` with every Dictionary's keys inserted in reverse order.
func _reversed(v: Variant) -> Variant:
if v is Dictionary:
var keys: Array = (v as Dictionary).keys()
keys.reverse()
var out := {}
for k: Variant in keys:
out[k] = _reversed((v as Dictionary)[k])
return out
if v is Array:
return (v as Array).map(func(x: Variant) -> Variant: return _reversed(x))
return v