extends SceneTree
## THE KNIFE, AGAINST THE REAL LEVEL — a crash hunt, not an acceptance test.
##
## "$HYPER_TPS_GODOT" --headless --script scenes/test/knife_real_map_gate.gd
##
## operator_gate cuts synthetic 8x4x1 slabs, and every one of them passes. The knife
## crashes the game on START.MAP. The difference is not the operator — it is the
## GEOMETRY: 1183 imported brushes with dormant planes, sky volumes, split pieces and
## faces that bound nothing, none of which a hand-made box has.
##
## So this drives cut_preview and split_brush over EVERY real brush and prints as it
## goes, so a segfault names the brush it died on instead of leaving a silent exit.
##
## (A segfault AFTER "RESULT" is the known godot 4.6 GDExtension atexit crash, which
## dc_level_gate documents too. One before it is the bug.)
func _press(code: int) -> InputEventKey:
var e := InputEventKey.new()
e.physical_keycode = code
e.keycode = code
e.pressed = true
return e
## Exactly what the knife reaches for on the map, and nothing else.
class KnifeStub extends Node3D:
const GRID_UNITS: Array[int] = [1, 2, 4, 8, 16, 32, 64]
const WORLD_SCALE := 0.028
var _grid_idx := 3
var _map_center := Vector3.ZERO
var _aligned_offset := Vector3.ZERO
var level: Node = null
var aim_brush := -1
var aim_point := Vector3.ZERO
var _persist: StubDots = StubDots.new()
func _crosshair_hit() -> Dictionary:
if aim_brush < 0:
return {}
return {"position": aim_point, "normal": Vector3.UP, "shape": 0}
func _crosshair_ray() -> Dictionary:
var cam := get_viewport().get_camera_3d() if get_viewport() != null else null
if cam == null:
return {}
return {"from": cam.global_position, "dir": -cam.global_transform.basis.z}
func _brush_from_hit(hit: Dictionary) -> int:
return aim_brush if not hit.is_empty() else -1
func _refresh_brush_view() -> void:
pass
static func snap_to_map_grid(d: float, _n: Vector3, units: int,
_mc: Vector3, _ao: Vector3) -> float:
var s: float = float(units) * WORLD_SCALE
return roundf(d / s) * s
class StubDots extends RefCounted:
var n := 0
func reserve_dot() -> String:
n += 1
return "opgate:%d" % n
func record_at(_dot: String, _kind: String, _payload: Dictionary) -> bool:
return true
func record(_kind: String, _payload: Dictionary) -> String:
return reserve_dot()
func is_applying() -> bool:
return false
const MAP_PATH := "/storage/projects/gamedev/quake/quakemash/START.MAP"
const WORLD_SCALE := 0.028
var _lvl: Node3D = null
var _frame := 0
var _checked := 0
var _previewed := 0
var _cut := 0
var _refused := 0
func _initialize() -> void:
_lvl = ClassDB.instantiate("DCLevel") as Node3D
root.add_child(_lvl)
func _process(_dt: float) -> bool:
_frame += 1
# Frame-staged for the same reason dc_level_gate is: collision uploads need a
# world, and shapes are not in the broadphase until the space has stepped.
if _frame == 2:
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("[knife-gate] imported %d brushes" % n)
return false
if _frame < 4:
return false
# ---- EDIT THE LEVEL FIRST, which is the condition the crash needs -----------
# The probe caught it inside get_cut_preview on brush 944 in a session with
# carves: 15, pockets: 2, brushes: 1200 and that brush's planes moved repeatedly.
# Every previous run of this harness previewed cuts on a VIRGIN import — 1183
# brushes, no carves, no moved faces — which is why it never reproduced.
print("[knife-gate] carving and moving faces before any preview")
var carved := 0
for i in range(0, 15):
var cpos := Vector3(-14.0 + float(i) * 1.7, 3.0 + float(i % 4), 24.0 + float(i % 6))
if _lvl.apply_carve_sphere(cpos, 0.6, "gatecarve:%d" % i) >= 0:
carved += 1
_lvl.rebuild()
print("[knife-gate] %d carves, stats %s" % [carved, str(_lvl.get_stats())])
var moved := 0
for bid in range(900, 1000):
var bb: Dictionary = _lvl.get_brush(bid)
if bb.is_empty():
continue
var pls: PackedFloat32Array = bb.get("planes", PackedFloat32Array())
for fi in range(pls.size() / 4):
var nn := Vector3(pls[fi * 4], pls[fi * 4 + 1], pls[fi * 4 + 2])
if _lvl.set_face_plane(bid, fi, nn, pls[fi * 4 + 3] + 0.224):
moved += 1
break
_lvl.rebuild()
print("[knife-gate] %d faces moved" % moved)
# Walk every brush. The plane is the one the knife itself would build: a world
# axis through the brush's own centre, which is exactly where an aimed cut lands.
var total: int = _lvl.get_brush_count()
print("[knife-gate] cutting across %d brushes" % total)
var id := 0
var scanned := 0
# Ids are not dense — brushes are addressed by id, not by index — so walk a
# generous range and skip what is not there.
while id < 4000 and scanned < total:
var b: Dictionary = _lvl.get_brush(id)
id += 1
if b.is_empty():
continue
scanned += 1
var pl: PackedFloat32Array = b.get("planes", PackedFloat32Array())
if pl.size() < 16:
continue
# The brush's centre, from its own edge points — the same points the wireframe
# draws, so this is a place the geometry really is.
var pts: PackedVector3Array = _lvl.get_brush_edges_of(id - 1)
if pts.size() < 2:
continue
var c := Vector3.ZERO
for p: Vector3 in pts:
c += p
c /= float(pts.size())
for axis: Vector3 in [Vector3.RIGHT, Vector3.UP, Vector3.BACK]:
_checked += 1
if _checked % 500 == 0:
print("[knife-gate] %d previews, brush id %d" % [_checked, id - 1])
var d: float = axis.dot(c)
var poly: PackedVector3Array = _lvl.get_cut_preview(id - 1, axis, d)
if poly.size() >= 3:
_previewed += 1
# And actually CUT on the axis that previewed, because split_brush does more
# than cut_preview does: it commits, renames faces and appends a brush.
var dx: float = Vector3.RIGHT.dot(c)
if _lvl.get_cut_preview(id - 1, Vector3.RIGHT, dx).size() >= 3:
var made: int = _lvl.split_brush(id - 1, Vector3.RIGHT, dx,
"knifegate:%d" % (id - 1))
if made > 0:
_cut += 1
else:
_refused += 1
# ---- and now the OPERATOR itself, which is what actually crashes ----------
# The loop above proves the C++ is sound on real geometry. What the game runs is
# the GDScript operator on top of it, and that reaches for things a raw store call
# never touches: the aim, the camera, the journal, the preview meshes.
print("[knife-gate] driving the real KnifeOperator over real brushes")
var cam := Camera3D.new()
root.add_child(cam)
cam.current = true
var stub := KnifeStub.new()
stub.level = _lvl
root.add_child(stub)
var kn := KnifeOperator.new()
kn.level = _lvl
kn.map = stub
root.add_child(kn)
var driven := 0
var id2 := 0
while id2 < 400:
var b2: Dictionary = _lvl.get_brush(id2)
id2 += 1
if b2.is_empty():
continue
var pts2: PackedVector3Array = _lvl.get_brush_edges_of(id2 - 1)
if pts2.size() < 2:
continue
var c2 := Vector3.ZERO
for p2: Vector3 in pts2:
c2 += p2
c2 /= float(pts2.size())
stub.aim_brush = id2 - 1
stub.aim_point = c2
cam.global_position = c2 + Vector3(0, 0, 6)
cam.look_at(c2, Vector3.UP)
if not kn.on_activate():
continue
kn.update_preview()
kn.status()
# place points too — the free-plane path, on real faces
kn.handle_key(_press(KEY_P))
stub.aim_point = c2 + Vector3(0.3, 0.2, 0.0)
kn.handle_key(_press(KEY_P))
kn.update_preview()
kn.confirm()
kn.on_deactivate()
driven += 1
if driven % 50 == 0:
print("[knife-gate] operator driven on %d brushes (id %d)" % [driven, id2 - 1])
print("[knife-gate] operator survived %d brushes" % driven)
# ---- THE BRUSH VIEW, which every harness so far stubbed out ---------------
# _refresh_brush_view() is a no-op in operator_gate and in KnifeStub, and
# set_aimed() runs EVERY FRAME in the real game as the crosshair moves. So the
# wireframe is the one editor path that has never been exercised outside the
# running game — which is exactly the shape of a bug that only ever crashes there.
print("[knife-gate] driving BrushView over real brushes")
var bv := BrushView.new()
bv.level = _lvl
_lvl.add_child(bv)
bv.set_shown(true)
print("[knife-gate] refresh ok, %d segments" % bv.segment_count())
var aimed := 0
for bid in range(0, 1300):
bv.set_aimed(bid) # includes get_brush_vertices_of for the handles
aimed += 1
if aimed % 400 == 0:
print("[knife-gate] set_aimed through %d" % bid)
bv.set_aimed(-1)
bv.set_aimed(99999) # a brush id that does not exist
print("[knife-gate] set_aimed survived %d ids incl. out-of-range" % aimed)
# ---- HOW MANY PLANES DOES A REAL VERTEX TOUCH? ---------------------------
# "vertex/edge do not select individual but the whole brush." Selecting a vertex
# selects its INCIDENT planes — three on a cube corner — and moving it moves those
# planes. If real START.MAP brushes routinely return far more than three, the pick
# is over-collecting and the report is a bug rather than the model.
var hist := {}
var sampled := 0
var id3 := 0
while id3 < 600 and sampled < 200:
var b3: Dictionary = _lvl.get_brush(id3)
id3 += 1
if b3.is_empty():
continue
var verts: PackedVector3Array = _lvl.get_brush_vertices_of(id3 - 1)
if verts.is_empty():
continue
var pl3: PackedFloat32Array = b3.get("planes", PackedFloat32Array())
# The same epsilon the map uses: max(grid_step * 0.05, 1e-4) at grid 8u.
var eps: float = maxf(8.0 * 0.028 * 0.05, 1e-4)
for v: Vector3 in verts:
var n_inc := 0
for i in range(pl3.size() / 4):
if not _lvl.face_is_live(id3 - 1, i):
continue
var nn := Vector3(pl3[i * 4], pl3[i * 4 + 1], pl3[i * 4 + 2])
if absf(nn.dot(v) - pl3[i * 4 + 3]) < eps:
n_inc += 1
hist[n_inc] = int(hist.get(n_inc, 0)) + 1
sampled += 1
var keys: Array = hist.keys()
keys.sort()
var line := ""
for k in keys:
line += "%d planes:%d " % [int(k), int(hist[k])]
print("[knife-gate] incident planes per REAL vertex over %d brushes -> %s" % [sampled, line])
print("[knife-gate] rebuilding after %d cuts" % _cut)
_lvl.rebuild()
print("[knife-gate] RESULT previews_ok=%d/%d cuts=%d refused=%d brushes_now=%d"
% [_previewed, _checked, _cut, _refused, _lvl.get_brush_count()])
quit(0)
return true