extends SceneTree
## DCLevel.build_shell() through the Godot binding — §5a.2, hyper-games #25.
##
## "$HYPER_TPS_GODOT" --headless --script scenes/test/shell_subtract_gate.gd
##
## dc-terrain's tests/test_level_shell.cpp already proves the ALGORITHM against
## the offline oracle. This proves the BINDING: that the numbers survive the trip
## through GDExtension, that `center` is what stamp_mesh_arrays needs, and that a
## DCTerrain accepts the result as one edit.
##
## The two are worth keeping separate. A C++ gate passing while the game shows
## nothing is exactly the seam hyper-games #23 named — the gate proved the
## operator does the right thing when told, never that it gets told.
##
## FRAME-STAGED: DCLevel::_ready builds its MeshInstance3D child, and _ready has
## not run for a node added during _initialize (dc_level_gate.gd documents this).
const MAP_PATH := "/storage/projects/gamedev/quake/quakemash/START.MAP"
const WORLD_SCALE := 0.028
var _lvl: Node3D = null
var _ter: Node3D = null
var _frame := 0
var _failures: Array[String] = []
func _check(label: String, ok: bool, detail: String) -> void:
if not ok:
_failures.append("%s (%s)" % [label, detail])
print(" %-36s %s %s" % [label, "OK " if ok else "FAIL", detail])
func _initialize() -> void:
if not FileAccess.file_exists(MAP_PATH):
print("SKIP — %s not present" % MAP_PATH)
quit(0)
return
_lvl = ClassDB.instantiate("DCLevel") as Node3D
if _lvl == null:
print("FAIL — DCLevel not registered; libdc_terrain did not load")
quit(1)
return
root.add_child(_lvl)
_ter = ClassDB.instantiate("DCTerrain") as Node3D
if _ter != null:
root.add_child(_ter)
func _process(_delta: float) -> bool:
_frame += 1
if _frame < 3:
return false
# include_sky=true is REQUIRED: a sky brush is solid and only its rendering is
# special, so without the 19 of them the boundary has holes, the flood fill
# leaks out through the ceiling and the shell encloses nothing.
var imp := QuakeMapImporter.new()
var n: int = imp.import_file(_lvl, MAP_PATH, WORLD_SCALE, Vector3.ZERO, 1.0,
null, false, {}, true)
_lvl.rebuild()
print("=== build_shell through the binding ===")
_check("import stamped brushes", n == 1183, "%d" % n)
if not _lvl.has_method("build_shell"):
_failures.append("DCLevel has no build_shell — stale .so")
print(" FAIL — no build_shell; the loaded libdc_terrain predates it")
print("\nRESULT: FAIL")
quit(1)
return true
var t0 := Time.get_ticks_msec()
var shell: Dictionary = _lvl.build_shell()
var ms := Time.get_ticks_msec() - t0
var verts: PackedVector3Array = shell.get("vertices", PackedVector3Array())
var idx: PackedInt32Array = shell.get("indices", PackedInt32Array())
var tris: int = int(shell.get("triangles", 0))
var vol: float = float(shell.get("volume", 0.0))
print(" built in %d ms: %d verts, %d tris, %.0f m^3, %.0f m^2, snapped %d, unmoved %d" % [
ms, verts.size(), tris, vol, float(shell.get("area", 0.0)),
int(shell.get("snapped", 0)), int(shell.get("unmoved", 0))])
# Same figures the C++ parity gate asserts — if the binding drops or reorders
# anything these move, and a silently truncated array would still "work".
_check("triangles match the oracle", tris == 563544, "%d vs 563544" % tris)
_check("indices == 3 * triangles", idx.size() == tris * 3, "%d" % idx.size())
_check("volume within 1%", absf(vol - 38498.0) < 385.0, "%.0f vs ~38498" % vol)
_check("enclosed >> level solid", vol > 12600.0, "%.0f > 12600" % vol)
# `center` must be the mesh's own centroid: stamp_mesh_arrays re-centres the
# vertices on theirs and treats `position` as where that lands, so a wrong
# value translates the whole shell without erroring.
var centre: Vector3 = shell.get("center", Vector3.INF)
var mean := Vector3.ZERO
for v in verts:
mean += v
mean /= maxf(1.0, float(verts.size()))
_check("center is the true centroid", centre.distance_to(mean) < 0.01,
"%s vs %s" % [str(centre), str(mean)])
# And the terrain accepts it as ONE edit — the whole reason a mesh was chosen
# over 2735 boxes against a per-chunk cap of 1536.
if _ter != null and _ter.has_method("stamp_mesh_arrays"):
var before: int = _ter.get_edit_count()
_ter.stamp_mesh_arrays(verts, idx, centre, 1.0, -1.0, false, 0)
var after: int = _ter.get_edit_count()
_check("terrain took it as ONE edit", after - before == 1, "%d -> %d" % [before, after])
else:
print(" (no DCTerrain in this build — stamp not exercised)")
# THE QUESTION THE GAME IS ACTUALLY ASKING. The shell is sealed and the terrain
# still floods the rooms, so: after the stamp, what does the FIELD say at a
# point inside the level? If it reads air, the edit works and the fault is
# downstream (meshing, upload, culling). If it reads solid, the edit is not
# being applied and everything upstream is irrelevant.
#
# (-5.376, 3.36, 39.648) is info_player_start from the live log, imported at
# the same offset the game uses, so it is interior air by construction.
if _ter != null and _ter.has_method("debug_sdf_at"):
print("\n=== FIELD PROBE: does the subtract reach the density? ===")
for probe in [Vector3(-5.376, 3.36, 39.648), Vector3(0, 0, 0), Vector3(200, 50, 200)]:
print("--- probe %s ---" % str(probe))
_ter.debug_sdf_at(probe)
print("\nRESULT: %s" % ("PASS" if _failures.is_empty() else "FAIL"))
for f in _failures:
print(" - %s" % f)
quit(0 if _failures.is_empty() else 1)
return true