extends SceneTree
## Importer dual-sink gate — level-mesh-implementation.md §5a.1 (integration).
##
## "$HYPER_TPS_GODOT" --headless --script scenes/test/importer_dual_sink_gate.gd
##
## THE INTEGRATED CONFIGURATION IS THE POINT: brushes into DCLevel (exact mesh)
## AND the carve into DCTerrain (the field), from ONE import.
##
## Until the `field` parameter existed that combination was unreachable, and not
## by oversight — `import_text` took a single `terrain` node that served as BOTH
## the brush sink and the carve target:
##
## var to_level := terrain.has_method("add_brush") # :287
## if carve and terrain.has_method("edit_terrain_box"): # :295
##
## A DCLevel has add_brush and no edit_terrain_box; a DCTerrain has the reverse.
## So one node could satisfy one branch or the other, never both. The CONTROLS
## below measure exactly that, because "unreachable" is a claim until something
## runs it:
##
## control A sink = DCLevel -> 1183 brushes, carve count 0
## control B sink = field stub -> 0 brushes, carve + 1183 convex SDF edits
##
## A is the shipped exact-mesh path with its interior UNPROTECTED. B is the
## shipped voxel path, where level and terrain are one field — which is why that
## one has no seam problem and no interior problem, and pays in polycount.
##
## The field is a STUB, not a real DCTerrain. What changed is the importer's
## dispatch, and a stub observes the calls exactly while keeping the voxel engine
## (GPU meshing, octree streaming, chunk requeue) out of a headless gate — see
## hyper-games #16 for what dragging the full path into headless costs.
##
## FRAME-STAGED for the reason dc_level_gate.gd documents: DCLevel::_ready is
## what creates the LevelMesh child, and _ready has not run for a node added
## during _initialize. The first cut of this gate checked containment straight
## after rebuild(), found no mesh, and skipped the check in silence — passing on
## everything except the one geometric assertion, which never executed.
const MAP_PATH := "/storage/projects/gamedev/quake/quakemash/START.MAP"
const WORLD_SCALE := 0.028
## Stands in for DCTerrain. Records calls instead of applying them; `has_method`
## is what the importer actually branches on, so declaring them is what matters.
##
## edit_terrain_convex is declared as well as edit_terrain_box, and not for
## symmetry: without it control B faults mid-loop on the first brush, which both
## aborts the measurement and prints a SCRIPT ERROR that reads like a gate
## failure. Recording it instead turns control B into the statement it should
## have been.
class FieldStub extends Node:
var carves: Array = []
var convex_edits: int = 0
func edit_terrain_box(pos: Vector3, half: Vector3, strength: float,
interactive: bool, material_id: int) -> void:
carves.append({"pos": pos, "half": half, "strength": strength,
"interactive": interactive, "material_id": material_id})
func edit_terrain_convex(_planes: PackedFloat32Array, _center: Vector3,
_radius: float, _strength: float, _interactive: bool,
_material_id: int, _face_proj: PackedInt32Array) -> void:
convex_edits += 1
var _failures: Array[String] = []
var _frame := 0
var _lvl_a: Node3D = null
var _lvl_b: Node3D = null
var _lvl_g: Node3D = null
var _field_a: FieldStub = null
var _field_b: FieldStub = null
var _field_g: FieldStub = null
func _check(label: String, got: Variant, want: Variant) -> void:
var ok: bool = got == want
if not ok:
_failures.append("%s: got %s, want %s" % [label, str(got), str(want)])
print(" %-28s %-12s want %-12s %s" % [label, str(got), str(want), "OK" if ok else "FAIL"])
func _check_gt(label: String, got: int, floor_v: int) -> void:
var ok: bool = got > floor_v
if not ok:
_failures.append("%s: got %d, want > %d" % [label, got, floor_v])
print(" %-28s %-12d want > %-10d %s" % [label, got, floor_v, "OK" if ok else "FAIL"])
func _initialize() -> void:
if not FileAccess.file_exists(MAP_PATH):
print("SKIP — %s not present on this machine" % MAP_PATH)
quit(0)
return
_lvl_a = ClassDB.instantiate("DCLevel") as Node3D
_lvl_b = ClassDB.instantiate("DCLevel") as Node3D
_lvl_g = ClassDB.instantiate("DCLevel") as Node3D
if _lvl_a == null or _lvl_b == null or _lvl_g == null:
print("FAIL — DCLevel class not registered; libdc_terrain did not load")
quit(1)
return
root.add_child(_lvl_a)
root.add_child(_lvl_b)
root.add_child(_lvl_g)
_field_a = FieldStub.new()
_field_b = FieldStub.new()
_field_g = FieldStub.new()
root.add_child(_field_a)
root.add_child(_field_b)
root.add_child(_field_g)
func _process(_delta: float) -> bool:
_frame += 1
if _frame < 3:
return false
print("=== CONTROL — one target cannot be both sinks ===")
_control()
print("\n=== GATE — one import, brushes to the level AND the carve to the field ===")
_gate()
print("\n=== RE-IMPORT — moving the level must not duplicate it ===")
_reimport()
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
## Both halves of the old behaviour, measured rather than asserted.
func _control() -> void:
# A: the exact-mesh path. carve=true is passed and still cannot fire, because
# the sink it is guarded on is the DCLevel.
var imp_a := QuakeMapImporter.new()
imp_a.import_file(_lvl_a, MAP_PATH, WORLD_SCALE, Vector3.ZERO, 1.0, null, true, {}, true)
print(" A sink=DCLevel brushes=%d carves_on_field=%d" % [
_lvl_a.get_brush_count(), _field_a.carves.size()])
_check("A: field never carved", _field_a.carves.size(), 0)
# B: the voxel path. The carve fires and every brush becomes an SDF edit, so
# the level and the terrain are ONE field.
var imp_b := QuakeMapImporter.new()
imp_b.import_file(_field_b, MAP_PATH, WORLD_SCALE, Vector3.ZERO, 1.0, null, true, {}, true)
print(" B sink=field brushes=%d carves_on_field=%d convex_edits=%d" % [
_lvl_b.get_brush_count(), _field_b.carves.size(), _field_b.convex_edits])
_check("B: level got nothing", _lvl_b.get_brush_count(), 0)
_check("B: field took the brushes", _field_b.convex_edits, 1183)
## The integrated configuration, in one call.
func _gate() -> void:
var imp := QuakeMapImporter.new()
var stamped: int = imp.import_file(_lvl_g, MAP_PATH, WORLD_SCALE, Vector3.ZERO, 1.0,
null, true, {}, true, _field_g)
_lvl_g.rebuild()
_check_gt("brushes into the level", _lvl_g.get_brush_count(), 1000)
_check("stamped == brush count", stamped, _lvl_g.get_brush_count())
_check("carves into the field", _field_g.carves.size(), 1)
if _field_g.carves.size() != 1:
return
var c: Dictionary = _field_g.carves[0]
# Subtractive, and on the landscape lane — a replayed/derived edit, not a live
# one, so it must not flood the interactive priority lane.
_check("carve is subtractive", float(c["strength"]) < 0.0, true)
_check("carve is non-interactive", bool(c["interactive"]), false)
# The moat still inflates by 1.1 (§5a.2 keeps it as the fallback until the
# convex decomposition lands); assert it actually CONTAINS the level rather
# than assert the constant, so tightening the volume later does not fail a
# gate that was only ever checking a magic number.
#
# The null branch FAILS rather than skipping — see the header.
var mi := _lvl_g.get_node_or_null("LevelMesh") as MeshInstance3D
if mi == null or mi.mesh == null:
_failures.append("carve containment: no LevelMesh after rebuild — check did not run")
print(" %-28s %s" % ["carve contains the level", "FAIL (no mesh)"])
return
var aabb: AABB = mi.get_aabb()
var half: Vector3 = c["half"]
var pos: Vector3 = c["pos"]
var lo: Vector3 = pos - half
var hi: Vector3 = pos + half
var contains: bool = lo.x <= aabb.position.x and lo.y <= aabb.position.y \
and lo.z <= aabb.position.z and hi.x >= aabb.end.x \
and hi.y >= aabb.end.y and hi.z >= aabb.end.z
print(" level aabb %s .. %s" % [str(aabb.position), str(aabb.end)])
print(" carve %s .. %s" % [str(lo), str(hi)])
_check("carve contains the level", contains, true)
## map_dc_world's `,`/`.` re-import the level at a new depth. map_dc_level imports
## exactly ONCE and so never had to undo anything — add_brush APPENDS, so a second
## import without a clear() gives 2366 brushes, a level rendered twice, and every
## number in the acceptance gate silently wrong.
##
## Reuses the SAME DCLevel node the gate already imported into, because that is
## what the map does: clear the store, import again, keep the node.
func _reimport() -> void:
var before: int = _lvl_g.get_brush_count()
var mi := _lvl_g.get_node_or_null("LevelMesh") as MeshInstance3D
var aabb_before: AABB = mi.get_aabb() if mi != null else AABB()
var lift := Vector3(0, 12, 0)
_lvl_g.clear()
var imp := QuakeMapImporter.new()
imp.import_file(_lvl_g, MAP_PATH, WORLD_SCALE, lift, 1.0, null, true, {}, true, _field_g)
_lvl_g.rebuild()
_check("brush count unchanged", _lvl_g.get_brush_count(), before)
# The level must actually have MOVED, or a "working" re-import could be doing
# nothing at all and still pass the count check.
mi = _lvl_g.get_node_or_null("LevelMesh") as MeshInstance3D
if mi == null:
_failures.append("re-import: no LevelMesh")
print(" %-28s %s" % ["level moved by the lift", "FAIL (no mesh)"])
return
var moved: float = mi.get_aabb().position.y - aabb_before.position.y
print(" aabb.y %.3f -> %.3f (moved %.3f, lift %.1f)" % [
aabb_before.position.y, mi.get_aabb().position.y, moved, lift.y])
# Quantized to a voxel boundary by the importer, so compare with tolerance
# rather than demanding exactly 12.000.
_check("level moved by the lift", absf(moved - lift.y) < 0.5, true)
# The carve must follow the level, not stay where the level used to be — a
# moat left behind is worse than no moat: it protects empty space and leaves
# the rooms open.
_check("a second carve was issued", _field_g.carves.size(), 2)
if _field_g.carves.size() == 2:
var c1: Vector3 = _field_g.carves[0]["pos"]
var c2: Vector3 = _field_g.carves[1]["pos"]
print(" carve centre %s -> %s" % [str(c1), str(c2)])
_check("carve followed the level", absf((c2 - c1).y - lift.y) < 0.5, true)