diff --git a/content/CAVERN.EGG b/content/CAVERN.EGG new file mode 100644 index 0000000..37fb05b --- /dev/null +++ b/content/CAVERN.EGG @@ -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 diff --git a/context/combat-damage.md b/context/combat-damage.md index e7d5a2c..09b1b21 100644 --- a/context/combat-damage.md +++ b/context/combat-damage.md @@ -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]:** diff --git a/context/decomp-reference.md b/context/decomp-reference.md index f03f039..cd373e0 100644 --- a/context/decomp-reference.md +++ b/context/decomp-reference.md @@ -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=` | 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=` / `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 | diff --git a/context/gauges-hud.md b/context/gauges-hud.md index d5d4892..ea3f3ca 100644 --- a/context/gauges-hud.md +++ b/context/gauges-hud.md @@ -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 diff --git a/context/multiplayer.md b/context/multiplayer.md index c147744..76cef36 100644 --- a/context/multiplayer.md +++ b/context/multiplayer.md @@ -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 diff --git a/context/wintesla-port.md b/context/wintesla-port.md index 57e99b2..63af428 100644 --- a/context/wintesla-port.md +++ b/context/wintesla-port.md @@ -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 1–2 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 203–207 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.311–1.326 s vs + authored 1.309 s (≤ 1 frame over); laser recharge 2.3–3.8 s, PPC 8.8–16.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 diff --git a/engine/MUNGA_L4/L4AUDLVL.cpp b/engine/MUNGA_L4/L4AUDLVL.cpp index 73285e1..337e705 100644 --- a/engine/MUNGA_L4/L4AUDLVL.cpp +++ b/engine/MUNGA_L4/L4AUDLVL.cpp @@ -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 +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); diff --git a/engine/MUNGA_L4/L4VIDEO.cpp b/engine/MUNGA_L4/L4VIDEO.cpp index d2c6328..1ff5150 100644 --- a/engine/MUNGA_L4/L4VIDEO.cpp +++ b/engine/MUNGA_L4/L4VIDEO.cpp @@ -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 diff --git a/engine/MUNGA_L4/L4VIDRND.cpp b/engine/MUNGA_L4/L4VIDRND.cpp index e3ba10e..e7c8f57 100644 --- a/engine/MUNGA_L4/L4VIDRND.cpp +++ b/engine/MUNGA_L4/L4VIDRND.cpp @@ -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); diff --git a/engine/MUNGA_L4/L4VIDRND.h b/engine/MUNGA_L4/L4VIDRND.h index 1805ed9..ba14087 100644 --- a/engine/MUNGA_L4/L4VIDRND.h +++ b/engine/MUNGA_L4/L4VIDRND.h @@ -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; diff --git a/game/reconstructed/btl4vid.cpp b/game/reconstructed/btl4vid.cpp index dddd21c..577f3c1 100644 --- a/game/reconstructed/btl4vid.cpp +++ b/game/reconstructed/btl4vid.cpp @@ -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; diff --git a/game/reconstructed/btvisgnd.cpp b/game/reconstructed/btvisgnd.cpp index df527df..18394b1 100644 --- a/game/reconstructed/btvisgnd.cpp +++ b/game/reconstructed/btvisgnd.cpp @@ -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; +} diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index f74aaaf..fd30a7b 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -156,6 +156,7 @@ #include // MechWeapon::GetExplosionResourceID (per-round detonation) #include // MissileLauncher::ClassDerivations (splash-radius gate) #include "btinput.hpp" // the CONTROLS.MAP + XInput binding engine +#include // [aud-tail] strchr -- BT_FIRE_PULSE "on,off" parse (Gitea #5, instrumentation only) #if !defined(PLAYER_HPP) # include // 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)