extends SceneTree
## Headless diagnostic: parse start.rtlights through RtlightManager and dump the DERIVED
## per-light values (DarkPlaces canonical — spec/quake-darkplaces-transfer.md §3), so we can
## verify the parse/intensity/range/shadow-flag math OFFLINE before any deploy. Run:
## "$HYPER_TPS_GODOT" --headless --script res://scenes/test/rtlight_dump.gd
##
## Expected (hand-computed from start.rtlights lines vs r_shadow.c):
## line 1 "!992 1948 -14 160 1.0 0.5 0.2 ... 1 1 1 2" -> shadow OFF, energy 1+1+1=3.0, range 160*0.028=4.48
## line 2 "-150 2550 148 256 1.0 0.7 0.4 ... 0.6 1 1 2" -> shadow ON, energy 0.6+1+1=2.6, range 256*0.028=7.17
## i.e. energy must VARY per line (was a flat 3.0) and range must be radius*scale (no ×1.25).
func _init() -> void:
var map_path := "/storage/projects/gamedev/quake/quakemash/START.MAP"
var rtlights_path := "/storage/projects/gamedev/quake/quake-rtlights/id1/maps/start.rtlights"
var scale := 0.028
# Import START.MAP first to recover the SAME recenter+offset the real map uses.
var importer := QuakeMapImporter.new()
var stub := StubTerrain.new()
importer.import_file(stub, map_path, scale, Vector3(0, 20, 0), 1.0, null, true, {})
var map_center: Vector3 = importer.out_map_center
var offset: Vector3 = importer.out_aligned_offset
var mgr := RtlightManager.new()
var n := mgr.setup(rtlights_path, map_center, scale, offset)
# Gather every spawned OmniLight3D + whether it is shadow-eligible (in _casters).
var caster_lamps := {}
for c in mgr._casters:
caster_lamps[c.light.get_instance_id()] = true
var energies: Array[float] = []
var ranges: Array[float] = []
var lamps: Array = []
for child in mgr.get_children():
if child is OmniLight3D:
lamps.append(child)
energies.append(child.light_energy)
ranges.append(child.omni_range)
print("=== rtlights dump: %d spawned, %d shadow-eligible ===" % [n, mgr._casters.size()])
# Distribution: proves energy is no longer flat and ranges vary.
print("energy min=%.3f max=%.3f mean=%.3f (flat 3.0 would mean the per-line scales are ignored)" % [
_min(energies), _max(energies), _mean(energies)])
print("range min=%.3f max=%.3f mean=%.3f (m; = radius*%.3f, no ×1.25 fudge)" % [
_min(ranges), _max(ranges), _mean(ranges), scale])
print("distinct energies:", _distinct(energies))
# Spot-check the two known lines by their world position.
_spot("line1 !992,1948,-14 (expect shadow OFF, E=3.0, R=4.48)",
importer, Vector3(992, 1948, -14), map_center, scale, offset, lamps, caster_lamps)
_spot("line2 -150,2550,148 (expect shadow ON, E=2.6, R=7.17)",
importer, Vector3(-150, 2550, 148), map_center, scale, offset, lamps, caster_lamps)
print("--- first 8 lamps ---")
for i in mini(8, lamps.size()):
var l: OmniLight3D = lamps[i]
print(" [%d] E=%.3f R=%.3f col=(%.2f,%.2f,%.2f) caster=%s" % [
i, l.light_energy, l.omni_range, l.light_color.r, l.light_color.g, l.light_color.b,
str(caster_lamps.has(l.get_instance_id()))])
quit()
func _spot(label: String, importer: QuakeMapImporter, quake_xyz: Vector3, mc: Vector3,
scale: float, off: Vector3, lamps: Array, casters: Dictionary) -> void:
var want := ((QuakeMapImporter._q2g(quake_xyz.x, quake_xyz.y, quake_xyz.z) - mc) * scale) + off
var best: OmniLight3D = null
var bd := INF
for l in lamps:
var d: float = (l as OmniLight3D).position.distance_to(want) # mgr at origin → position == world
if d < bd:
bd = d; best = l
if best == null:
print(" %s -> NO LAMP" % label); return
print(" %s -> E=%.3f R=%.3f col=(%.2f,%.2f,%.2f) caster=%s (match dist=%.3fm)" % [
label, best.light_energy, best.omni_range, best.light_color.r, best.light_color.g,
best.light_color.b, str(casters.has(best.get_instance_id())), bd])
class StubTerrain extends Node:
func edit_terrain_box(_c, _h, _s, _i, _m) -> void: pass
func edit_terrain_convex(_planes, _c, _r, _s, _i, _m, _fp) -> void: pass
func _min(a: Array[float]) -> float:
var m := INF
for v in a: m = minf(m, v)
return m if a.size() > 0 else 0.0
func _max(a: Array[float]) -> float:
var m := -INF
for v in a: m = maxf(m, v)
return m if a.size() > 0 else 0.0
func _mean(a: Array[float]) -> float:
if a.is_empty(): return 0.0
var s := 0.0
for v in a: s += v
return s / a.size()
func _distinct(a: Array[float]) -> Array:
var seen := {}
for v in a:
seen[snappedf(v, 0.01)] = true
var keys := seen.keys()
keys.sort()
return keys