OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_collision_continuity.gd

93 lines · gd ref:
extends SceneTree
## GATE — the terrain's COLLISION never vanishes under the player while the camera is
## still (user 2026-10-06: "the character can too easily fall inside of the terrain").
##
## A map loads with the camera 24 m above (0, 16) looking down (inside collision_distance,
## 100 m); after 10 s of streaming, physics rays are cast straight down at 100 fixed
## columns every 0.5 s for 30 s. A column whose ray hit ground and then hits NOTHING is a
## collision gap: a player standing there falls into the terrain.
##
## Measured before the fix: map_geiss 59 gaps (51 columns), map_geiss_world 45 (43) —
## dc-terrain freed a split parent's collision as soon as its children were VISIBLE, not
## when their collision existed (dc_octree_clipmap.cpp Stage 2), and Geiss terrain keeps
## re-splitting. The smooth terraces map had 0 and is the CONTROL: if it gaps, the
## measurement is broken.
##
## ⚠ WINDOWED (collision needs the GPU mesher). Unset CI or the window is 2x2:
##   env -u CI HYPERTPS_DAEMON_TCP=127.0.0.1:1 DISPLAY=:0 "$HYPER_TPS_GODOT" --path . \
##       --script res://scenes/test/gate_collision_continuity.gd
## One map per run (a fresh DCTerrain each): pass the map name as a user arg, or none
## for map_geiss.

const SETTLE_FRAMES := 600
const EVERY := 30
const SAMPLES := 60
## A window hidden behind a fullscreen one (i3 + Guake) presents at ~1 Hz (the NVIDIA
## driver sleeps in present): the terrain barely streams, nothing re-splits, and 0 gaps
## would be a FALSE PASS. Measured 2026-10-06: 1 fps, 896 chunks in 120 s hidden.
const MIN_FPS := 15
const ProbeWindow := preload("res://scenes/test/probe_window.gd")

var _ws := ""
var _fps_min := 1 << 30
var _map: Node
var _f := 0
var _cols: Array = []
var _prev: Array = []
var _samples := 0
var _gaps := 0
var _gap_cols := {}
var _first_hits := 0
var _name := ""


func _initialize() -> void:
	var a := OS.get_cmdline_user_args()
	_name = a[0] if a.size() > 0 else "map_geiss"
	_ws = ProbeWindow.free_workspace()
	OS.set_environment("GEISS_CAM", "0,40,16,0,-60")
	_map = (load("res://scenes/maps/%s.tscn" % _name) as PackedScene).instantiate()
	root.add_child(_map)
	for i in 10:
		for k in 10:
			_cols.append(Vector2(-45.0 + i * 10.0, 16.0 - 45.0 + k * 10.0))
	_prev.resize(_cols.size())
	_prev.fill(NAN)


func _physics_process(_d: float) -> bool:
	_f += 1
	if _f % 60 == 0 and not ProbeWindow.drawing(root):
		ProbeWindow.move_to(_ws, Vector2i(1600, 900))
	if _f < SETTLE_FRAMES or _f % EVERY != 0:
		return false
	_fps_min = mini(_fps_min, int(Engine.get_frames_per_second()))
	var space := root.get_world_3d().direct_space_state
	var hits := 0
	for i in _cols.size():
		var c: Vector2 = _cols[i]
		var r := space.intersect_ray(PhysicsRayQueryParameters3D.create(
				Vector3(c.x, 600.0, c.y), Vector3(c.x, -400.0, c.y)))
		var hit := not r.is_empty()
		if hit:
			hits += 1
		if _prev[i] == 1.0 and not hit:
			_gaps += 1
			_gap_cols[i] = true
		_prev[i] = 1.0 if hit else 0.0
	_samples += 1
	if _samples == 1:
		_first_hits = hits
	if _samples < SAMPLES:
		return false
	var ok := _first_hits >= 90 and _gaps == 0 and _fps_min >= MIN_FPS
	print("  %s: ground under %d/100 columns at the start; over %d samples (30 s) the ground VANISHED %d times under %d columns (min fps %d)"
			% [_name, _first_hits, _samples, _gaps, _gap_cols.size(), _fps_min])
	if _first_hits < 90:
		print("FAIL  too few columns had collision at all — the rig proves nothing")
	if _fps_min < MIN_FPS:
		print("FAIL  min fps %d < %d — window hidden (workspace '%s')? the terrain did not stream; proves nothing" % [_fps_min, MIN_FPS, _ws])
	print("PASS" if ok else "FAIL")
	quit(0 if ok else 1)
	return true