OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / shell_subtract_gate.gd

130 lines · gd ref:
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