extends SceneTree
## GATE — runtime models end to end: ModelCache + the `model` prefab through the fold.
## M1 cache: request -> queued -> parsed (timed) -> ready; ONE scene, shared meshes
## M2 a file GlbCheck refuses is a FINAL failure with the reason, never parsed
## M3 a placed model (object.create + object.set_prop) shows the model + collision
## on layers 1|2; scale lands on the visual and in the collision points
## M4 modes: none has no body; named removes the -colonly visual from THIS copy only
## M5 placeholder while the bytes are missing -> the model when they arrive
## M6 a blocked model says why, on screen
##
## Test files go in user://content under made-up hashes (prefixed 'e0e0') and are
## removed at the end — only the files this gate wrote.
##
## Run: CI=true HYPERTPS_DAEMON_TCP=127.0.0.1:1 "$HYPER_TPS_GODOT" --headless --path . --script res://scenes/test/gate_model_object.gd
var ok := true
var checks := 0
var _frame := 0
var _written: Array[String] = []
var _mc: Node
func _p(c: bool, m: String, got: Variant = "") -> void:
checks += 1
print(("ok " if c else "FAIL: ") + m + ("" if str(got) == "" else " (" + str(got) + ")"))
if not c:
ok = false
func _oid(tag: String) -> String:
return ("e0e0" + tag.sha256_text()).substr(0, 64)
func _process(_d: float) -> bool:
_frame += 1
if _frame < 3:
return false
_run()
for p in _written:
DirAccess.remove_absolute(ProjectSettings.globalize_path(p))
print("\n%d checks — %s" % [checks, "PASS" if ok else "FAIL"])
quit(0 if ok else 1)
return true
func _box(n: String, size: Vector3, at: Vector3) -> MeshInstance3D:
var mi := MeshInstance3D.new()
mi.name = n
var b := BoxMesh.new()
b.size = size
mi.mesh = b
mi.position = at
return mi
## A .glb of `parts` (MeshInstance3Ds), exported by Godot.
func _glb(parts: Array) -> PackedByteArray:
var root := Node3D.new()
root.name = "Model"
for p: Node in parts:
root.add_child(p)
p.owner = root
var doc := GLTFDocument.new()
var st := GLTFState.new()
doc.append_from_scene(root, st)
var bytes: PackedByteArray = doc.generate_buffer(st)
root.free()
return bytes
func _put(oid: String, bytes: PackedByteArray) -> void:
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path("user://content"))
var p: String = _mc.path_of(oid)
var f := FileAccess.open(p, FileAccess.WRITE)
f.store_buffer(bytes)
f.close()
_written.append(p)
func _rec(seq: int, type_id: String, pos: Vector3) -> Dictionary:
return {"author": "g", "seq": seq, "kind": "object.create", "undone": false, "type": type_id,
"xform": Transform3D(Basis(), pos)}
func _sp(seq: int, target: String, props: Dictionary) -> Dictionary:
return {"author": "g", "seq": seq, "kind": "object.set_prop", "undone": false, "target": target, "props": props}
func _run() -> void:
_mc = root.get_node_or_null("/root/ModelCache")
_p(_mc != null, "ModelCache is an autoload")
if _mc == null:
return
# ---- M1 ----
var good := _oid("box")
_put(good, _glb([_box("Body", Vector3(2, 2, 2), Vector3(0, 1, 0))]))
var e: Dictionary = _mc.request(good)
_p(String(e["state"]) == "queued", "M1 a file already here is queued, not fetched", e["state"])
_mc.parse_now(good)
e = _mc.status(good)
_p(String(e["state"]) == "ready", "M1 then ready", e.get("reason", ""))
_p(int(e["triangles"]) == 12, "M1 triangle count recorded", e["triangles"])
_p(float(e["parse_ms"]) > 0.0, "M1 parse is timed (main thread)", "%.2f ms" % float(e["parse_ms"]))
var a: Node3D = _mc.instance(good)
var b: Node3D = _mc.instance(good)
var ma := (a.find_children("*", "MeshInstance3D", true, false)[0] as MeshInstance3D).mesh
var mb := (b.find_children("*", "MeshInstance3D", true, false)[0] as MeshInstance3D).mesh
_p(ma != null and ma == mb, "M1 two copies share ONE mesh resource")
a.free()
b.free()
_p(_mc.request(good) == _mc.status(good), "M1 asking again returns the same entry (no reparse)")
_p(_mc.shapes(good, "convex", 1.0) == _mc.shapes(good, "convex", 1.0), "M1 collision is cached per (model, mode, scale)")
# ---- M2 ----
var hostile := _oid("uri")
var parts := GlbCheck.unpack(_glb([_box("B", Vector3.ONE, Vector3.ZERO)]))
(parts["json"] as Dictionary)["buffers"].append({"uri": "../../x.bin", "byteLength": 4})
_put(hostile, GlbCheck.pack(parts["json"], parts["bin"]))
_mc.request(hostile)
_mc.parse_now(hostile)
var he: Dictionary = _mc.status(hostile)
_p(String(he["state"]) == "failed" and String(he["reason"]).contains("external"),
"M2 a file with a uri is refused, with the reason", he["reason"])
_p(bool(he["final"]) and _mc.request(hostile)["state"] == "failed", "M2 and the refusal is final")
_p(String(_mc.request("not-a-hash")["reason"]) == "bad asset id", "M2 a non-hash asset id is refused")
# ---- M3: through the fold, like a real placement ----
var objs := SceneObjects.new()
root.add_child(objs)
var recs: Array = [_rec(1, "model", Vector3(5, 0, 0)),
_sp(2, "g:1", {"asset": good, "name": "Crate", "scale": 2.0})]
var st: Dictionary = objs.apply_journal(recs)
_p(int(st["applied"]) == 2 and int(st["rejected_props"]) == 0, "M3 create + set_prop apply", st)
var m: Node = objs.node_for("g:1")
m.rebuild_now()
_p(String(m.look()) == "model", "M3 the model is shown", m.look())
var vis := m.get_node_or_null("Model/Visual") as Node3D
_p(vis != null and vis.scale.is_equal_approx(Vector3(2, 2, 2)), "M3 scale 2 on the visual", vis.scale if vis != null else "?")
var body := m.get_node_or_null("Model/Body") as StaticBody3D
_p(body != null and body.collision_layer == 3, "M3 a static body on layers 1|2 (grapple)", body.collision_layer if body != null else "none")
_p(body != null and body.scale.is_equal_approx(Vector3.ONE), "M3 the body itself is NOT scaled")
var hull := (body.get_child(0) as CollisionShape3D).shape as ConvexPolygonShape3D if body != null else null
var top := -INF
if hull != null:
for p in hull.points:
top = maxf(top, p.y)
_p(is_equal_approx(top, 4.0), "M3 scale is baked into the hull points (2 m box at y 0..2, x2)", top)
_p(objs.props("g:1")["collision"] == "auto", "M3 collision defaults to Automatic")
# ---- M4 ----
objs.apply_journal(recs + [_sp(3, "g:1", {"collision": "none"})])
m.rebuild_now()
_p(m.get_node_or_null("Model/Body") == null and m.get_node_or_null("Model/Visual") != null,
"M4 none: visual, no body")
var named := _oid("named")
_put(named, _glb([_box("Shell", Vector3(2, 2, 2), Vector3.ZERO), _box("Floor-colonly", Vector3(4, 0.2, 4), Vector3.ZERO)]))
_mc.request(named)
_mc.parse_now(named)
objs.apply_journal(recs + [_rec(4, "model", Vector3.ZERO), _sp(5, "g:4", {"asset": named, "collision": "named"})])
var mn: Node = objs.node_for("g:4")
mn.rebuild_now()
_p(mn.get_node_or_null("Model/Visual/Floor-colonly") == null and mn.get_node_or_null("Model/Visual/Shell") != null,
"M4 named: the -colonly visual is removed, the rest kept")
_p(mn.get_node_or_null("Model/Body") != null and mn.get_node("Model/Body").get_child_count() == 1,
"M4 named: its collision is there")
# THE USER'S CASE: placed with the DEFAULT collision, a -colonly mesh is not drawn.
objs.apply_journal(recs + [_rec(4, "model", Vector3.ZERO), _sp(5, "g:4", {"asset": named})])
mn.rebuild_now()
_p(mn.get_node_or_null("Model/Visual/Floor-colonly") == null, "M4 default (auto): the -colonly mesh is not drawn")
_p(mn.get_node_or_null("Model/Body") != null, "M4 default (auto): and it collides via the named mesh")
var fresh: Node3D = _mc.instance(named)
_p(fresh.get_node_or_null("Floor-colonly") != null, "M4 the CACHED scene still has it (removal was per copy)")
fresh.free()
# ---- M5: placeholder -> model ----
var late := _oid("late")
objs.apply_journal(recs + [_rec(6, "model", Vector3.ZERO), _sp(7, "g:6", {"asset": late, "name": "Late"})])
var ml: Node = objs.node_for("g:6")
ml.rebuild_now()
var waiting := String(ml.look())
_p(waiting == "placeholder" or (waiting == "blocked" and String(_mc.status(late)["reason"]).contains("unavailable")),
"M5 missing bytes: a placeholder (or 'transfer unavailable' with no overlay)", "%s / %s" % [waiting, _mc.status(late).get("reason", "")])
_put(late, _glb([_box("L", Vector3.ONE, Vector3.ZERO)]))
if waiting == "placeholder":
_mc._on_ready(late, _mc.path_of(late)) # the overlay's event for THIS hash
else:
_mc.request(late) # unavailable is retried on request
_mc.parse_now(late)
ml.rebuild_now()
_p(String(ml.look()) == "model", "M5 the bytes arrive -> the model replaces the placeholder", ml.look())
# ---- M7: scale about the BASE (user, 2026-10-04: "around the base, where it stands") ----
# A model whose mesh sits AWAY from its origin, like the user's skull (0.41 m off).
var off := _oid("offset")
_put(off, _glb([_box("Statue", Vector3(1, 1, 1), Vector3(3, 0.5, 0))]))
_mc.request(off)
_mc.parse_now(off)
_p((_mc.base(off) as Vector3).is_equal_approx(Vector3(3, 0, 0)), "M7 the base is the mesh's bottom-centre", _mc.base(off))
objs.apply_journal(recs + [_rec(10, "model", Vector3.ZERO), _sp(11, "g:10", {"asset": off, "scale": 2.0})])
var mo: Node = objs.node_for("g:10")
mo.rebuild_now()
var vb := SceneObjects.bounds_of(mo as Node3D)
_p(is_equal_approx(vb.position.y, 0.0) and is_equal_approx(vb.get_center().x, 3.0) and vb.size.is_equal_approx(Vector3(2, 2, 2)),
"M7 at scale 2 it grows from where it stands (base fixed at x 3, y 0)", vb)
var ob := mo.get_node_or_null("Model/Body") as StaticBody3D
var hb := AABB()
if ob != null:
var pts: PackedVector3Array = ((ob.get_child(0) as CollisionShape3D).shape as ConvexPolygonShape3D).points
hb = AABB(pts[0], Vector3.ZERO)
for p in pts:
hb = hb.expand(p)
_p(hb.position.is_equal_approx(vb.position) and hb.size.is_equal_approx(vb.size),
"M7 the collision matches the drawn model exactly", "%s vs %s" % [hb, vb])
# ---- M6 ----
objs.apply_journal(recs + [_rec(8, "model", Vector3.ZERO), _sp(9, "g:8", {"asset": hostile, "name": "Bad"})])
var mb2: Node = objs.node_for("g:8")
mb2.rebuild_now()
var lbl := mb2.find_children("*", "Label3D", true, false)
_p(String(mb2.look()) == "blocked" and lbl.size() == 1 and String((lbl[0] as Label3D).text).contains("external"),
"M6 a blocked model says why on screen", (lbl[0] as Label3D).text if lbl.size() > 0 else "?")
objs.queue_free()