Boresight parallax FIXED + the #4/#5 verdict instrumentation (Gitea #16, #4, #5)

PARALLAX (#16): the pick/fire ray was anchored at mech.y+5.0 (a port
improvisation) while the sight line ran from the eyepoint (y=6.23) --
two parallel rays whose constant offset grew into the reported low-miss
as range closed (measured ry +0.072 @50u -> +1.54 @2.7u).  The decomp's
sight and pick share the eye origin (HudSimulation @4b7830 chain).  Fix:
the viewpoint mech's cockpit eye owns the aim-camera publish in BOTH
views, origin = its own eye translation; leveling + deliberate elevation
untouched; chase view now converges to cockpit ballistics (V cannot
change where shots land).  After: pick pinned to the crosshair (ry <=
5e-6) from 50u to point-blank; 26 laser + 8 missile center-mass hits at
3/4-screen.  Awaiting the reporters' approach-test.

VERDICT INSTRUMENTATION (#4 closed authentic, #5 verdict posted):
BT_RANGE_LOG per-frame pick tracing + BTGroundRayHitExact analytic
cross-check (0/8000 arena fall-throughs; cavern 6/8400 single-frame
grazes -- the 'crazy sliding' is the authentic world-pick + 500 m/s
slide over depth discontinuities); BT_AUD_TAIL StopNote/fade timing +
BT_FIRE_PULSE single-shot driver (the energy 'buzz' is the AUTHORED
charging loop: crescendo through recharge, 1.309s authored release,
measured within one frame).  CAVERN.EGG: solo cavern test egg.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-20 14:51:54 -05:00
co-authored by Claude Fable 5
parent d7158f1f82
commit 70eea6c1a4
13 changed files with 744 additions and 18 deletions
+229
View File
@@ -0,0 +1,229 @@
[mission]
adventure=BattleTech
map=cavern
scenario=freeforall
time=day
weather=clear
temperature=27
length=600
[ordinals]
bitmap=Ordinal::BitMap::1
bitmap=Ordinal::BitMap::2
bitmap=Ordinal::BitMap::3
bitmap=Ordinal::BitMap::4
[Ordinal::BitMap::1]
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=000038000003C000001FF00000000F00
bitmap=0000F8000003C000003FF80000000F00
bitmap=0001F8000003C00000707C0000000F00
bitmap=0001F8000003C00000603C0000000F00
bitmap=00007801FC0FF00000003C3DF807FF00
bitmap=00007803FE0FF00000003C3FFC0FFF00
bitmap=00007803C703C00000003C3E3C1F0F00
bitmap=000078078303C0000000783C1E1E0F00
bitmap=000078078003C0000000783C1E1E0F00
bitmap=00007807C003C0000000F03C1E1E0F00
bitmap=00007807F003C0000001E03C1E1E0F00
bitmap=00007803FC03C0000003C03C1E1E0F00
bitmap=00007801FE03C0000007803C1E1E0F00
bitmap=000078007F03C000000F003C1E1E0F00
bitmap=000078001F03C000001E003C1E1E0F00
bitmap=000078000F03C000003C003C1E1E0F00
bitmap=000078060F03C0000078003C1E1E0F00
bitmap=000078071E03E0000078003C1E1F1F00
bitmap=00007803FE01F000007FFC3C1E0FFF00
bitmap=00007801FC00F000007FFC3C1E07EF00
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
x=128
y=32
width=8
[Ordinal::BitMap::2]
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=001FFF0000003C0000003C03C0780000
bitmap=001FFF0000003C0000007C03C0780000
bitmap=00000E0000003C000000FC03C0780000
bitmap=00003C0000003C000001BC03C0780000
bitmap=0000700F3E1FFC000003BC0FF07BF000
bitmap=0001E00F7E3FFC0000073C0FF07FF800
bitmap=0003800FFE7C3C0000063C03C07C7800
bitmap=0007F80FFE783C00000C3C03C0783C00
bitmap=0007FE0F80783C0000183C03C0783C00
bitmap=00001E0F00783C0000383C03C0783C00
bitmap=00000F0F00783C0000703C03C0783C00
bitmap=00000F0F00783C00007FFF03C0783C00
bitmap=00000F0F00783C00007FFF03C0783C00
bitmap=00000F0F00783C0000003C03C0783C00
bitmap=00000F0F00783C0000003C03C0783C00
bitmap=00000F0F00783C0000003C03C0783C00
bitmap=00180F0F00783C0000003C03C0783C00
bitmap=001C1F0F007C7C0000003C03E0783C00
bitmap=000FFE0F003FFC0000003C01F0783C00
bitmap=0007FC0F001FBC0000003C00F0783C00
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
x=128
y=32
width=8
[Ordinal::BitMap::3]
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=001FFF03C07800000000FC03C0780000
bitmap=001FFF03C07800000003FC03C0780000
bitmap=001E0003C07800000007C003C0780000
bitmap=001E0003C0780000000F0003C0780000
bitmap=001E000FF07BF000000F000FF07BF000
bitmap=001E000FF07FF800001E000FF07FF800
bitmap=001E0003C07C7800001E0003C07C7800
bitmap=001FFC03C0783C00001EFC03C0783C00
bitmap=001FFE03C0783C00001FFE03C0783C00
bitmap=00001F03C0783C00001F1F03C0783C00
bitmap=00000F03C0783C00001E0F03C0783C00
bitmap=00000F03C0783C00001E0F03C0783C00
bitmap=00000F03C0783C00001E0F03C0783C00
bitmap=00000F03C0783C00001E0F03C0783C00
bitmap=00000F03C0783C00001E0F03C0783C00
bitmap=00000F03C0783C00001E0F03C0783C00
bitmap=00180F03C0783C00001E0F03C0783C00
bitmap=001C1F03E0783C00001F1F03E0783C00
bitmap=000FFE01F0783C00000FFE01F0783C00
bitmap=0007FC00F0783C000007FC00F0783C00
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
x=128
y=32
width=8
[Ordinal::BitMap::4]
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=001FFF03C07800000007FC03C0780000
bitmap=001FFF03C0780000000FFE03C0780000
bitmap=00000F03C0780000001F1F03C0780000
bitmap=00000F03C0780000001E0F03C0780000
bitmap=00000F0FF07BF000001E0F0FF07BF000
bitmap=00001F0FF07FF800001E0F0FF07FF800
bitmap=00001E03C07C7800001E0F03C07C7800
bitmap=00003E03C0783C00001E0F03C0783C00
bitmap=00003C03C0783C00000F1E03C0783C00
bitmap=00003C03C0783C000007FC03C0783C00
bitmap=00007803C0783C000007FC03C0783C00
bitmap=00007803C0783C00000F1E03C0783C00
bitmap=00007803C0783C00001E0F03C0783C00
bitmap=0000F003C0783C00001E0F03C0783C00
bitmap=0000F003C0783C00001E0F03C0783C00
bitmap=0000F003C0783C00001E0F03C0783C00
bitmap=0000F003C0783C00001E0F03C0783C00
bitmap=0000F003E0783C00000F1E03E0783C00
bitmap=0000F001F0783C00000FFE01F0783C00
bitmap=0000F000F0783C000007FC00F0783C00
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
x=128
y=32
width=8
[pilots]
pilot=200.0.0.96
[200.0.0.96]
hostType=0
advancedDamage=1
loadzones=1
name=Aeolus
bitmapindex=1
experience=expert
badge=VGL
patch=Yellow
role=Role::Default
dropzone=one
vehicle=bhk1
vehicleValue=1000
color=White
[largebitmap]
bitmap=BitMap::Large::Aeolus
[BitMap::Large::Aeolus]
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=000003FE0000000001E0000000000000
bitmap=000007FF0000000001E0000000000000
bitmap=00000F8F8000000001E0000000000000
bitmap=00000F078000000001E0000000000000
bitmap=00000F0781FF00FF81E0F0781FC00000
bitmap=00000F0783FF81FFC1E0F0783FE00000
bitmap=00000F0787C7C3E3E1E0F0783C700000
bitmap=00000F078783C3C1E1E0F07878300000
bitmap=00000F078783C3C1E1E0F07878000000
bitmap=00000FFF8783C3C1E1E0F0787C000000
bitmap=00000FFF8783C3C1E1E0F0787F000000
bitmap=00000F0787FFC3C1E1E0F0783FC00000
bitmap=00000F0787FFC3C1E1E0F0781FE00000
bitmap=00000F07878003C1E1E0F07807F00000
bitmap=00000F07878003C1E1E0F07801F00000
bitmap=00000F07878003C1E1E0F07800F00000
bitmap=00000F078780C3C1E1E0F07860F00000
bitmap=00000F0787C1C3E3E1E078F871E00000
bitmap=00000F0783FF81FFC1E07FF83FE00000
bitmap=00000F0781FF00FF81E03F781FC00000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
x=128
y=32
width=8
[smallbitmap]
bitmap=BitMap::Small::Aeolus
[BitMap::Small::Aeolus]
bitmap=00000000000000000000000000000000
bitmap=0000000000000000003E00000C000000
bitmap=006300000C00000000631F0F8CC63C00
bitmap=00633198CCC66200007F3198CCC66000
bitmap=00633F98CCC6780000633018CCC61E00
bitmap=00633018CCC6060000633098CCC64600
bitmap=00631F0F8C7E3C000000000000000000
bitmap=00000000000000000000000000000000
x=64
y=16
width=4
[Role::Default]
model=dfltrole
[Role::NoReturn]
model=noretun
+41 -1
View File
@@ -79,11 +79,39 @@ jointtorso — NO explicit eye-side compose exists, one briefly added on false "
probe readings double-rotated the view and is retired, see [[cockpit-view]]); the crosshair sits
at screen center (`gBTAimX = 0` — the old `BTTwistToReticleX` slew was THE bug: the camera
already yawed, so the crosshair counter-slid to HULL-forward, "twisting leaves the crosshairs
behind"; `BT_AIM="x y"` still pins it for headless tests) → `BTGetAimRay` (the ACTIVE eye
behind"; `BT_AIM="x y"` still pins it for headless tests) → `BTGetAimRay` (the BORESIGHT eye
publishes pos+basis via `BTSetAimCamera`, leveled in pitch, the twist yaw riding in the basis) →
`Mech::PickRayHit` (world ray → local frame → `BoundingBox::HitBy` slab test vs the collision
template's ExtentBox → world hit point).
**PICK-RAY ORIGIN = the TRUE EYEPOINT (Gitea #16 "shots land LOW, worse up close" — FIXED
2026-07-20) [T1 evidence / T2 fix].** The task-#48 publish anchored the pick ray at
`mech.y + 5.0` (a port improvisation) while the crosshair's sight line runs from the eyepoint
(~y 6.2 on the DEV mech) — two parallel rays with a constant vertical offset, so the pick point
slid DOWN the reticle as range closed (measured pre-fix via `BT_PARALLAX_LOG`, the pick point
back-projected through the RENDER view: ry +0.07 @ 50u → +0.17 @ 16u → +0.32 @ 10u → +1.5 @
2.7u; lasers AND missiles consume the pick, so both landed low). The binary's sight math is
EYE-ORIGINATED [T1]: HudSimulation's designator HotBoxVector = target position transformed by
`inverse(eyeSegToWorld)``inverse(R(EyepointRotation))` where eyeSegToWorld = the HUD's mount
SEGMENT frame × mech localToWorld (`@004b7830` part_013.c:5688-5702 — Ghidra drops the segment
arg, disasm shows `hud+0xdc`; the seg→world composer is `FUN_00424da8` @00424da8); the VIEW is
`inverse(jointtorso→jointeye→siteeyepoint)` (`FUN_004c22c4` part_013.c:11742); and the fired
beam CONVERGES from the muzzle segment to the picked point `mech+0x37c` (`@004bace8`
part_013.c:7742-7752) — so pick ray and sight line share the eye origin (the pod's division
card cast from the view per Auric; the 0x37c/0x388 writer itself is in the un-exported gap).
**The fix:** the viewpoint mech's cockpit `siteeyepoint` eye is marked the **BORESIGHT eye**
(`DPLEyeRenderable::SetBoresightEye`, set in btl4vid.cpp) and owns the `BTSetAimCamera`
publish in BOTH views (it executes in the mech's draw traversal every frame), origin = its own
eye translation (eyeW row 4 — the exact frame the crosshair projects from); the leveling +
`gBTEyeElev` re-application are unchanged. Chase ballistics are therefore IDENTICAL to cockpit
(verified: chase-view pick lands at y=6.23076 = the cockpit eye height while the chase camera
sits at y=11); the legacy `+5` anchor survives only as the no-boresight-eye fallback
(spectator). Verified after: ry ≤ 5e-6 from 50u to point-blank 2.2u in cockpit AND glass
builds; 26 laser `[dmghit]`s + 8 missile hits at range 1.7-2.2 on center-mass/upper zones with
the lock ring on the target chest (`scratchpad/i16_laser.png`/`i16_missile.png`). Diags:
`BT_PARALLAX_LOG` (pick→render-view back-projection via `BTProjectToRenderView`),
`BT_GOTO_STOP`/`BT_GOTO_THR` (approach-test stop radius / walk throttle).
**THE WORLD-PICK TARGET MODEL (task #41, the reconciliation — binary + pod-video evidence).**
The target slot `mech+0x388` holds **whatever entity is downrange of the torso boresight** — a
mech OR a WORLD entity (terrain/mesa/building). Evidence: (a) the weapon fire path is DOUBLY
@@ -387,6 +415,18 @@ thing: an EXPLICIT `JointedMover::~JointedMover()` in the reconstructed `~Mech`
chain TWICE (the dtor-epilogue rule, [[reconstruction-gotchas]] §7) → a double-free of `collisionLists`
+ the segment table. Removing it fixed BOTH the death-row crash AND app-exit crashes. [T2]
**⚠ The SILENT death-crash (Gitea #12, FIXED 2026-07-20) was NOT a mech-teardown bug at all — it
was the DEATH-EXPLOSION AUDIO.** A silent hard crash (no WER/markers) a few seconds after ANY mech
death — solo dummy, MP self-death, MP peer PEER_DOWN — deterministic on the first kill. Root cause:
the death boom is the **AllExplosion preset (25 layered zones)**, but the port's OpenAL
`SourceSet.sources[]` array was only **5** wide, so acquiring channels for it overwrote adjacent
heap; the corruption turned fatal seconds later at the death Explosion's own audio-entity teardown
(`FryDeathRow → Explosion::~Explosion → …DestroyEntityAudioObjects → ~Dynamic3DPatchSource →
operator delete`) or the next sequenced virtual dispatch (`AudioControlSequence::RunSequence →
Send`, garbage vtable). Fixed by sizing `sources[]` to the real max — see [[reconstruction-gotchas]]
§21 (Gitea #12). NOT the held #16 boresight-eye dtor (exonerated), NOT the `rev=-64` mppr value
(a `ControlsButton` negative = release, benign). [T2]
## Death SEQUENCE reconstruction — the map (2026-07-08)
Full decomp map of the death path. Two tiers:
**EXPORTED (reconstructable) [T1 read]:**
+6 -1
View File
@@ -131,7 +131,10 @@ valid for reading the decomp, NOT for our compiled layout (see [[reconstruction-
### BTPlayer (0x294) experience/game-mode flag block (ctor @4c0bc8; see [[experience-levels]]) [T1]
Seeded on the master branch from `btMission(+0x1f8)->experienceLevel(+0xe4)` (egg `experience`,
parse @4d2f3e`BTMission+0xe4`) and `advancedDamageOn(+0xf0)`:
parse @4d2f3e`BTMission+0xe4`) and `advancedDamageOn(+0xf0)`. The +0x1f8 mission pointer is
the inherited `Player::playerMission`. Port members (issue #2): `simLive`/`heatModelOn`/
`advancedDamageOn`(+2)/`levelFlag26c`/`levelFlag270`/`experienceLevel`; bridges
`BTPlayerExperienceSimLive`/`…HeatModelOn`/`BTPlayerRoleLocksAdvanced` (btplayer.cpp):
| Offset | Value by level (nov/std/vet/exp) | Verified readers |
|---|---|---|
@@ -321,6 +324,8 @@ default-ON (`'0'` disables).
| `BT_CRIT_PROBE=<zone>` | hammer one own-mech zone every 4s (crit-propagation diag, task #2; mech4.cpp) |
| `BT_REPL_LOG` | replicant/MP replication log (mech4.cpp) |
| `BT_GOTO="enemy"\|"x z"` / `BT_GOTO_LOG` | self-driving beeline (to the enemy or a map coordinate) + its log |
| `BT_GOTO_STOP=<u>` / `BT_GOTO_THR=<0..1>` | beeline stop-radius / throttle overrides (Gitea #16 approach test: walk all the way in) |
| `BT_PARALLAX_LOG` | 2 Hz `[parallax]`: the pick point back-projected through the RENDER view (`BTProjectToRenderView`, L4VIDEO.cpp) — a healthy boresight holds (rx,ry)≈(0,0) at every range (Gitea #16; [[combat-damage]] Targeting) |
| `BT_MP_LOG` / `BT_MP_NET` / `BT_MP_FORCE_DMG` | MP diagnostics / forced cross-pod damage |
| `BT_AUTODRIVE=<0..1>` | headless throttle |
| `BT_KEY_NOFOCUS=1` | accept key input without window focus |
+21 -1
View File
@@ -337,7 +337,27 @@ and every instrument is now live [T2]:**
**GREEN width-1 caret triangle** (ctor @4550-4551 sets green/1 AFTER the yellow bar call —
transcription color bug caught by a period reference screenshot, 2026-07-09; same for the
bottom bowtie carets @4569-4570); pegs at 1200 with no target; the DISPLAYED range slides at
**500 m/s** toward the true pick range (HudSimulation :5652 [T1]). ⚠ A period pod screenshot
**500 m/s** toward the true pick range (HudSimulation :5652 [T1]).
**VERDICT (Gitea #4, 2026-07-20): the "range slides in/out crazily while walking" report is
AUTHENTIC behavior, not a bug [T2 measured].** Per-frame `BT_RANGE_LOG` traces (mech4.cpp, with
an independent Möller-Trumbore cross-check `BTGroundRayHitExact` in btvisgnd.cpp) on scripted
walks: ARENA1 garage cluster — every frame a real structure-face pick, true range legitimately
hopping 40↔600 m at silhouette edges, the caret in motion 74% of frames / ~24 direction
reversals per 10 s; CAVERN butte field — 806 instant >50 m jumps, 776 butte↔1200-default
boundary crossings, 106/139 s windows swinging >300 m (max 1143 m), caret reversing ~54×/10 s.
Every value tracks real geometry; the 500 m/s slide (authentic) then never rests. Elevation was
NOT the driver (arena floor is flat y=0) — depth discontinuities are. Cyd's "ray falls through
geometry" was checked: 0 fall-through frames on arena, 6/8400 (0.07%, single-frame silhouette
grazes) on cavern. Bounded infidelities noted for follow-up, NOT the reported symptom: (a) butte
collision (stepped YCyl tiers + cone, buttee_c.sld) is NARROWER than the sculpted buttee.bgf
rock — measured a 0.27 m graze past the r=14.6 tier while visibly inside rock (terrain-march
tier backstops it, error a few meters at point-blank); (b) the cavern's `butteeu` upper butte
sections (buttee.bgf re-instanced at y=50, VideoModel-only, NO solid stream) are
pick-transparent — aiming steeply up at a rock tower's top half reads through it; (c) boot
transient: before the aim camera is valid, the max-range fallback designates from uninitialized
ray floats → `shown` overshoots (~3400) and slides down for the first ~7 s (cosmetically hidden
by the 1200 caret peg). Traces: scratchpad i4_*_walk*.log; issue #4 comment has the summary.
⚠ A period pod screenshot
(`C:\git\image.webp`, likely a DIFFERENT pod revision — its crosshair is ~2.5× taller with
arrowhead arms, NOT our binary's ±0.04..0.16 program) structurally CONFIRMS our layout: yellow
bar + green caret + colored pip dots on the right ladder, bottom tape + green bowtie, rotating
+6 -1
View File
@@ -141,7 +141,12 @@ and the boresight pointed that far DOWN, hitting terrain ~50-100 m short of the
started over the mech's shoulder. Fix (L4VIDRND `DPLEyeRenderable::Execute`, the `BTSetAimCamera` publish):
LEVEL the boresight (drop the view-dir pitch, rebuild an upright basis with world +Y) AND anchor its ORIGIN
at the mech's torso (`myEntity->localToWorld` translation + ~5) instead of the render eye. The guns now fire
along the mech's horizontal heading from the torso, independent of view mode and terrain tilt. The reticle X
along the mech's horizontal heading from the torso, independent of view mode and terrain tilt.
**⚠ ORIGIN part SUPERSEDED (2026-07-20, Gitea #16):** the `+5` torso anchor sat BELOW the sight line →
shots landed LOW, worse at close range (boresight parallax). The pick ray origin is now the TRUE
EYEPOINT: the viewpoint mech's cockpit `siteeyepoint` eye (the BORESIGHT eye) publishes the aim camera
in BOTH views from its own eye translation — see [[combat-damage]] Targeting §parallax. The LEVELING +
`gBTEyeElev` re-application stand unchanged. The reticle X
still carries the torso twist (`BTTwistToReticleX`); reticle Y any elevation. VERIFIED end-to-end: solo,
enemy 120 m ahead, **both chase AND cockpit** view → `[target] MECH under boresight ... mechPicks=59
groundPicks=0` → beam → `*** DESTROYED`. Also proved `PickRayHit` resolves a REPLICANT when aimed at it
+17
View File
@@ -127,6 +127,23 @@ on top of it. Full detail: `docs/PROGRESS_LOG.md §5b, §8`.
saturate early and fake "per-stride" rates — always timestamp before inferring cadence.
- Still deferred: F21 attack velocity/time (all captured events were default 1), F22 HRTF
(user has 2 speakers; enable via alsoft.ini `hrtf=true` for headphones).
- **Energy-weapon post-beam buzz = AUTHENTIC (Gitea #5 verdict, 2026-07-20)** [T2, measured]:
the reported "buzz/electrical sustaining 12 s past the beam" is the AUTHORED charge loop
(`LaserACharge01-04`, looped, release 1.309 s), started by the WeaponState==3 (Loading) watcher
at beam-off, volume authored to track PercentDone (crescendo to ready), stopped on 3→2 with the
authored 1.309 s release fade. Measured live (BT_AUD_TAIL=1 + BT_FIRE_PULSE single shots):
beam 203207 ms (authored DischargeTime 0.2 s); sustain loops cut ≤2 ms after leave-Firing
(authored release 0); charge StopNote ≤2 ms after Loading→Loaded; fades 1.3111.326 s vs
authored 1.309 s (≤ 1 frame over); laser recharge 2.33.8 s, PPC 8.816.3 s under multi-weapon
generator load (authentic electrical model). NOT the #12 source overflow — no energy preset
exceeds 5 zones (max 3; >5 is only the death/UI families: AllExplosion 25, MechExplosion01/
Death01 16, AllButtons 14, AllCoolant/AllProjectile 9, Warnings01/AuxExplosion01/AllHits 8,
AllWarning 7). Caveat: the reporter's 2026-07-18 build predated 2edde29 (Emitter::SetDirty
DelayWatchersFlag poison retirement — PerformAndWatch skips ExecuteWatchers while set,
SIMULATE.cpp:461; cleared only by the encore path, UPDATE.cpp:215), which made emitter audio
watcher timing encore-erratic that session. Harness kept (env-gated, additive): `[aud-tail]`
logging in `L4AUDLVL.cpp` (PLAY/STOP/fade) + `emitter.cpp` (state transitions), and
`BT_FIRE_PULSE="on,off"` single-shot trigger pulsing in `mech4.cpp`.
- **L4 HAL on Windows + the VS build system: DONE.** [T1]
- **Red Planet game logic: COMPLETE & buildable** — `VTV/VTVMPPR/WEAPSYS/...`. RP is at/near playable. [T1]
- **Multiplayer: the DOS `NETNUB` driver is already replaced by a ~3.5k-line WinSock2 TCP
+36
View File
@@ -17,6 +17,19 @@
// per frame from AudioHead::Execute; a fade finishing (or being reclaimed by a
// restart) stops + rewinds the source so the next SetupPatch sees AL_INITIAL.
//
// [aud-tail] (Gitea #5 verdict harness, additive): BT_AUD_TAIL=1 gates
// timestamped play/stop/fade tracing so the audible tail past beam-off can be
// measured against the authored releaseSec. clock() is CRT wall-clock ms and
// matches the [aud-tail] emitter state-transition logs (emitter.cpp).
#include <ctime>
static bool AudTailLog()
{
static int s_on = -1;
if (s_on < 0) s_on = (getenv("BT_AUD_TAIL") != 0) ? 1 : 0;
return s_on != 0;
}
namespace
{
struct ReleaseFade
@@ -32,6 +45,10 @@ namespace
void FinishFade(ReleaseFade &fade)
{
if (AudTailLog())
DEBUG_STREAM << "[aud-tail] t=" << (long)clock() << "ms fade-END src="
<< fade.source << " after " << fade.elapsed << "/" << fade.duration
<< "s\n" << std::flush;
alSourceStop(fade.source);
alSourceRewind(fade.source);
alSourcef(fade.source, AL_GAIN, fade.gain0);
@@ -57,6 +74,10 @@ namespace
fade.duration = duration;
fade.elapsed = 0.0f;
fade.active = true;
if (AudTailLog())
DEBUG_STREAM << "[aud-tail] t=" << (long)clock() << "ms fade-START src="
<< source << " dur=" << duration << "s gain0=" << fade.gain0
<< "\n" << std::flush;
return;
}
}
@@ -315,6 +336,15 @@ void PatchLevelOfDetail::PlayNote(SourceSet sourceSet, int note)
{
alSourcePlay(sourceSet.sources[i]);
{ static int s_p=0; if (getenv("BT_AUDIO_LOG") && s_p++<30) DEBUG_STREAM << "[audio] PlayNote alSourcePlay src=" << sourceSet.sources[i] << " note=" << note << "\n" << std::flush; }
if (AudTailLog())
{
SAMPLEINFO zinfo = PRESET_getSampleInfo(bankID, patchID, i);
DEBUG_STREAM << "[aud-tail] t=" << (long)clock() << "ms PLAY src="
<< sourceSet.sources[i] << " bank=" << (int)bankID << " patch="
<< (int)patchID << " file=" << (zinfo.file ? zinfo.file : "?")
<< " loop=" << (int)(zinfo.loop == LoopAtWill)
<< " rel=" << zinfo.releaseSec << "s\n" << std::flush;
}
}
ALenum error = alGetError();
@@ -340,6 +370,12 @@ void PatchLevelOfDetail::StopNote(SourceSet sourceSet)
SAMPLEINFO info = PRESET_getSampleInfo(bankID,patchID,i);
ALenum state;
alGetSourcei(sourceSet.sources[i], AL_SOURCE_STATE, &state);
if (AudTailLog() && state == AL_PLAYING)
DEBUG_STREAM << "[aud-tail] t=" << (long)clock() << "ms STOP src="
<< sourceSet.sources[i] << " bank=" << (int)bankID << " patch="
<< (int)patchID << " file=" << (info.file ? info.file : "?")
<< " rel=" << info.releaseSec << "s -> "
<< ((info.releaseSec > 0.05f) ? "fade" : "cut") << "\n" << std::flush;
if (state == AL_PLAYING && info.releaseSec > 0.05f)
{
StartReleaseFade(sourceSet.sources[i], info.releaseSec);
+65
View File
@@ -133,6 +133,31 @@ static float gBTAimVpW = 0.0f; // D3D viewport (backbuffer) size --
static float gBTAimVpH = 0.0f; // the dpl2d frame's pixel space
static int gBTAimCamValid = 0;
// issue #16 (boresight parallax) diagnostics: the RENDER camera pose -- the
// frame the crosshair is actually drawn in (screen centre of the ACTIVE view).
// Published unconditionally by the active eye each frame; BT_PARALLAX_LOG
// projects the PICK point back into this view to measure how far the shot
// lands from the crosshair. A healthy boresight keeps the pick point pinned
// at reticle (0,0) in cockpit view from max range all the way in.
static float gBTRenderCamPos[3] = {0, 0, 0};
static float gBTRenderCamX[3] = {1, 0, 0}; // camera right
static float gBTRenderCamY[3] = {0, 1, 0}; // camera up
static float gBTRenderCamZ[3] = {0, 0, 1}; // camera BACK (view dir = -Z)
static int gBTRenderCamValid = 0;
void BTSetRenderCamera(const float pos[3], const float xax[3],
const float yax[3], const float zax[3])
{
for (int i = 0; i < 3; ++i)
{
gBTRenderCamPos[i] = pos[i];
gBTRenderCamX[i] = xax[i];
gBTRenderCamY[i] = yax[i];
gBTRenderCamZ[i] = zax[i];
}
gBTRenderCamValid = 1;
}
void BTSetAimCamera(const float pos[3], const float xax[3],
const float yax[3], const float zax[3])
{
@@ -263,6 +288,46 @@ int BTProjectToReticle(const float world[3], float *rx, float *ry)
return 1;
}
//
// issue #16 (boresight parallax) diag: world point -> reticle coords through
// the RENDER camera (the view the crosshair is drawn in), NOT the aim camera
// (projecting the pick point through the aim camera is trivially (0,0) --
// the pick point lies ON the aim ray). eyeOut receives the render eye
// position so the caller can log the true sight-line range. Same reticle
// frame as BTProjectToReticle (+y down; unit = half the viewport height).
//
int BTProjectToRenderView(const float world[3], float *rx, float *ry,
float eyeOut[3])
{
if (!gBTRenderCamValid || gBTAimP11 <= 0.0f || gBTAimP22 <= 0.0f
|| gBTAimVpH <= 0.0f)
{
*rx = 0.0f; *ry = 0.0f;
return 0;
}
if (eyeOut != 0)
for (int i = 0; i < 3; ++i)
eyeOut[i] = gBTRenderCamPos[i];
float rel[3];
for (int i = 0; i < 3; ++i)
rel[i] = world[i] - gBTRenderCamPos[i];
const float xc = rel[0]*gBTRenderCamX[0] + rel[1]*gBTRenderCamX[1] + rel[2]*gBTRenderCamX[2];
const float yc = rel[0]*gBTRenderCamY[0] + rel[1]*gBTRenderCamY[1] + rel[2]*gBTRenderCamY[2];
const float zc = rel[0]*gBTRenderCamZ[0] + rel[1]*gBTRenderCamZ[1] + rel[2]*gBTRenderCamZ[2];
const float depth = -zc; // camera looks down -Z
if (depth < 0.1f)
{
*rx = (xc >= 0.0f) ? 2.0f : -2.0f;
*ry = 0.0f;
return 0;
}
const float ndcX = (xc * gBTAimP11) / depth;
const float ndcY = (yc * gBTAimP22) / depth;
*rx = ndcX * BTGetPresentAspect();
*ry = -ndcY;
return 1;
}
//
// The target HOTBOX projection (the recovered reticle Execute @4cdff7-4ce0f9
// [T1]): project the box hugging the target's extents -- x +-4 around the
+87 -12
View File
@@ -16,6 +16,12 @@ float gBTEyeTwist = 0.0f;
// pitch from the pick ray -- this is the pilot's DELIBERATE pitch, re-applied
// after the leveling so aimed shots follow the R/F (stick-Y) elevation.
float gBTEyeElev = 0.0f;
// issue #16 (boresight parallax): non-zero while a BORESIGHT eye (the
// viewpoint mech's cockpit siteeyepoint, marked by SetBoresightEye) is alive
// and publishing the aim camera. While set, the active-camera fallback
// publish is suppressed -- the cockpit eyepoint owns the pick/sight ray in
// BOTH views, so toggling V cannot change ballistics.
int gBTBoresightEyeLive = 0;
#pragma hdrstop
#include "l4vidrnd.h"
@@ -5492,6 +5498,7 @@ DPLEyeRenderable::DPLEyeRenderable(
myEntity = This_Entity;
// myParentDCS = Parent_DCS;
myOrientationMatrix = Offset_Matrix;
mIsBoresightEye = false; // issue #16: set via SetBoresightEye
myEyepointRotation = eyepoint_rotation;
if(myEyepointRotation)
{
@@ -5610,6 +5617,30 @@ DPLEyeRenderable::~DPLEyeRenderable()
//// but Phil is making a patch to allow us to check that.
//// dpl_RemoveDCSFromDCS(myParentDCS, myDCS);
// dpl_DeleteDCS(myDCS);
// issue #16: a deleted boresight eye releases the aim-camera publish so
// the active camera's fallback path takes over (spectator / teardown)
// instead of the aim ray going permanently stale.
if (mIsBoresightEye)
{
extern int gBTBoresightEyeLive;
gBTBoresightEyeLive = 0;
}
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// SetBoresightEye (issue #16 boresight parallax): mark this eye as the
// VIEWPOINT mech's cockpit eyepoint -- the owner of the aim-camera (pick/
// sight ray) publish in BOTH views. Tracks the process-wide latch that
// gates the active-camera fallback publish (a viewpoint without a cockpit
// eyepoint, e.g. a spectator camera).
//
void
DPLEyeRenderable::SetBoresightEye(bool is_boresight)
{
extern int gBTBoresightEyeLive;
mIsBoresightEye = is_boresight;
if (is_boresight)
gBTBoresightEyeLive = 1;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// TestInstance for the DPLEyeRenderable
@@ -5805,13 +5836,42 @@ void
{
myDevice->SetTransform(D3DTS_VIEW, &view);
// issue #16 diag: publish the RENDER camera pose (the frame the
// crosshair is drawn in) for BT_PARALLAX_LOG's pick-point
// back-projection (BTProjectToRenderView). eyeW rows are
// right/up/BACK + translation (view = inverse(eyeW)).
{
extern void BTSetRenderCamera(const float pos[3], const float xax[3],
const float yax[3], const float zax[3]);
const float rp[3] = { eyeW._41, eyeW._42, eyeW._43 };
const float rxx[3] = { eyeW._11, eyeW._12, eyeW._13 };
const float ryy[3] = { eyeW._21, eyeW._22, eyeW._23 };
const float rzz[3] = { eyeW._31, eyeW._32, eyeW._33 };
BTSetRenderCamera(rp, rxx, ryy, rzz);
}
if (dbgEyeExec < 8)
DEBUG_STREAM << "[EYE] ACTIVE eye " << (void *)this
<< " (mCamera match) pos.y=" << pos.y << "\n" << std::flush;
}
// aim-ray feed (task #36): publish this eye's world pose --
// the LookAtRH basis (zaxis = back, view direction = -zaxis)
// -- for the reticle pick ray / designator projection.
// aim-ray feed (task #36): publish this eye's world pose --
// the LookAtRH basis (zaxis = back, view direction = -zaxis)
// -- for the reticle pick ray / designator projection.
//
// OWNERSHIP (issue #16, boresight parallax): the publish is owned by
// the BORESIGHT eye -- the viewpoint mech's cockpit siteeyepoint --
// in BOTH views (it executes in the mech's draw traversal every
// frame, active camera or not), so the chase view's ballistics are
// IDENTICAL to the cockpit's and the V toggle cannot change where a
// shot lands. Only when no boresight eye is alive (spectator /
// teardown) does the ACTIVE camera publish, with the legacy entity
// anchor as the origin.
{
extern int gBTBoresightEyeLive;
const bool aimOwner = mIsBoresightEye
|| (!gBTBoresightEyeLive && myRenderer->mCamera == this);
if (aimOwner)
{
extern void BTSetAimCamera(const float pos[3], const float xax[3],
const float yax[3], const float zax[3]);
@@ -5865,16 +5925,31 @@ void
D3DXVec3Normalize(&xax, &xax);
D3DXVec3Cross(&yax, &zax, &xax);
}
// ORIGIN: the guns fire from the MECH's torso, not the render
// eye. In CHASE view the eye sits behind+above the mech, so a
// ray from `pos` would start over the shoulder (and, being level,
// fly above a ground target). Anchor the boresight at the mech's
// XZ + a torso/gun height so the pick works in BOTH views (chase
// is the default; cockpit toggles with V). In cockpit view this
// is ~the eyepoint anyway.
float p[3] = { pos.x, pos.y, pos.z };
if (myEntity != 0)
// ORIGIN (issue #16, boresight parallax): the pick/sight ray
// starts at the TRUE EYEPOINT -- this eye's own world
// translation (eyeW row 4, the same origin the crosshair's
// view projects from). The binary's sight math runs in the
// eyepoint-segment frame: HudSimulation computes the
// designator HotBoxVector by transforming the target position
// with inverse(eyeSegToWorld @00424da8) o inverse(
// R(EyepointRotation)) (@004b7830, part_013.c:5688-5702), the
// VIEW is inverse(the live jointtorso->jointeye->siteeyepoint
// chain) (FUN_004c22c4 @part_013.c:11742), and the fired beam
// CONVERGES from the muzzle segment to the picked point
// (@004bace8:7742-7752) -- so pick ray and sight line share
// the eye origin. The old `mech.y + 5.0` anchor (a port
// improvisation) sat BELOW the sight line: a constant
// vertical offset whose angular error grew as range closed --
// shots landing LOW under the target up close (Gitea #16).
// Measured pre-fix: the pick point slid down the reticle
// ry=+0.07 @ 50u -> +0.32 @ 10u -> +1.5 @ 2.7u.
float p[3] = { eyeW._41, eyeW._42, eyeW._43 };
if (!mIsBoresightEye && myEntity != 0)
{
// no cockpit eyepoint alive (spectator/camera director):
// legacy entity anchor -- this camera sits behind/above,
// so a ray from its own origin would start over the
// shoulder.
p[0] = myEntity->localToWorld(3,0);
p[1] = myEntity->localToWorld(3,1) + 5.0f; // feet -> torso/gun height
p[2] = myEntity->localToWorld(3,2);
+10
View File
@@ -1716,7 +1716,17 @@ public:
virtual void
Execute();
// issue #16 (boresight parallax): mark the VIEWPOINT mech's cockpit
// eyepoint (siteeyepoint) as the BORESIGHT eye -- it publishes the aim
// camera (pick/sight ray) every frame in BOTH views, so the V toggle
// cannot change ballistics. Defined in L4VIDRND.cpp (tracks the
// gBTBoresightEyeLive fallback latch).
void
SetBoresightEye(bool is_boresight);
protected:
bool mIsBoresightEye; // issue #16: this eye owns the aim-camera publish
dpl_DCS
*myDCS,
*myParentDCS;
+6
View File
@@ -466,6 +466,12 @@ HierarchicalDrawComponent*
//
mEyeCockpit = new DPLEyeRenderable(
entity, offset_matrix, parent_DCS, eye_rot);
// issue #16 (boresight parallax): the VIEWPOINT mech's cockpit
// eyepoint is the BORESIGHT eye -- it owns the aim-camera
// (pick/sight ray) publish in BOTH views, from its own eye
// origin, so chase and cockpit ballistics are identical (see
// DPLEyeRenderable::Execute).
mEyeCockpit->SetBoresightEye(true);
if (type == insideEntity) // true inside build: it IS the camera
mCamera = mEyeCockpit;
dbg_eye = 1;
+69
View File
@@ -329,3 +329,72 @@ bool
}
return false;
}
//---------------------------------------------------------------------------//
// BTGroundRayHitExact -- BT_RANGE_LOG diagnostic (Gitea #4 verdict
// instrumentation, uncommitted): ANALYTIC closest-hit ray intersect
// (Moller-Trumbore, no backface cull, no step size, no height band) over the
// SAME visual triangle set the march samples -- including vertical walls and
// ceilings, which SampleBand's XZ point-in-triangle test cannot see. The
// march-vs-exact range difference on the same ray is direct evidence for (or
// against) the "ray falls through geometry" hypothesis; it is NOT used by any
// gameplay path.
//---------------------------------------------------------------------------//
bool
BTGroundRayHitExact(float ox, float oy, float oz,
float dx, float dy, float dz,
float maxDist, float *hx, float *hy, float *hz)
{
if (gState == 0)
gState = BuildTables() ? 1 : -1;
if (gState != 1)
return false;
float bestT = maxDist;
bool found = false;
for (size_t ii = 0; ii < gInsts.size(); ++ii)
{
const VgInst &inst = gInsts[ii];
// local-space ray (instances are translation-placed only)
const float lox = ox - inst.px, loy = oy - inst.py, loz = oz - inst.pz;
const VgMesh &m = gMeshes[inst.mesh];
for (size_t ti = 0; ti < m.tris.size(); ++ti)
{
const VgTri &t = m.tris[ti];
const float e1x = t.bx - t.ax, e1y = t.by - t.ay, e1z = t.bz - t.az;
const float e2x = t.cx - t.ax, e2y = t.cy - t.ay, e2z = t.cz - t.az;
// p = dir x e2
const float px = dy * e2z - dz * e2y;
const float py = dz * e2x - dx * e2z;
const float pz = dx * e2y - dy * e2x;
const float det = e1x * px + e1y * py + e1z * pz;
if (det > -1e-9f && det < 1e-9f)
continue; // parallel
const float inv = 1.0f / det;
const float tx = lox - t.ax, ty = loy - t.ay, tz = loz - t.az;
const float u = (tx * px + ty * py + tz * pz) * inv;
if (u < 0.0f || u > 1.0f)
continue;
// q = tvec x e1
const float qx = ty * e1z - tz * e1y;
const float qy = tz * e1x - tx * e1z;
const float qz = tx * e1y - ty * e1x;
const float v = (dx * qx + dy * qy + dz * qz) * inv;
if (v < 0.0f || u + v > 1.0f)
continue;
const float tt = (e2x * qx + e2y * qy + e2z * qz) * inv;
if (tt > 0.05f && tt < bestT)
{
bestT = tt;
found = true;
}
}
}
if (found)
{
*hx = ox + dx * bestT;
*hy = oy + dy * bestT;
*hz = oz + dz * bestT;
}
return found;
}
+151 -2
View File
@@ -156,6 +156,7 @@
#include <mechweap.hpp> // MechWeapon::GetExplosionResourceID (per-round detonation)
#include <mislanch.hpp> // MissileLauncher::ClassDerivations (splash-radius gate)
#include "btinput.hpp" // the CONTROLS.MAP + XInput binding engine
#include <string.h> // [aud-tail] strchr -- BT_FIRE_PULSE "on,off" parse (Gitea #5, instrumentation only)
#if !defined(PLAYER_HPP)
# include <player.hpp> // Player::VehicleDeadMessage -- the death->respawn notification (task #52)
#endif
@@ -2932,6 +2933,34 @@ void
sGotoDrive = (gv && *gv) ? 1 : 0;
}
gBTDrive.fireForced = sAutoFire;
// [aud-tail] (Gitea #5 verdict harness, additive): BT_FIRE_PULSE="on,off"
// (frames) pulses the forced trigger instead of holding it, so a SINGLE
// energy shot is followed by trigger silence -- the reported scenario
// (beam ends, buzz tail runs out alone). Overrides BT_AUTOFIRE's hold.
{
static int s_fpOn = -1, s_fpOff = 0, s_fpCount = 0;
if (s_fpOn < 0)
{
const char *fp = getenv("BT_FIRE_PULSE");
if (fp && *fp)
{
s_fpOn = atoi(fp);
if (s_fpOn <= 0) s_fpOn = 6;
const char *comma = strchr(fp, ',');
s_fpOff = comma ? atoi(comma + 1) : 600;
if (s_fpOff <= 0) s_fpOff = 600;
}
else
{
s_fpOn = 0;
}
}
if (s_fpOn > 0)
{
gBTDrive.fireForced =
((s_fpCount++ % (s_fpOn + s_fpOff)) < s_fpOn) ? 1 : 0;
}
}
if (sAutoDrive > 0.0f)
{
gBTDrive.forced = 1;
@@ -3324,7 +3353,18 @@ void
}
// STOP radius: "enemy" holds at weapon range so it can shoot
// instead of ramming; a fixed point drives right up to the spot.
const float stopDist = (s_goto == 2) ? 300.0f : 5.0f;
// BT_GOTO_STOP overrides (issue #16 parallax test: approach the
// dummy from range all the way in to 3/4-screen).
float stopDist = (s_goto == 2) ? 300.0f : 5.0f;
{
static float s_stopOv = -1.0f;
if (s_stopOv < 0.0f)
{
const char *so = getenv("BT_GOTO_STOP");
s_stopOv = (so && *so) ? (float)atof(so) : 0.0f;
}
if (s_stopOv > 0.0f) stopDist = s_stopOv;
}
const float stopDist2 = stopDist * stopDist;
const int arrived = (dist2 < stopDist2);
// publish for the mapper bridge (the real-controls path reads
@@ -3353,7 +3393,20 @@ void
// only from BT_AUTODRIVE). Drive forward toward the target and cut
// the throttle inside the stop radius so it holds at firing range.
gBTDrive.forced = 1;
gBTDrive.forcedThrottle = arrived ? 0.0f : 0.8f;
// BT_GOTO_THR: approach-speed override (issue #16 parallax test --
// a walking approach spans the render-live window so the curve can
// be sampled from far to near).
float gthr = 0.8f;
{
static float s_thrOv = -1.0f;
if (s_thrOv < 0.0f)
{
const char *to = getenv("BT_GOTO_THR");
s_thrOv = (to && *to) ? (float)atof(to) : 0.0f;
}
if (s_thrOv > 0.0f) gthr = s_thrOv;
}
gBTDrive.forcedThrottle = arrived ? 0.0f : gthr;
turn = gBTGotoTurn; // non-real-controls fallback path
}
}
@@ -4581,6 +4634,16 @@ void
Point3D hotPoint; // picked world point on its hull
Entity *pickTarget = 0; // what the boresight ray hit (mech/terrain)
Point3D pickPoint; // where it hit
// BT_RANGE_LOG (Gitea #4 VERDICT instrumentation -- uncommitted diag):
// capture the boresight ray + which pick tier won this frame, so the
// HUD-feed block below can trace the range chain per frame.
// rlPickClass: 0=no ray/no pick 1=mech 2=structure (static solid
// tree) 3=ground march (visual-terrain step march) 4=max-range
// default designation (nothing pickable within 1200)
int rlPickClass = 0;
int rlHaveRay = 0;
float rlRayS[3] = {0,0,0}, rlRayD[3] = {0,0,0};
float rlStructD = -1.0f; // sqrt range of the structure tier hit
static int gAimNoRay = 0, gAimGround = 0, gAimHits = 0; // 1Hz diagnostics
{
extern int BTGetAimRay(float rx, float ry, float outStart[3], float outDir[3]);
@@ -4731,6 +4794,9 @@ void
{
Point3D rayStart(rs[0], rs[1], rs[2]);
Vector3D rayDir(rd[0], rd[1], rd[2]);
rlHaveRay = 1; // BT_RANGE_LOG capture (issue #4)
rlRayS[0] = rs[0]; rlRayS[1] = rs[1]; rlRayS[2] = rs[2];
rlRayD[0] = rd[0]; rlRayD[1] = rd[1]; rlRayD[2] = rd[2];
// MP TARGETING (task #46): walk EVERY other living mech (the solo
// dummy AND every peer replicant, from the live-mech registry) and
// pick the CLOSEST one the boresight ray strikes -- generalising
@@ -4776,6 +4842,7 @@ void
structDist = dx*dx + dy*dy + dz*dz;
structPoint = sp;
haveStruct = true;
rlStructD = (float)Sqrt(structDist); // BT_RANGE_LOG (issue #4)
static float s_wp = 0.0f; s_wp += dt;
if (getenv("BT_GROUND_LOG") && s_wp >= 1.0f)
{
@@ -4795,6 +4862,7 @@ void
{
pickTarget = hotTarget; // mech is the closest hit
pickPoint = hotPoint;
rlPickClass = 1; // BT_RANGE_LOG (issue #4)
++gAimHits;
}
else if (haveStruct)
@@ -4815,6 +4883,7 @@ void
// HudSim part_013.c:5620).
pickTarget = (structOwner != 0) ? structOwner : gBTTerrainEntity;
pickPoint = structPoint;
rlPickClass = 2; // BT_RANGE_LOG (issue #4)
++gAimGround;
}
else
@@ -4831,6 +4900,7 @@ void
{
pickTarget = gBTTerrainEntity;
pickPoint.x = hx; pickPoint.y = hy; pickPoint.z = hz;
rlPickClass = 3; // BT_RANGE_LOG (issue #4)
++gAimGround;
}
}
@@ -4871,12 +4941,44 @@ void
MECH_TARGET_POS(this) = farPt;
targetReticle.targetEntity = gBTTerrainEntity;
targetReticle.rayIntersection = farPt;
rlPickClass = 4; // BT_RANGE_LOG: max-range default (issue #4)
}
else
{
MECH_TARGET_ENTITY(this) = 0; // no world sentinel -> no target
MECH_TARGET_SUBIDX(this) = -1;
}
// [parallax] diag (issue #16): project the pick point back into the
// RENDER view -- the frame the crosshair is drawn in. Reticle +y is
// down, so ry > 0 = the pick lands BELOW the crosshair. A healthy
// boresight holds (rx,ry) ~ (0,0) from max range all the way in; a
// pick ray anchored below the sight line shows ry sliding DOWN
// (positive) as range closes.
if (getenv("BT_PARALLAX_LOG") && pickTarget != 0)
{
static float s_pl = 0.0f; s_pl += dt;
if (s_pl >= 0.5f)
{
s_pl = 0.0f;
extern int BTProjectToRenderView(const float world[3],
float *rx, float *ry, float eyeOut[3]);
float w[3] = { (float)pickPoint.x, (float)pickPoint.y,
(float)pickPoint.z };
float prx = 0.0f, pry = 0.0f, eo[3] = {0,0,0};
if (BTProjectToRenderView(w, &prx, &pry, eo))
{
float pdx = w[0]-eo[0], pdy = w[1]-eo[1], pdz = w[2]-eo[2];
float rng = (float)Sqrt(pdx*pdx + pdy*pdy + pdz*pdz);
DEBUG_STREAM << "[parallax] range=" << rng
<< " rx=" << prx << " ry=" << pry
<< " mech=" << ((hotTarget != 0 && pickTarget == hotTarget) ? 1 : 0)
<< " pick=(" << w[0] << "," << w[1] << "," << w[2] << ")"
<< " eye=(" << eo[0] << "," << eo[1] << "," << eo[2] << ")"
<< "\n" << std::flush;
}
}
}
}
// DETERMINISTIC FIRE-AT-STRUCTURE TEST (BT_FIRE_AT_STRUCT, task #50 verify):
@@ -5052,6 +5154,53 @@ void
BTSetHudTargetRange((Scalar)sShownRange);
}
// BT_RANGE_LOG (Gitea #4 VERDICT instrumentation -- uncommitted diag):
// per-frame trace of the whole range chain. Alongside the pick the
// game actually used, re-cast the SAME ray through (a) the 4m-step
// visual-terrain march (BTGroundRayHit) and (b) an ANALYTIC
// Moller-Trumbore intersect over the same visual triangle set incl.
// walls/ceilings (BTGroundRayHitExact) -- march-vs-exact divergence is
// direct fall-through evidence; exact-vs-structure divergence flags
// visible geometry missing from the static collision tree.
if (getenv("BT_RANGE_LOG"))
{
static float s_rlT = 0.0f; s_rlT += (float)dt;
extern bool BTGroundRayHit(float,float,float, float,float,float,
float, float*,float*,float*);
extern bool BTGroundRayHitExact(float,float,float, float,float,float,
float, float*,float*,float*);
float mhx=0, mhy=0, mhz=0, ehx=0, ehy=0, ehz=0;
float marchD = -1.0f, exactD = -1.0f;
if (rlHaveRay)
{
if (BTGroundRayHit(rlRayS[0], rlRayS[1], rlRayS[2],
rlRayD[0], rlRayD[1], rlRayD[2], 1200.0f, &mhx, &mhy, &mhz))
{
float ddx = mhx-rlRayS[0], ddy = mhy-rlRayS[1], ddz = mhz-rlRayS[2];
marchD = sqrtf(ddx*ddx + ddy*ddy + ddz*ddz);
}
if (BTGroundRayHitExact(rlRayS[0], rlRayS[1], rlRayS[2],
rlRayD[0], rlRayD[1], rlRayD[2], 1200.0f, &ehx, &ehy, &ehz))
{
float ddx = ehx-rlRayS[0], ddy = ehy-rlRayS[1], ddz = ehz-rlRayS[2];
exactD = sqrtf(ddx*ddx + ddy*ddy + ddz*ddz);
}
}
Point3D rp = MECH_TARGET_POS(this);
DEBUG_STREAM << "[range] t=" << s_rlT
<< " true=" << trueRange << " shown=" << sShownRange
<< " cls=" << rlPickClass << " ray=" << rlHaveRay
<< " structD=" << rlStructD
<< " marchD=" << marchD << " exactD=" << exactD
<< " pick=(" << rp.x << "," << rp.y << "," << rp.z << ")"
<< " eye=(" << rlRayS[0] << "," << rlRayS[1] << "," << rlRayS[2] << ")"
<< " dir=(" << rlRayD[0] << "," << rlRayD[1] << "," << rlRayD[2] << ")"
<< " exact=(" << ehx << "," << ehy << "," << ehz << ")"
<< " pos=(" << localOrigin.linearPosition.x << ","
<< localOrigin.linearPosition.y << ","
<< localOrigin.linearPosition.z << ")\n" << std::flush;
}
gBTHudHeading = gDriveHeading; // CompassHeading (attr 0xD)
gBTHudTwist = (float)TorsoHeading(); // RotationOfTorsoHorizontal (attr 4)
gBTHudTwistLimit = (float)GetHorizontalFiringReach();// HorizontalTorsoLimit (attrs 5/6)