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")