OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_collision_shapes.gd

138 lines · gd ref:
extends Node
## GATE: no terrain collision shape is rejected by Jolt. ashatron's log (2026-10-01)
## has "Failed to build Jolt Physics concave polygon shape with {vertex_count=6}.
## ... 'Need triangles to create a mesh shape!'": a chunk shape whose every triangle
## Jolt's MeshShapeSettings::Sanitize drops (vertices welded within 1e-4, |cross|^2
## <= 1e-12, or collapsed by quantization), leaving a body with an empty shape.
##
##   "$G" --path . --rendering-driver vulkan --resolution 1280x720 res://scenes/test/gate_collision_shapes.tscn
##
## Replays the ashatron session's 600 carves on the mountain (the same fixture as
## gate_kernel_finite) with a Logger counting engine errors that mention
## "Need triangles" or "concave polygon shape"; passes when there are none.
## ⛔ NOT HEADLESS (the mountain's field is GPU-only).

const MAP := "res://scenes/maps/map_mountain.tscn"
const MAP_ID := "map_mountain"
const REPLAY := "res://scenes/test/data/ashatron_edits_2026-10-01.txt"
const R := 3.0
const SETTLE_FRAMES := 60
const SETTLE_CAP_S := 90.0


class ShapeErrors extends Logger:
	var mutex := Mutex.new()
	var count := 0
	var first := ""

	func _log_error(_function: String, _file: String, _line: int, code: String, rationale: String,
			_editor_notify: bool, _error_type: int, _script_backtraces: Array[ScriptBacktrace]) -> void:
		var text := rationale if not rationale.is_empty() else code
		if "Need triangles" in text or "concave polygon shape" in text:
			mutex.lock()
			count += 1
			if first.is_empty():
				first = text
			mutex.unlock()

	func _log_message(_message: String, _error: bool) -> void:
		pass


var _map: Node
var _cam: Camera3D
var _errors := ShapeErrors.new()


func _ready() -> void:
	OS.add_logger(_errors)
	_run.call_deferred()


func _since(t0: int) -> float:
	return (Time.get_ticks_msec() - t0) / 1000.0


func _terrain() -> Node:
	return _map.get_node_or_null(^"MountainTerrain")


func _settle() -> void:
	var tr := _terrain()
	var t0 := Time.get_ticks_msec()
	var calm := 0
	while calm < SETTLE_FRAMES and _since(t0) < SETTLE_CAP_S:
		await get_tree().process_frame
		var ok: bool = bool(tr.call("is_lod_settled")) and int(tr.call("get_pending_count")) == 0 \
				and int(tr.call("get_collision_queue_size")) == 0 and int(tr.call("get_worker_queue_depth")) == 0
		calm = calm + 1 if ok else 0


func _errs() -> int:
	_errors.mutex.lock()
	var n := _errors.count
	_errors.mutex.unlock()
	return n


func _run() -> void:
	if DisplayServer.get_name() == "headless":
		print("RESULT: SKIP (needs a display)")
		get_tree().quit(2)
		return
	GameState.enter_map(MAP_ID)
	var mgr := TerrainEditManager.new()
	mgr.name = "TerrainEditManager"
	add_child(mgr)
	_cam = Camera3D.new()
	_cam.far = 20000.0
	add_child(_cam)
	_cam.current = true
	_map = (load(MAP) as PackedScene).instantiate()
	add_child(_map)
	var spawn := Vector3(0, 250, 0)
	var sp := get_tree().get_nodes_in_group("spawn_points")
	if not sp.is_empty():
		spawn = (sp[0] as Node3D).global_position
	_cam.global_position = spawn + Vector3(0, 2, 0)
	_cam.look_at(spawn + Vector3(20, 0, 0))
	await _settle()
	print("[GATE-COL] after load: %d rejected shapes" % _errs())

	var f := FileAccess.open(REPLAY, FileAccess.READ)
	var edits: Array = []
	while f != null and not f.eof_reached():
		var l := f.get_line().strip_edges()
		if l.is_empty() or l.begins_with("#"):
			continue
		var t := l.split(" ")
		edits.append([t[0], Vector3(float(t[1]), float(t[2]), float(t[3]))])
	var prev := _errs()
	for i in edits.size():
		var kind: String = edits[i][0]
		var c: Vector3 = edits[i][1]
		_cam.global_position = c + Vector3(0, 12, 12)
		_cam.look_at(c)
		match kind:
			"hard": mgr.call("apply_sphere", c, R, -3.0, 1)
			"smooth": mgr.call("apply_sphere", c, R, -3.0, 0)
			_: mgr.call("apply_edit", c, Vector3(R, R, R), -3.0)
		for k in 4:
			await get_tree().process_frame
		var n := _errs()
		if n > prev:
			print("[GATE-COL] +%d rejected shape(s) after edit %d (%s at %s)" % [n - prev, i, kind, str(c)])
			prev = n
	await _settle()
	var total := _errs()
	if tr_has_stats():
		print("[GATE-COL] dc-terrain collision sanitize: %s" % str(_terrain().call("get_collision_sanitize_stats")))
	print("[GATE-COL] %s: %d rejected shape(s)%s" % ["PASS" if total == 0 else "FAIL", total,
			(" -- first: " + _errors.first) if total > 0 else ""])
	print("RESULT: %s" % ("PASS" if total == 0 else "FAIL"))
	get_tree().quit(0 if total == 0 else 1)


func tr_has_stats() -> bool:
	return _terrain() != null and _terrain().has_method("get_collision_sanitize_stats")