extends SceneTree
## DOES A REAL ARCH'S BORE END UP INSIDE THE SHELL? — hyper-games #25 §2.
##
## "$HYPER_TPS_GODOT" --headless --script scenes/test/arch_cutout_gate.gd
##
## THE GAP EVERY OTHER GATE LEFT. clearance_shapes_gate checks the operator's
## PLANE SETS with its own point-in-halfspace test. clearance_gate checks a
## hand-made box through the shell. Neither runs the real operator into the real
## level and then asks the question that matters: is the bore INSIDE the surface
## that gets subtracted from the terrain?
##
## Reported from the running game at 0.929.0: the arch is created, the shell
## re-derives, seals and stamps — and the tunnel is still full of dirt. The shell
## volume grew by only 14 m^3 where the clearance envelope is ~34 m^3, so
## something is in the brush set but not in the subtract.
##
## Placed OUTSIDE the level's AABB, because that is the reported case and because
## it is the one islands changed.
const MAP_SCENE := "res://scenes/maps/map_dc_world.tscn"
const OP := preload("res://scenes/maps/operators/create_box_operator.gd")
var _map: Node = null
var _t0 := 0
var _done := false
var _failures: Array[String] = []
func _check(label: String, ok: bool, detail: String) -> void:
if not ok:
_failures.append("%s (%s)" % [label, detail])
print(" %-50s %s %s" % [label, "OK " if ok else "FAIL", detail])
func _initialize() -> void:
_t0 = Time.get_ticks_msec()
_map = (load(MAP_SCENE) as PackedScene).instantiate()
root.add_child(_map)
func _process(_d: float) -> bool:
if _done:
return true
var lvl: Node = _map.get("level")
if lvl == null or not is_instance_valid(lvl) or lvl.get_brush_count() == 0:
if Time.get_ticks_msec() - _t0 > 90000:
print("FAIL — level never imported"); quit(1); return true
return false
_done = true
print("arch cutout gate")
var mi := lvl.get_node_or_null("LevelMesh") as MeshInstance3D
var ab: AABB = mi.get_aabb()
print(" level aabb %s size %s" % [str(ab.position), str(ab.size)])
var s0: Dictionary = lvl.build_shell()
var vol0: float = float(s0.get("volume", 0.0))
# Drive the REAL operator. Only the crosshair is bypassed — headless has no
# camera — so _origin/_normal/_axis/_view are set the way update_preview would.
var op := OP.new()
op.map = _map
op.level = lvl
_map.add_child(op)
op._shape = 2 # ARCH
op._size_steps = 13 # 104 units at grid 8, as reported
op._normal = Vector3.UP
op._axis = 1
op._view = Vector3(0, 0, 1)
op._material = 0
op._level_id = 0
# OUTSIDE the level, the reported placement.
op._origin = Vector3(-54.432, 5.081, 79.072)
op._valid = true
var pieces: Array = op._pieces()
var clearance: Array = op._clearance_pieces()
_check("the operator makes a ring", pieces.size() == OP.ARCH_SEGMENTS,
"%d voussoirs" % pieces.size())
_check("and one clearance volume", clearance.size() == 1, "%d" % clearance.size())
if clearance.is_empty():
return _finish()
# The bore: on the springing line, mid-span, mid-tunnel. Inside no voussoir.
var c: Array = op._corners()
var lo: Vector3 = c[0]
var hi: Vector3 = c[1]
var bore := Vector3((lo.x + hi.x) * 0.5, lo.y + 0.15, (lo.z + hi.z) * 0.5)
var in_ring := false
for p: Dictionary in pieces:
if _inside(p["planes"], bore):
in_ring = true
_check("the bore is inside no voussoir", not in_ring, str(bore))
_check("but IS inside the clearance plane set",
_inside(clearance[0]["planes"], bore), "by the operator's own planes")
var before: int = lvl.get_brush_count()
_check("confirm() creates it", op.confirm(), "")
_check("nine brushes landed", lvl.get_brush_count() - before == 9,
"%d" % (lvl.get_brush_count() - before))
_check("one of them is clearance",
(lvl.get_clearance_brush_ids() as PackedInt32Array).size() >= 1,
"%d" % (lvl.get_clearance_brush_ids() as PackedInt32Array).size())
# ---- THE QUESTION ----------------------------------------------------
var s1: Dictionary = lvl.build_shell()
var vol1: float = float(s1.get("volume", 0.0))
print(" shell volume %.1f -> %.1f (+%.1f m^3)" % [vol0, vol1, vol1 - vol0])
print(" shell tris %d -> %d" % [int(s0.get("triangles", 0)), int(s1.get("triangles", 0))])
var verts: PackedVector3Array = s1.get("vertices", PackedVector3Array())
var idx: PackedInt32Array = s1.get("indices", PackedInt32Array())
var f := FileAccess.open("/tmp/arch_shell.obj", FileAccess.WRITE)
for v: Vector3 in verts:
f.store_line("v %.6f %.6f %.6f" % [v.x, v.y, v.z])
for i in range(0, idx.size(), 3):
f.store_line("f %d %d %d" % [idx[i] + 1, idx[i+1] + 1, idx[i+2] + 1])
f.close()
# THE VOLUME IS THE MEASUREMENT. load_obj is not bound to GDScript, so the
# SDF cannot be probed from here — but the clearance envelope is a
# half-ellipse prism of known size, and the shell either grew by it or did
# not. That is the number the reported failure differs on: a correct build
# adds ~40 m^3, the stale cached one added 14.
var gained: float = vol1 - vol0
_check("the shell GREW BY THE CLEARANCE ENVELOPE, not just the ring",
gained > 25.0, "+%.1f m^3 (the ring alone is ~3)" % gained)
_check("and it is still sealed", bool(s1.get("sealed", false)), "yes")
# ---- THE CACHE MUST NOT SERVE A DIFFERENT LEVEL ----------------------
# The reported failure was a cache HIT on a key that did not describe this
# geometry, which subtracted a shell 26 m^3 too small while printing SEALED.
# The key is now the geometry fingerprint, so it has to move when the
# geometry does.
if _map.has_method("_shell_cache_key"):
var key_now: String = _map._shell_cache_key()
_check("the cache key is not a tuple of counts",
key_now.contains("fp"), key_now)
lvl.translate_brush((lvl.get_live_brush_ids() as PackedInt32Array)[-1],
Vector3(0.5, 0, 0))
lvl.rebuild()
_check("and it CHANGES when a brush moves without changing any count",
_map._shell_cache_key() != key_now, "key moved")
return _finish()
static func _inside(planes: PackedFloat32Array, p: Vector3, eps := 1.0e-4) -> bool:
for i in range(0, planes.size(), 4):
if planes[i] * p.x + planes[i+1] * p.y + planes[i+2] * p.z - planes[i+3] > eps:
return false
return true
func _finish() -> bool:
print("\nRESULT: %s" % ("PASS" if _failures.is_empty() else "FAIL"))
for f: String in _failures:
print(" - %s" % f)
quit(0 if _failures.is_empty() else 1)
return true