OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / arch_cutout_gate.gd

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