Compare commits
105
Commits
+12
@@ -49,3 +49,15 @@ operator_secret.txt
|
|||||||
scratchpad/night*/*.log
|
scratchpad/night*/*.log
|
||||||
scratchpad/night*/*.rar
|
scratchpad/night*/*.rar
|
||||||
scratchpad/night*/*.zip
|
scratchpad/night*/*.zip
|
||||||
|
# tester-authored notes / lastrun dumps carry player handles + machine names
|
||||||
|
# (deliberate exceptions: git add -f)
|
||||||
|
scratchpad/night*/*notes*.txt
|
||||||
|
scratchpad/night*/lastrun*.txt
|
||||||
|
|
||||||
|
# Bench artifacts in content/ (screenshots, session logs, generated eggs, deployed exe)
|
||||||
|
# -- receipts stay on disk, out of `git status`. A new AUTHORED egg: git add -f.
|
||||||
|
content/*.png
|
||||||
|
content/*.log
|
||||||
|
content/matchlog_*.txt
|
||||||
|
content/btl4.exe
|
||||||
|
content/*.EGG
|
||||||
|
|||||||
@@ -10,9 +10,12 @@
|
|||||||
`BTL4OPT.EXE` binary on top of the working WinTesla engine.
|
`BTL4OPT.EXE` binary on top of the working WinTesla engine.
|
||||||
**Repo of record:** the top-level `CMakeLists.txt` + `README.md` build `btl4.exe`. Layout:
|
**Repo of record:** the top-level `CMakeLists.txt` + `README.md` build `btl4.exe`. Layout:
|
||||||
`engine/ game/ content/ docs/ reference/ tools/ context/`.
|
`engine/ game/ content/ docs/ reference/ tools/ context/`.
|
||||||
**Current front:** `btl4.exe` runs a full single-player loop; the gauge system is complete; the
|
**Current front (2026-08-06):** core gameplay reconstruction is COMPLETE — SP+MP loop, combat/
|
||||||
active work is reconstructing each subsystem's authentic behavior from the binary. Details +
|
damage/scoring, locomotion (gait/gimp/CROUCH), night kit (searchlight), death/respawn, replication
|
||||||
what's-next: `context/project-overview.md`, `context/open-questions.md`, recent git log.
|
all authentic + benched (builds 4.11.774→801). The decomp export was REBUILT 2026-08-06 (#60
|
||||||
|
closed: coverage 93.5%, dark code 41 KB — cite `@ADDR`, NOT `part_0NN.c:LINE`; the old export is
|
||||||
|
archived). Active work: the POLISH list. Details + what's-next: `context/project-overview.md`,
|
||||||
|
`context/open-questions.md`, recent git log.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -59,6 +62,7 @@ precise than anything you can infer.
|
|||||||
|---|---|
|
|---|---|
|
||||||
| What the project is, engine, platform, goal | `context/project-overview.md` |
|
| What the project is, engine, platform, goal | `context/project-overview.md` |
|
||||||
| Missing BT source, decompilation strategy | `context/source-completeness.md` |
|
| Missing BT source, decompilation strategy | `context/source-completeness.md` |
|
||||||
|
| **Export DARK REGIONS / "is X in the decomp?"** — the gap census | `reference/decomp/GAP_CENSUS.md` (tools: `tools/gapcensus.py`, `tools/ghidra_reexport.sh`, `tools/gapdiff.py`; log: `phases/phase-04-gap-census.md`) |
|
||||||
| The 4.10 literal-source manifest (per-TU function/size/address map) | `reference/BT410_SOURCE_MANIFEST.md` (tool: `tools/manifest410.py`; log: `phases/phase-03-bt410-source-manifest.md`) |
|
| The 4.10 literal-source manifest (per-TU function/size/address map) | `reference/BT410_SOURCE_MANIFEST.md` (tool: `tools/manifest410.py`; log: `phases/phase-03-bt410-source-manifest.md`) |
|
||||||
| The WinTesla Windows port (renderer/audio/HAL) | `context/wintesla-port.md` |
|
| The WinTesla Windows port (renderer/audio/HAL) | `context/wintesla-port.md` |
|
||||||
| Build / run / debug / repo layout / env gates | `context/build-and-run.md` + `context/decomp-reference.md` §6 |
|
| Build / run / debug / repo layout / env gates | `context/build-and-run.md` + `context/decomp-reference.md` §6 |
|
||||||
@@ -150,7 +154,9 @@ bt411/
|
|||||||
│ └── <topic>.md # one concept each
|
│ └── <topic>.md # one concept each
|
||||||
├── reference/
|
├── reference/
|
||||||
│ ├── glossary.yaml # terms/acronyms
|
│ ├── glossary.yaml # terms/acronyms
|
||||||
│ ├── decomp/ # raw Ghidra pseudocode (the source-of-truth)
|
│ ├── decomp/ # raw Ghidra pseudocode (the source-of-truth; REBUILT 2026-08-06,
|
||||||
|
│ │ # 93.5% coverage. archive_2025export/ = the old export, kept
|
||||||
|
│ │ # ONLY so old `part_0NN.c:LINE` citations resolve -- cite @ADDR)
|
||||||
│ └── ghidra_scripts/ # the exporters
|
│ └── ghidra_scripts/ # the exporters
|
||||||
├── docs/ # PROGRESS_LOG.md (the full old CLAUDE.md) + detailed running ledgers
|
├── docs/ # PROGRESS_LOG.md (the full old CLAUDE.md) + detailed running ledgers
|
||||||
├── phases/ # restructuring / investigation logs
|
├── phases/ # restructuring / investigation logs
|
||||||
|
|||||||
@@ -164,3 +164,31 @@ pad X action LookBehind
|
|||||||
pad Start action ModeCycle
|
pad Start action ModeCycle
|
||||||
pad RightThumb action DisplayCycle
|
pad RightThumb action DisplayCycle
|
||||||
pad Back action Valve
|
pad Back action Valve
|
||||||
|
|
||||||
|
# COOLANT FLUSH -- HELD key dumps reservoir coolant into the loops
|
||||||
|
# (Reservoir InjectCoolant, msg 4). The tank drains while held; with the
|
||||||
|
# tank empty the loops burn coolant with no refill -- run hot long enough
|
||||||
|
# and the bank drops below 5%, which ARMS the eject (Panic lights).
|
||||||
|
key G action Flush
|
||||||
|
|
||||||
|
# PANIC / EJECT -- the punch-out (Mech msg 0x19). Only fires on a CRIPPLED
|
||||||
|
# mech (no generators, coolant leaked below 5%, or leg-gimped novice) -- a
|
||||||
|
# healthy mech refuses the button, so a stray press is a no-op.
|
||||||
|
key Back action Eject
|
||||||
|
pad LeftThumb action Eject
|
||||||
|
|
||||||
|
# PILOT KEYPAD (the pod's MFD program keypad, KeyboardPilot unit). While
|
||||||
|
# PANIC mode is armed, ANY pilot-keypad key fires the eject (the binary's
|
||||||
|
# keyboardGroup[KeyboardPilot].Add(0x200000, mech, 0x19) -- its ONLY armed
|
||||||
|
# eject binding); un-armed, the keys feed program entry. Desktop numpad =
|
||||||
|
# that keypad.
|
||||||
|
key NumPad0 keypad pilot 0
|
||||||
|
key NumPad1 keypad pilot 1
|
||||||
|
key NumPad2 keypad pilot 2
|
||||||
|
key NumPad3 keypad pilot 3
|
||||||
|
key NumPad4 keypad pilot 4
|
||||||
|
key NumPad5 keypad pilot 5
|
||||||
|
key NumPad6 keypad pilot 6
|
||||||
|
key NumPad7 keypad pilot 7
|
||||||
|
key NumPad8 keypad pilot 8
|
||||||
|
key NumPad9 keypad pilot 9
|
||||||
|
|||||||
@@ -182,6 +182,18 @@ default (`-DBT_STEAM=ON` is the documented dev-checkout state). `BT_EXPIRE=ON` (
|
|||||||
14-day tester window; an expire-OFF zip is renamed `-noexpire` and warns. Verify by extracting the
|
14-day tester window; an expire-OFF zip is renamed `-noexpire` and warns. Verify by extracting the
|
||||||
zip somewhere clean and booting it with no repo present — that is what catches a missing runtime DLL.
|
zip somewhere clean and booting it with no repo present — that is what catches a missing runtime DLL.
|
||||||
|
|
||||||
|
## Build ritual — the stale-link flake (bit 4+ times; MANDATORY for bench work)
|
||||||
|
MSBuild often does NOT relink `btl4.exe` when only static-lib members changed — the bench then
|
||||||
|
runs a STALE exe and the session burns hours on phantom results. Ritual for every code-change build:
|
||||||
|
1. `rm -f build/Release/btl4.exe` FIRST (force the relink);
|
||||||
|
2. build; a REAL relink prints the 20 known-benign `CreateStreamedSubsystem` LNK2019s (`/FORCE`
|
||||||
|
baseline — see the top of this file); an up-to-date run prints none;
|
||||||
|
3. STRING-VERIFY before benching: `python -c "d=open(r'build/Release/btl4.exe','rb').read();
|
||||||
|
print(b'<A-NEW-LOG-MARKER>' in d)"` — pick a string your change added (NB: release strings
|
||||||
|
carry LOG TEXT, not function names — check the .map for symbols);
|
||||||
|
4. use ABSOLUTE paths in every bench command — `cd` leaks between compound commands (the cwd
|
||||||
|
trap) and has broken builds/appends/copies repeatedly.
|
||||||
|
|
||||||
## Key Relationships
|
## Key Relationships
|
||||||
- Base: [[wintesla-port]] (the engine build recipe).
|
- Base: [[wintesla-port]] (the engine build recipe).
|
||||||
- Verify loop: [[reconstruction-method]]; env gates: [[decomp-reference]] §6.
|
- Verify loop: [[reconstruction-method]]; env gates: [[decomp-reference]] §6.
|
||||||
|
|||||||
+42
-2
@@ -17,8 +17,21 @@ Verified in-game on all 8 mechs (Thor vs gameplay footage). [T2]
|
|||||||
|
|
||||||
- One per mech (12: AVX BLX FIX JAX LOX MAX OWX RAX SNX STX THX VUX; model→skeleton table in
|
- One per mech (12: AVX BLX FIX JAX LOX MAX OWX RAX SNX STX THX VUX; model→skeleton table in
|
||||||
BTL4.RES @~3232850: `madcat=mad`, `blkhawk=blh`, … — the INSIDE skeleton is the X-variant, so
|
BTL4.RES @~3232850: `madcat=mad`, `blkhawk=blh`, … — the INSIDE skeleton is the X-variant, so
|
||||||
Madcat = `MAX_COP`). It is the torso segment's SkeletonType_A mesh; the inside view loads
|
Madcat = `MAX_COP`). It is the torso segment's SkeletonType_A mesh. [T2]
|
||||||
EXACTLY ONE segment mesh (the `_cop`) — 25 others hidden. [T2]
|
- **The inside view is a PER-MECH AUTHORED SET, not the cop alone** (#91 decode, 2026-08-04;
|
||||||
|
the earlier "EXACTLY ONE segment mesh" claim was madcat-derived and over-generalized —
|
||||||
|
corrected [T1, decomp-grounded: the binary's renderable build `FUN_004cef28`
|
||||||
|
(part_014.c:5077) sets skeleton type 4 (=A) for the inside build and its per-segment loop
|
||||||
|
calls `FUN_00424084(segment, type)` (:5570) for EVERY segment — NULL name = no mesh, no
|
||||||
|
torso-only filter, no cop special-case (only `siteeyepoint` diverts to the eye ctor
|
||||||
|
:5560-64); engine side: no type fallback in `EntitySegment::GetVideoObjectName`,
|
||||||
|
SEGMENT.cpp:97 — so the binary drew every authored type-A mesh, own-body pieces included;
|
||||||
|
T2: live `[view]` roster logs]). Authored type-A rosters: **madcat / vulture / bhk1 = cop only; sunder /
|
||||||
|
loki / avatar = cop + `<pfx>_tor` torso piece; thor = cop + `thx_tor` + `thx_msl` (the
|
||||||
|
shoulder LRM pod!); owens = cop + BOTH complete legs (`owx_{l,r}{ule,dle,bto,fto}`) +
|
||||||
|
`own_tshd`** — the Owens pilot saw their own legs walking; the Thor pilot saw their
|
||||||
|
missile pod edge bob at the lower frame. `BT_HIDE_INSIDE_SEG=<substr>` hides a non-cop
|
||||||
|
type-A mesh by name (diag, btl4vid.cpp ApplyViewSkeleton).
|
||||||
- Material: `<pfx>skin:blakskn_dz_{u,r,l}torso_mtl` — DIFFUSE/AMBIENT (0,0,0) + RAMP_REF
|
- Material: `<pfx>skin:blakskn_dz_{u,r,l}torso_mtl` — DIFFUSE/AMBIENT (0,0,0) + RAMP_REF
|
||||||
`softer`, **NO texture** (BLXSKIN.BMF is 238 bytes total; byte-verified). The 56 nonzero-UV
|
`softer`, **NO texture** (BLXSKIN.BMF is 238 bytes total; byte-verified). The 56 nonzero-UV
|
||||||
verts in BLX_COP sample nothing (planar-projection authoring leftover). [T1]
|
verts in BLX_COP sample nothing (planar-projection authoring leftover). [T1]
|
||||||
@@ -230,6 +243,33 @@ the shell green), `BT_COP_DUMP` (per-batch punch/bbox), `BT_EYE_FWD=<f>` (eye po
|
|||||||
- Feeds: [[gauges-hud]] (the HUD overlays this view) · [[subsystems]] (gyro, task #56)
|
- Feeds: [[gauges-hud]] (the HUD overlays this view) · [[subsystems]] (gyro, task #56)
|
||||||
- Gotchas: [[reconstruction-gotchas]] §14 (LookAt axis guess), §15 (per-patch edge counting)
|
- Gotchas: [[reconstruction-gotchas]] §14 (LookAt axis guess), §15 (per-patch edge counting)
|
||||||
|
|
||||||
|
## The #91 "black rectangle" (thor) — own-body inside meshes + the blakskn rule [T2]
|
||||||
|
|
||||||
|
Three testers (Oracle + RajelAran night 7; Ronin 2026-08-03: "moves diagonally 10→4 with the
|
||||||
|
footsteps, at rest sits ON the lower frame bar, right side, half-in") reported a black
|
||||||
|
rectangle in the Thor cockpit. Decoded 2026-08-04:
|
||||||
|
- The rectangle is the **thor's own `thx_msl` missile-pod mount plate** — authored, authentic
|
||||||
|
own-body geometry (see the roster above; "Summoner" = the `thr1` FE label, same THX canopy,
|
||||||
|
so the cross-reference isolated nothing). It rides the walk-animated shoulder while the eye
|
||||||
|
rides the gyro springs → the diagonal footstep-rhythm sway. Pixel-tracked across walk
|
||||||
|
captures: ~29px wide, x/y oscillating together along the 10↔4 axis, bottom center-right,
|
||||||
|
alternating with the gait [T2, scratchpad/night11/thorrect.sh].
|
||||||
|
- **What WAS wrong:** the pilot-facing surfaces of these own-body meshes are authored with the
|
||||||
|
same `<pfx>skin:blakskn_dz_*` interior-structure material as the canopy frame (texture-less,
|
||||||
|
black diffuse, `softer` ramp lo 0.25 → hi 0.99), but the unlit frame-constant treatment was
|
||||||
|
keyed on the `_cop` FILENAME — so the identical material rendered (0.13,0.12,0.15) on the
|
||||||
|
canopy and PURE (0,0,0) on the pod/legs (`[matlog]`-verified: owx_cop blakskn vcol=FF211F26
|
||||||
|
vs owx_lule blakskn vcol=FF000000). Fixed in bgfload.cpp: the constant now keys on
|
||||||
|
`meshIsCop || material contains "skin:blakskn_dz_"`. `mechfx:blakskn_mtl` (tshd shadow
|
||||||
|
quads) deliberately does NOT match. Verified: zero pure-black pixels in the lower view band
|
||||||
|
post-fix, tab renders in the frame tone and blends with the bar at rest; owens legs read as
|
||||||
|
coherent dark structure; canopy/terrain un-regressed. The textured pod/leg batches
|
||||||
|
(THX/GEN/OWEN.BSL pages — bright, mean texel 0.55-0.74) face outboard and were never the
|
||||||
|
visible surfaces.
|
||||||
|
- Diags from the dig: `BT_MAT_LOG=<mesh-stem substr>` (bgfload per-batch material routing:
|
||||||
|
resolved colour/texture/ramp/vcol), `BT_HIDE_INSIDE_SEG=<substr>`, `[view]` per-segment
|
||||||
|
inside-roster log. The `softer` ramp resolves lo=(0.25,0.25,0.25) hi=(0.99,0.99,0.99) [T1].
|
||||||
|
|
||||||
## Torso-elevation aim -> EYE PITCH (fixed 2026-07-19) [T2, pixel-calibrated]
|
## Torso-elevation aim -> EYE PITCH (fixed 2026-07-19) [T2, pixel-calibrated]
|
||||||
User+tester report "pitch does not work": the Torso sim integrated R/F (stick-Y)
|
User+tester report "pitch does not work": the Torso sim integrated R/F (stick-Y)
|
||||||
into `currentElevation` (authored limits/rates) but NOTHING consumed it — the
|
into `currentElevation` (authored limits/rates) but NOTHING consumed it — the
|
||||||
|
|||||||
+201
-9
@@ -102,6 +102,50 @@ the DCS = the struck SEGMENT → its `dzone` (the SKL segment→zone map, `GetSe
|
|||||||
`targetDamageZone`. The cylinder lottery (STEP 6) was only ever the UNAIMED path. The port's
|
`targetDamageZone`. The cylinder lottery (STEP 6) was only ever the UNAIMED path. The port's
|
||||||
whole-mech box pick + "STEP-6 zone under the boresight" funnels AIMED fire through the unaimed
|
whole-mech box pick + "STEP-6 zone under the boresight" funnels AIMED fire through the unaimed
|
||||||
lottery — which is exactly the night-6 report (#73): aim at the arm, get the spray.
|
lottery — which is exactly the night-6 report (#73): aim at the arm, get the spray.
|
||||||
|
**COMPLETED 2026-08-03 (#124, the zone-walk matrix): the pick is TRIANGLE-ACCURATE** [T2 matrix].
|
||||||
|
The sphere approximation measurably mis-picked (aim dead-on dtorso -> picked rgun/ruleg) —
|
||||||
|
`MechSegmentPick` runs a sphere PRE-FILTER then Moller-Trumbore nearest-hit over the threaded
|
||||||
|
segments' posed meshes (CPU triangle cache from each object's own BGF buffers, `BTGetPickMesh`).
|
||||||
|
**⚠ CORRECTED 2026-08-04 (field pushback from era players — they were RIGHT): the 08-03 zone rule
|
||||||
|
("the struck segment's SKL dzone, ALWAYS" -> every aimed torso hit = CENTER TORSO) was WRONG.**
|
||||||
|
The authentic rule: **the pick's zone is the struck PATCH's authored `dz_*` tag** — the art
|
||||||
|
zone-tags the hull PER PANEL (`MAD_TOR.BGF` carries `dz_utorso`×36 / `dz_ltorso`×18 /
|
||||||
|
`dz_rtorso`×18 / `dz_dtorso`×16 + all four rear panels + `dz_searchlight` across its patches
|
||||||
|
[T1 bytes]); the dpl hit result kept GEOGROUP granularity (`dplHitGeoGroup` in the renderer's
|
||||||
|
result block [T0]); and the binary's segment->zone map `@49db20` has NO runtime caller (sole
|
||||||
|
caller = `CreateStreamedDamageZone`, load-time — raw call-scan [T1]), so no segment-level
|
||||||
|
collapse mechanism even exists. Aimed fire in the pod stripped the exact hull panel under the
|
||||||
|
reticle. The segment-level model looked airtight because LIMBS agree at both granularities (one
|
||||||
|
zone per limb mesh) — the collapse only bit the hull, and Oracle's night-10 audit ("only LCT gets
|
||||||
|
hits") was the BUG's fingerprint, not the pod's design. Port: `MechSegmentPick` attributes the
|
||||||
|
struck triangle to its draw op (index-range) and takes the op's `.DZM`-bound zone (the #87
|
||||||
|
armour-darkening bindings — the same authored patch->zone mapping that already paints those
|
||||||
|
panels), segment dzone as the untagged-patch fallback; `ZoneAimPoint` aims hull zones at their
|
||||||
|
patch CENTROIDS so the zone walker can exercise per-panel. Bench: [[test-harness]] §zone-walk
|
||||||
|
(madcat variant, scratchpad/night11/zonewalk_madcat.sh).
|
||||||
|
**MISS-MEANS-MISS (#131, 2026-08-05) [T2 both-direction bench]:** the pick answers ONLY for
|
||||||
|
drawn geometry -- the any-object SPHERE fallback and the caller's whole-mech AABB fallback are
|
||||||
|
gone (sphere only for a mesh the reader cannot parse -- none exist, counters clean; AABB only as
|
||||||
|
the pre-tree replicant grace, `BTMechSegmentPick` returns 1/0/-1). They were port stand-ins the
|
||||||
|
1995 card never had, and their signature regime was a LEVEL boresight over a SHORT mech: the
|
||||||
|
blackhawk's mesh tops out below eye-ray height, so rays cleared every triangle while threading
|
||||||
|
the fat cull spheres -- lock ring lit with the reticle above its head (night-12 field report,
|
||||||
|
operator-witnessed on the sweep bench). Instruments: `BT_LOCK_SWEEP` (torso-pan lock envelope,
|
||||||
|
operator-watchable), `BT_LOCK_ENVELOPE` (synthetic unit sweep), `[picksrc]`/`[pickbox]` source
|
||||||
|
telemetry + objs/invFail/noTri localization. Locking is now strictly TIGHTER than 716-774; if
|
||||||
|
era testers report the pods were more forgiving, Draco's "slight lock linger" memory is the
|
||||||
|
sourced follow-up (tracked on #131).
|
||||||
|
**TWIST-SIGN VERIFIED (2026-08-04) [T2 live]:** the frame adapter's inferred angular flip
|
||||||
|
(`SelectSlice: theta -= TorsoHeading()`, vs the binary's `+=` before the z-reflection) is
|
||||||
|
CORRECT — bench: target torso pinned 0/+140°/−140° (`BT_FORCE_TWIST`, sim-level
|
||||||
|
`analogTwistAxis` hold in torso.cpp — the input-level pin is DEAD, live input rides the
|
||||||
|
CONTROLS.MAP device push and Basic mode auto-centers), LRM salvos (missiles = the authentic
|
||||||
|
cylinder consumers; the binary DROPPED zone-−1 beam damage). Slice picks track the
|
||||||
|
physically-facing flank in both twist directions (positive twist = `horizontalLimitLeft` =
|
||||||
|
CCW), deterministic; the wrong sign is cleanly excluded (would pick slice 7 where slice 1 is
|
||||||
|
observed). Diag: `[slice]` log (live twist, thetaIn/thetaAdj, wedge index) + zone NAMES on
|
||||||
|
`[dmgresolve]` (both under `BT_DMGTABLE_LOG`); bench `scratchpad/night11/twistsign.sh`.
|
||||||
|
|
||||||
**IMPLEMENTED (2026-07-29/30) [T2 bench]: the aimed PER-PART pick.**
|
**IMPLEMENTED (2026-07-29/30) [T2 bench]: the aimed PER-PART pick.**
|
||||||
`BTL4VideoRenderer::MechSegmentPick` (btl4vid.cpp): at tree build, each segment's draw object +
|
`BTL4VideoRenderer::MechSegmentPick` (btl4vid.cpp): at tree build, each segment's draw object +
|
||||||
`GetPrimaryDamageZone()` (SEGMENT.h — carried per segment in the skeleton STREAM, read by
|
`GetPrimaryDamageZone()` (SEGMENT.h — carried per segment in the skeleton STREAM, read by
|
||||||
@@ -234,7 +278,7 @@ no-arc). The old hardwired lock, the ±30°-default cone, and the projectile pat
|
|||||||
fallback are all REMOVED. LMB fires lasers / RMB missiles (with SPACE/CTRL). ⚠ A view-selection
|
fallback are all REMOVED. LMB fires lasers / RMB missiles (with SPACE/CTRL). ⚠ A view-selection
|
||||||
bug was fixed en route: every
|
bug was fixed en route: every
|
||||||
renderable rebuild stomped `mCamera` back to the chase eye (btl4vid `mViewInside` now persists
|
renderable rebuild stomped `mCamera` back to the chase eye (btl4vid `mViewInside` now persists
|
||||||
the chosen view — the aim camera feed and the V toggle both depend on it).
|
the chosen view — the aim camera feed and the view toggle (backtick since task #68 took 'V' for rear-view) both depend on it).
|
||||||
|
|
||||||
**WORLD STRUCTURES ARE TARGETABLE via the STATIC COLLISION TREE (task #50, 2026-07-15) [T2].**
|
**WORLD STRUCTURES ARE TARGETABLE via the STATIC COLLISION TREE (task #50, 2026-07-15) [T2].**
|
||||||
The `BTGroundRayHit` "terrain" tier only samples the VISUAL heightfield, which on arena1 is a
|
The `BTGroundRayHit` "terrain" tier only samples the VISUAL heightfield, which on arena1 is a
|
||||||
@@ -324,6 +368,34 @@ per ER-M laser; [[decomp-reference]] §5). Flying projectiles (LRM/autocannon) a
|
|||||||
reconstruction (`BTPushProjectile` — the 2007 Entity is too small for the binary's raw integrator
|
reconstruction (`BTPushProjectile` — the 2007 Entity is too small for the binary's raw integrator
|
||||||
offsets). [T2]
|
offsets). [T2]
|
||||||
|
|
||||||
|
## "Mechs die with lightly damaged armor" — AUDITED, no port bug: it's AMMO COOK-OFF (2026-08-02) [T1 chain, T2 measured]
|
||||||
|
Field complaint (night 9): mechs die while their panels read nearly pristine, so "the scaling
|
||||||
|
must be off". A byte-level audit of the ENTIRE damage chain found **every link faithful**:
|
||||||
|
zone stream reads (arcade base ctor `@0041df5c` == engine DAMAGE.cpp, incl. the JointedMover
|
||||||
|
skip-5 branch); the BT ctor normalization loop (`@0049ce50` constants 1.0/1e-4/0.5 — and in
|
||||||
|
SHIPPED CONTENT the raw cells are EXACTLY `1/armorPts` uniform across all 5 types, so the
|
||||||
|
normalize is a no-op); `TakeDamage @0041e4e0` formula+clamps; the override `@0049c690`
|
||||||
|
(redirect gate, vital/leg thresholds 1.0/0.5/0.0 `@0049c99c..a4`); `CriticalHit @0049ccc4`
|
||||||
|
(fraction 0.5 `@0049ce48`, cap 1.0 `@0049ce4c`, the per-subsystem `applied<damagePercentage`
|
||||||
|
cap); the cook-off chain (`stamp @004bc3fc` per-SALVO before the missile divide, `CookOff
|
||||||
|
@004bd300` = ammoCount×amount, `DistributeCriticalHit @004ac274` = ÷(zones plugging the bin)
|
||||||
|
then one DESIGNATED-zone TakeDamageMessage each).
|
||||||
|
**The measured kill route** (death-kinetics bench, 7 deaths): 6 of 7 were **bay-fire
|
||||||
|
detonations** — e.g. `AmmoBinLRM15 10 rounds × 50 = 500 (burst 1)` into the bin's ONE authored
|
||||||
|
carrier zone (madcat bins are single-zone: LRM15_1=roster[26]→dz_ltorso, LRM15_2=[28]→dz_rtorso,
|
||||||
|
AFC100=[21]→{larm,lgun}), which is VITAL (madcat authors **15 of 22 zones vital** — all torsos,
|
||||||
|
legs, feet) → one application 0→1.0 → instant death, every other panel bright. The ÷1 is the
|
||||||
|
CONTENT's authoring, not a port shortcut — the arithmetic is the binary's own. The one mech that
|
||||||
|
died conventionally (300 applications) ended VISIBLY charred: 0.97/0.93/0.92/0.85/0.79 across 13
|
||||||
|
zones — mid-fight darkening DOES accumulate when mechs live long enough.
|
||||||
|
**Field logs confirm dominance**: Rajel's night-9 log alone has 12 bay-fire events (LRM10
|
||||||
|
455-490 pts); ConnMan 10; the "salvo barely dented my armor" reporter was dying to their OWN
|
||||||
|
cook-offs. **The real lever is HEAT** (bay fire arms at the bin heat-watcher's FAILURE level;
|
||||||
|
the [[subsystems]] heat/#96 too-hot economy arms it constantly) plus player training: EJECT
|
||||||
|
**hold** purges the burning bay (`DumpAmmo` → "bay fire EXTINGUISHED"); pod players were
|
||||||
|
manual-trained to purge ([[pod-hardware]] §Manual), testers are not. Novice sim gates also
|
||||||
|
suppress the whole path ([[experience-levels]]).
|
||||||
|
|
||||||
## The RAM economy — CLOSED (2026-07-12) [T1 evidence, T3 normalization]
|
## The RAM economy — CLOSED (2026-07-12) [T1 evidence, T3 normalization]
|
||||||
Three-layer story, measured live + decomp-verified:
|
Three-layer story, measured live + decomp-verified:
|
||||||
1. **The armor economy is POINTS**: every zone streams `damageScale[5]` — 5 cells indexed by
|
1. **The armor economy is POINTS**: every zone streams `damageScale[5]` — 5 cells indexed by
|
||||||
@@ -497,7 +569,9 @@ percent tables, MadCat) — see #73 on the tracker.
|
|||||||
## layers are now fixed and live-verified
|
## layers are now fixed and live-verified
|
||||||
The recovery: the un-exported gap held `Mech::TakeDamageMessageHandler @0x4a0230` (found via the
|
The recovery: the un-exported gap held `Mech::TakeDamageMessageHandler @0x4a0230` (found via the
|
||||||
Mech **message table @0x50bdf8** — rows `{id, name, handler}`: 0x12 TakeDamage, 0x14 PlayerLink,
|
Mech **message table @0x50bdf8** — rows `{id, name, handler}`: 0x12 TakeDamage, 0x14 PlayerLink,
|
||||||
0x15 RealMaxSpeed, 0x16 BalanceCoolant, 0x17/0x18 Set/ClearBurningState, 0x19 EjectPilot @0x49f854,
|
0x15 RealMaxSpeed, 0x16 BalanceCoolant, 0x17/0x18 Set/ClearBurningState (⚠ label/body mismatch:
|
||||||
|
the 0x17 handler @0x49f674 randomizes position + graphicAlarm(2) + re-enables sim — not burning;
|
||||||
|
possibly a shifted name row, cf. the factory ClassID mislabels — open-questions), 0x19 EjectPilot @0x49f854,
|
||||||
0x1a DuckRequest @0x49fa00) and the **crit-chance roll `@0x4a0164`**:
|
0x1a DuckRequest @0x49fa00) and the **crit-chance roll `@0x4a0164`**:
|
||||||
`p = clamp(0.7·damageLevel² + 0.01, 0..1)`, gated on the player's `simLive` flag (+0x25c — novice
|
`p = clamp(0.7·damageLevel² + 0.01, 0..1)`, gated on the player's `simLive` flag (+0x25c — novice
|
||||||
never crits), rolled per BURST on the current zone (skip if the zone is already burning). The
|
never crits), rolled per BURST on the current zone (skip if the zone is already burning). The
|
||||||
@@ -510,12 +584,58 @@ entirely — multi-burst under-applied (burst-1)×) is superseded. `MechSubsyste
|
|||||||
@0x4ac0bc` is real (zone damage → destroyed alarms → **vital-subsystem kill**: owner
|
@0x4ac0bc` is real (zone damage → destroyed alarms → **vital-subsystem kill**: owner
|
||||||
`graphicAlarm` level 9, the #28 machinery — CLASSMAP's "HandleMessage@4ac0bc" was a mislabel).
|
`graphicAlarm` level 9, the #28 machinery — CLASSMAP's "HandleMessage@4ac0bc" was a mislabel).
|
||||||
Live-verified: `[subarmor]` shows parsed scales at spawn; `[critroll] zone=3 -> Myomers subLvl=1`
|
Live-verified: `[subarmor]` shows parsed scales at spawn; `[critroll] zone=3 -> Myomers subLvl=1`
|
||||||
— a full chain crit destroying a subsystem. Still deferred [T4→documented]: the `damageType==0`
|
— a full chain crit destroying a subsystem. The `damageType==0` COLLISION divert (@0x4a0368 →
|
||||||
COLLISION divert (@0x4a0368 → `0x49ffcc`, its own distribution) and the **id-0x16 damage/kill
|
`0x49ffcc`) and the id-0x16 report tail are BOTH reconstructed — see the next section. Diags:
|
||||||
reports** to shooter+victim players (@0x4a04da-0x4a07b2 — the authentic stats plumbing, feeds #45:
|
`BT_CRIT_LOG` (`[subarmor]` + `[critroll]`), `BT_DMG_LOG`.
|
||||||
`{tally, zone, destroyed-flag, inflicting subsystem, victim name}`, kill-flagged variant on newly
|
|
||||||
disabled, plus a killed-by block gated on movementMode 9/10). Diags: `BT_CRIT_LOG` (`[subarmor]` +
|
## ✅ THE AUTHENTIC SCORE/DEATH REPORT TAIL (2026-08-05, #45/#134) [T1 raw disasm, T2 both benches]
|
||||||
`[critroll]`), `BT_DMG_LOG`.
|
The rest of `Mech::TakeDamageMessageHandler` (@0x4a02f4-0x4a0890, dark-gap raw disasm) is now
|
||||||
|
reconstructed: `BTMechPostCombatReports` + `BTMechPostVehicleDead` (btplayer.cpp), called from the
|
||||||
|
handler at the binary's exact positions (mech.cpp). Retires `BTPostDamageScore`/`BTPostKillScore`.
|
||||||
|
|
||||||
|
**Entry sequence** [T1]: gyro bounce → death-edge latch `[ebp-0x10]` (was-destroyed at ENTRY —
|
||||||
|
before the divert!) → `lastInflictingID` stamp (mech+0x43c ← msg inflictor) → shooter resolve
|
||||||
|
(registry find → `shooter+0x190` = shooter's PLAYER; victim's = `mech+0x190`) → **collision divert
|
||||||
|
JUMPS TO THE DEATH TAIL** (@0x4a0375 `jmp 0x4a07b5`, NOT a return — a wall/fall death still posts
|
||||||
|
VehicleDead + blasts, but skips the score blocks: collision deaths credit no one). Our old early
|
||||||
|
`return` here was a latent "wall-death strands the pilot" hazard, fixed with the move.
|
||||||
|
|
||||||
|
**The three id-0x16 reports** (all `Player::ScoreMessageID` 0x16, sizeof 0x3C — layout in
|
||||||
|
btplayer.hpp, static_assert-locked; `scoreAward`=APPLIED tally in all three):
|
||||||
|
- **A @0x4a04da, newly destroyed** → type 2 KillScore to the SHOOTER's player. Basis (+0x24) =
|
||||||
|
the VICTIM role's `killBonus` (role+0x1c). senderMechID = the victim. Suicide (eject charge,
|
||||||
|
self-damage) IS dispatched — the handler negates the award (@0x4c03ab `fchs`) and skips
|
||||||
|
`killCount++`: **the #134 panic penalty, live** (bench: `type=2 award=-39.00 kills=0`).
|
||||||
|
- **B @0x4a05d9, not newly killed && tally≠0** → type 0 to the shooter. The ONLY registered 0x16
|
||||||
|
receiver Verify-rejects type 0 → 1995 folded an UNINITIALIZED stack float into the shooter's
|
||||||
|
score on every non-lethal hit (real 1995 bug — @0x4c0200, the handler that accepts type 0, is
|
||||||
|
in NO table entry: dead code). Port sends it for wire fidelity, banks award 0.
|
||||||
|
- **C @0x4a06c0, tally>0 (kills included)** → type 1 DamageReceivedScore to the VICTIM's player.
|
||||||
|
Basis = INTENDED damage (burstCount×amount). senderMechID = the INFLICTOR. Feeds the received
|
||||||
|
penalty (`CalcDamageReceivedScore` returns the NEGATIVE) + the operator-console VTVDamaged line
|
||||||
|
(gated `GetConsoleHost()` && !`suppressConsole` — which is why that field is named that).
|
||||||
|
Reports carry the LOOP-ENTRY zone (msg+0x24, never rewritten mid-loop), the vital-wreck flag, and
|
||||||
|
`inflictingSubsystemID` (msg+0x5c, engine T0 name).
|
||||||
|
|
||||||
|
**Score model consequence** [T1]: 1995 pod scoring = **kill awards + received-damage penalties +
|
||||||
|
death costs. No per-hit inflicted credit** (the port's old per-hit crediting — and the #95 salvo
|
||||||
|
fix on top of it — were inventions riding the dead @0x4c0200 channel; both retired).
|
||||||
|
Kill award = `(victimKillBonus + tally) × killerRole.damageInflictedModifier ×
|
||||||
|
(victimAvgZoneDamage@0x354 × damageBias + 1.0) × (victimTonnage/killerTonnage)`; same-team kill in
|
||||||
|
a non-FFA game = `-friendlyFirePenalty` basis (inline strcmp of `teamName@0x20c`, gate
|
||||||
|
`freeForAll@0x250==0`); self-kill negates the whole award.
|
||||||
|
|
||||||
|
**The death tail** (@0x4a07b5, shared by the divert): gate = was-alive-at-entry && movementMode
|
||||||
|
9|10 → **`Player::VehicleDeadMessage` id 0x17 to the mech's OWN player** — the respawn trigger,
|
||||||
|
in the BT 0x38-byte extension `{deathCount -1, dropZone Null, +0x2c killed-by PLAYER EntityID,
|
||||||
|
+0x34 kill zone}` (bench: `killedBy=2:1 zone=3` cross-node) — then the death Explosion (id 3,
|
||||||
|
0x5C, model 0x31; port fires the authored death list from the transition instead, #42) and the
|
||||||
|
#89 SplashDamage (re-verified after the reorder: 72 bursts at ~9u). The old mech4
|
||||||
|
death-transition dispatch site (which was flagged [T3 sender-undecoded]) is retired — this IS
|
||||||
|
that sender. The #55 NULL-playerLink fallback + DEAD_NOTIFY forensics moved into the bridge.
|
||||||
|
Benches: `scratchpad/night12/scorekill.sh` (cross-node kill: killer `kills=1 award=4.88`,
|
||||||
|
victim respawns, death #1 single-cycle) + `scoreself.sh` (#134 negation). Collision-death tail
|
||||||
|
fallthrough is inspection-tier [T3] — shares the benched tail code; field wall-deaths exercise it.
|
||||||
|
|
||||||
## (HISTORICAL — the gap as found 2026-07-29, superseded above) [T1]
|
## (HISTORICAL — the gap as found 2026-07-29, superseded above) [T1]
|
||||||
The authored crit machinery exists and is reconstructed — `Mech__DamageZone::CriticalHit @0049ccc4`
|
The authored crit machinery exists and is reconstructed — `Mech__DamageZone::CriticalHit @0049ccc4`
|
||||||
@@ -833,7 +953,9 @@ stub let it fire and corrupt `graphicAlarm`. Now `mech->IsDisabled()` — the la
|
|||||||
defence-in-depth. Sibling fix: `mechdmg.cpp:451` read the phantom `stance` (perma-0) instead of
|
defence-in-depth. Sibling fix: `mechdmg.cpp:451` read the phantom `stance` (perma-0) instead of
|
||||||
`MovementMode()`, so the leg-shot-out → `graphicAlarm=9` fall/death branch was entirely DEAD; now live.
|
`MovementMode()`, so the leg-shot-out → `graphicAlarm=9` fall/death branch was entirely DEAD; now live.
|
||||||
|
|
||||||
## Kill-score damage (task #60)
|
## Kill-score damage (task #60) — ⚠ SUPERSEDED 2026-08-05 by §THE AUTHENTIC SCORE/DEATH REPORT
|
||||||
|
## TAIL above (`BTPostKillScore` is RETIRED; the kill basis is now the applied damage TALLY +
|
||||||
|
## the victim role's killBonus, posted by the victim's own TakeDamage handler). History below.
|
||||||
`BTPostKillScore` (btplayer.cpp:1491) feeds the ScoreMessage `damageAmount` into the kill award
|
`BTPostKillScore` (btplayer.cpp:1491) feeds the ScoreMessage `damageAmount` into the kill award
|
||||||
(`@0x4c02e4` → `(damageAmount + scoreAward) × roleScalar × teamMult × tonnageRatio`, `@0x4c052c`).
|
(`@0x4c02e4` → `(damageAmount + scoreAward) × roleScalar × teamMult × tonnageRatio`, `@0x4c052c`).
|
||||||
The port passed a flat `kShotDamage=12` (mech4.cpp:1551), so every kill scored identically regardless
|
The port passed a flat `kShotDamage=12` (mech4.cpp:1551), so every kill scored identically regardless
|
||||||
@@ -965,6 +1087,60 @@ returned NULL) -- the cascade "ran" and touched nothing, which is why blown-off
|
|||||||
arms left firing gun pods across every chassis (Conn Man's three-chassis audit).
|
arms left firing gun pods across every chassis (Conn Man's three-chassis audit).
|
||||||
See reconstruction-gotchas #25.
|
See reconstruction-gotchas #25.
|
||||||
|
|
||||||
|
## Zone-LEVEL replication -- the observer's picture (#87 root cause, 2026-08-03) [T1 decomp / T2 verified]
|
||||||
|
|
||||||
|
How an OBSERVER (replicant holder) learns a mech's zone damageLevels, end to end:
|
||||||
|
|
||||||
|
1. Damage messages do NOT echo locally: `Entity::Dispatch` on a REPLICANT
|
||||||
|
forwards to the master's node and returns (ENTITY.cpp:244) -- there is no
|
||||||
|
broadcast-bus application. The MASTER alone consumes `TakeDamage`.
|
||||||
|
2. The master ships levels via zone UPDATE RECORDS
|
||||||
|
(`DamageZone::Write/ReadUpdateRecord`, `DamageZoneUpdateModelBit`), gated by
|
||||||
|
`ForceUpdate(DamageZoneUpdateModelFlag)`.
|
||||||
|
3. **Who raises the flag (the binary's effect watcher `FUN_0042aa2c @0042aa2c`,
|
||||||
|
the BT analog of the engine's `EntityEffectWatcher` -- which itself never
|
||||||
|
runs for mechs: mech zones carry BAND DESCRIPTORS, the engine `ExplosionTable`
|
||||||
|
stays NULL and `EntityEffectWatcher` is constructed nowhere in the image):**
|
||||||
|
* level branch (zone changedFlags & 4): if the rise CROSSES a band-descriptor
|
||||||
|
threshold (`FUN_0042a5f4` = `Mech__DamageZone::DescriptorCrossed`) AND the
|
||||||
|
entity is a MASTER (`(entity+0x28 & 0xc) == 0`) -> `*(entity+0x18) |= 2`
|
||||||
|
(= ForceUpdate(DamageZoneUpdateModelFlag)).
|
||||||
|
* gstate branch (changedFlags & 8): ForceUpdate unconditionally (masters).
|
||||||
|
So observers track a fight at **band-crossing granularity** -- not per hit.
|
||||||
|
4. Consumption on the observer is passive: `ReadUpdateRecord` writes the level,
|
||||||
|
the armour watcher (`TickArmourDamage`) re-reads zones every frame and pushes
|
||||||
|
the draw-op tint; the doll gauges read `damageLevel*100` continuously.
|
||||||
|
|
||||||
|
**The port had dropped the level branch** -- mechdmg.cpp's band hub ForceUpdated
|
||||||
|
only on `graphicState != Exists`, so every observer's copy sat at 0.0 until a
|
||||||
|
zone DESTRUCTED: no enemy hull darkening, no observer-side doll movement, ever
|
||||||
|
(the "no armour discoloration" night-10 report). Restored 2026-08-03
|
||||||
|
(mechdmg.cpp band loop: `DescriptorCrossed(prev, level)` + master gate on both
|
||||||
|
branches). Verified 2-node: A-side watcher pushed 114 level changes; every
|
||||||
|
replicant peak matched the master's final level to 4 decimals (dtorso 0.9321
|
||||||
|
both sides).
|
||||||
|
|
||||||
|
Render-path note (gotcha #23 discharged): the tint itself was pixel-proven with
|
||||||
|
`BT_ARMOR_FORCE` A/B captures -- all-zones 1.0 renders the hull charcoal (0.1x
|
||||||
|
floor), forced 0.4 reads visibly grey at close range on a light skin, so "no
|
||||||
|
discoloration" was never the renderer. Solo/master-side perception is the
|
||||||
|
authentic ECONOMY: 2-25 pt laser events vs 68-185 pt zone pools. The eye
|
||||||
|
catches SNAPS, not creep: a single heavy hit (ERLG large laser / missile
|
||||||
|
class, amt=25) jumps a 77-pt arm 0 -> 0.3247 in one frame (measured live) =
|
||||||
|
the pod veterans' "a single missile leaves visible armor damage"; 2-pt ERS
|
||||||
|
fire creeps ~1.6%/hit and stays visually silent for a dozen hits (the
|
||||||
|
"legs never show" reports -- their levels peak ~0.08-0.13). The cockpit doll
|
||||||
|
maps all 28 zones (L4GAUGE.CFG:4788-4814 cmArmor lines -- feet + lower legs
|
||||||
|
included); nothing is unwired (orphan audit 2026-08-03: every drawn op with a
|
||||||
|
material name is claimed by a zone).
|
||||||
|
|
||||||
|
Fidelity boundary [T1]: the HOST binary's response is LINEAR (watcher
|
||||||
|
@004573e4/@00457784: 13 material floats lerped toward x0.1 by level, then
|
||||||
|
"flush_material" to the division card). The CARD-side material->ramp->texel
|
||||||
|
curve is i860 firmware absent from BTL4OPT.EXE -- see [[open-questions]]
|
||||||
|
"i860 DIVISION-CARD firmware". If pod darkening was perceptually steeper at
|
||||||
|
mid levels, that steepness lived there.
|
||||||
|
|
||||||
## The hit-location CYLINDER -- raw-bytes audit + chassis->table map (2026-08-02) [T1]
|
## The hit-location CYLINDER -- raw-bytes audit + chassis->table map (2026-08-02) [T1]
|
||||||
|
|
||||||
Full independent verification of the type-29 DamageLookupTable streams, parsed
|
Full independent verification of the type-29 DamageLookupTable streams, parsed
|
||||||
@@ -984,6 +1160,22 @@ code involved; grammar = the binary ctors @0x49ea48/@0x49e5e4). Results:
|
|||||||
"Right*", W1/W2 = "Front*" -- with theta = atan2(z,x) from +X toward +Z, the
|
"Right*", W1/W2 = "Front*" -- with theta = atan2(z,x) from +X toward +Z, the
|
||||||
mech-local FRONT is the **+Z arc (theta 45-135 deg)** and dead-ahead is the
|
mech-local FRONT is the **+Z arc (theta 45-135 deg)** and dead-ahead is the
|
||||||
W1|W2 SEAM. (Corrects the earlier #92 comment that put frontal hits in W6.)
|
W1|W2 SEAM. (Corrects the earlier #92 comment that put frontal hits in W6.)
|
||||||
|
* **⚠ PORT FRAME ADAPTER (#124, fixed 2026-08-03; corrected same day by the
|
||||||
|
MUZZLE-ANCHOR probe)**: that +Z-forward (and +X-right, from the W0/W7
|
||||||
|
"Right*" names) is the **1995 resolver frame**. OUR engine frame, measured
|
||||||
|
with the mech's own asymmetric geometry as the anchor (`BT_ASPECT_TEST`:
|
||||||
|
four world-cardinal self-impacts + one at each weapon's physical muzzle):
|
||||||
|
**RIGHT = +X, same as 1995; FORWARD = −Z, flipped** — the LEFT missile pod
|
||||||
|
sits at raw local x=−2.32, the RIGHT at +2.32, and `WorldToLocal` is a clean
|
||||||
|
R(yaw) with no hidden terms (verified at yaws −30°/150°/40°). The frames
|
||||||
|
differ by a **Z-NEGATION ONLY — a reflection** (the first commit's π
|
||||||
|
rotation CROSSED the pods: the left muzzle resolved rtorso). `ResolveHit`
|
||||||
|
negates local z; and because a reflection reverses angular direction, the
|
||||||
|
torso-twist term in `SelectSlice` enters with the OPPOSITE sign (twist was 0
|
||||||
|
in every probe — the twisted-torso verify is the tracked follow-up).
|
||||||
|
Anchor acceptance: left pod→ltorso, right pod→right wedges, left/right arm
|
||||||
|
muzzles→larm/rarm, nose→front cells, tail→rear cells, at a third yaw. The
|
||||||
|
separate "one band LOW" Y-signature is #16 (pick-ray parallax), not this.
|
||||||
Frontal unaimed foot-band hits therefore straddle the RightFoot/LeftFoot
|
Frontal unaimed foot-band hits therefore straddle the RightFoot/LeftFoot
|
||||||
cells by impact-x sign, and the authored cross-bleed (20% opposite foot,
|
cells by impact-x sign, and the authored cross-bleed (20% opposite foot,
|
||||||
10+10% opposite leg) is a data-true source of "hit the opposite leg" reports.
|
10+10% opposite leg) is a data-true source of "hit the opposite leg" reports.
|
||||||
|
|||||||
+166
-3
@@ -176,6 +176,22 @@ rows until sanity fails (`python + struct`, see the session commits `cc2b109`/`2
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Master posture / crouch block (FUN_004a9b5c region, raw disasm 2026-08-05/06) [T1]
|
||||||
|
- mech+0x3f8 `mapPosture` (0 none / 1 may-duck / 2 holding-squat) — selector @0x4a9f61-0x4aa007:
|
||||||
|
gates = movementMode normal, simLive (player+0x25c, NOVICE lockout), leg alarm 0/4 → 1, leg
|
||||||
|
alarm 1 → 2, myomer factor |x|>1e-4 (const @0x4ab16c).
|
||||||
|
- mech+0x79c `myomerEffectiveness` — MAX over the myomer chain's speedEffect@0x31C, published by
|
||||||
|
the mapper drive-scale block (mechmppr.cpp:990 == @0x4a9cf2-0x4a9da4); scales speedDemand,
|
||||||
|
zeroes turnDemand at ~0 (the seek-4 freeze), and gates the crouch.
|
||||||
|
- DuckRequest consumer @0x4aa011-0x4aa0af: duckState && squatCapable → SetLegAnimation(2 'sqd')
|
||||||
|
/ (3 'squ'), ForceUpdate(8)+(1) (type-3 record ships legState → peers pose it), stability
|
||||||
|
alarm 0/1, request consumed. Clips: animationClips[2]=sqd(i), [3]=squ(i) (BTL4.RES, all 8
|
||||||
|
chassis; loader slots byte-verified vs @0x50d9c8 suffix pool).
|
||||||
|
- Myomers cluster: Performance wrapper @004b8b9c → drive-heat integrator @004b8be3;
|
||||||
|
AvailableOutput @004b8ac0 = gear clamp × QUADRATIC heat degrade (+0x114 vs +0x118/+0x11c band)
|
||||||
|
× (1 − legZoneDamage); speedEffect = output / ownerBaseSpeed(+0x34C) — unclamped (gear-4
|
||||||
|
supercharge ≈1.43).
|
||||||
|
|
||||||
## 4. Damage delivery
|
## 4. Damage delivery
|
||||||
|
|
||||||
- `Entity::TakeDamageMessage(id, size, inflictingEntityID, zone, Damage&)` → `target->Dispatch(&msg)`.
|
- `Entity::TakeDamageMessage(id, size, inflictingEntityID, zone, Damage&)` → `target->Dispatch(&msg)`.
|
||||||
@@ -183,6 +199,7 @@ rows until sanity fails (`python + struct`, see the session commits `cc2b109`/`2
|
|||||||
- `class Damage { damageType(enum Collision/Ballistic/Explosive/Laser/Energy), damageAmount, damageForce, surfaceNormal, impactPoint, burstCount }`. [T1]
|
- `class Damage { damageType(enum Collision/Ballistic/Explosive/Laser/Energy), damageAmount, damageForce, surfaceNormal, impactPoint, burstCount }`. [T1]
|
||||||
- Weapon effect id: **"explode" = 13** (`Explosion::Make`). [T2]
|
- Weapon effect id: **"explode" = 13** (`Explosion::Make`). [T2]
|
||||||
- `DestroyEntityMessage(id,size)` removes an entity — but a killed mech STAYS (a WRECK); death = a STATE transition (`SetGraphicState(DestroyedGraphicState)`), NOT removal. Issuing removal-on-death is the P5 teardown bug (do not). [T2]
|
- `DestroyEntityMessage(id,size)` removes an entity — but a killed mech STAYS (a WRECK); death = a STATE transition (`SetGraphicState(DestroyedGraphicState)`), NOT removal. Issuing removal-on-death is the P5 teardown bug (do not). [T2]
|
||||||
|
- **BT effect watcher (zone replication + band effects)** [T1, 2026-08-03]: ctor `FUN_0042a984 @0042a984` (hooks entity+0xbc, allocs oldLevel[damageZoneCount@+0x11c]); Execute `FUN_0042aa2c @0042aa2c` — per zone: flag&4 → `FUN_0042a664` DescriptorForLevel + `FUN_0042a5f4` DescriptorCrossed(old,new) → **crossed && master (`(entity+0x28 & 0xc)==0`) → `*(ushort*)(entity+0x18) |= 2`** (= `ForceUpdate(DamageZoneUpdateModelFlag)`, the zone-record send); flag&8 → `FUN_0042a6c4` DescriptorForGraphicState + the same master-gated ForceUpdate; descriptor → effect via `FUN_0043663c/FUN_004364e4` (renderer mgr @`DAT_004efc94+0x38`); changedFlags reset when (master && !pending-update) or replicant. Zone band table @ zone+0xd4; level @+0x158; gstate @+0x78. Engine `EntityEffectWatcher`/`ExplosionTable` are DEAD for mechs (table NULL, watcher never constructed — band descriptors replace them). Port: mechdmg.cpp band hub. See [[combat-damage]] §Zone-LEVEL replication.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -246,9 +263,36 @@ From the weapon `.SUB` records + the charge-curve `.data` constants (PE-parsed a
|
|||||||
### Binary message tables decoded 2026-07-20 (glass input audit) [T1]
|
### Binary message tables decoded 2026-07-20 (glass input audit) [T1]
|
||||||
16-byte HandlerEntry {id, namePtr, fnPtr, 0} rows in section_dump. Per-receiver-class id spaces.
|
16-byte HandlerEntry {id, namePtr, fnPtr, 0} rows in section_dump. Per-receiver-class id spaces.
|
||||||
- **Mech (entity)** @0x50BDF8..: 0x12 TakeDamage@004a0230, 0x14 PlayerLink@0049f624, 0x15
|
- **Mech (entity)** @0x50BDF8..: 0x12 TakeDamage@004a0230, 0x14 PlayerLink@0049f624, 0x15
|
||||||
RealMaxSpeed@0049f604, **0x16 BalanceCoolant@0049f728 (WIRED 2026-07-21, issue #20**: press-only, NO novice guard; sets every condenser valveState@0x1D0=1 then the shared redistribute @0049f788 == BTRecomputeCondenserValves; verify BT_BALTEST=1 + BT_VALVE_LOG=1**)**, 0x17 SetBurningState@0049f674, 0x18
|
RealMaxSpeed@0049f604, **0x16 BalanceCoolant@0049f728 (WIRED 2026-07-21, issue #20**: press-only, NO novice guard; sets every condenser valveState@0x1D0=1 then the shared redistribute @0049f788 == BTRecomputeCondenserValves; verify BT_BALTEST=1 + BT_VALVE_LOG=1**)**, 0x17 SetBurningState@0049f674 (⚠ body mismatch — randomizes position, graphicAlarm 2, sim re-arm; see open-questions), 0x18
|
||||||
ClearBurningState@0049f700, **0x19 EjectPilot@0049f854**, **0x1a DuckRequest@0049fa00** (the
|
ClearBurningState@0049f700, **0x19 EjectPilot@0049f854**, **0x1a DuckRequest@0049fa00** (the
|
||||||
manual's CROUCH; streamed button 0x13 sends it — unreconstructed).
|
manual's CROUCH; streamed button 0x13 sends it — unreconstructed).
|
||||||
|
- **BTPlayer** (byte-scanned 2026-08-05, file 0x112dxx; 20-byte rows {id, namePtr, fn, 0, 0} — 6
|
||||||
|
entries EXACTLY, matching the T0 PLAYER.h enum): **0x15 DropZoneReply@0x4bffd0, 0x16
|
||||||
|
Score@0x4c02e4** (the type-1/2 switch — BT shadows the engine id with its override), **0x17
|
||||||
|
VehicleDead@0x4c05c4, 0x18 MissionStarting@0x4bfbe8, 0x19 MissionEnding@0x4bfc20, 0x1a
|
||||||
|
ScoreUpdate@0x4c02a8** (= Player::NextMessageID). **@0x4c0200 ("ScoreInflicted", type-0-only,
|
||||||
|
Verify line 0x18b) is in NO entry — dead code in 1995.** The 1995 type-0 path therefore lands in
|
||||||
|
@0x4c02e4's Verify arm and folds an UNINITIALIZED [ebp-0xc] into currentScore (real bug; port
|
||||||
|
banks 0). @0x4c02e4 internals [T1 raw]: `+0x40==4` MissionEnding gate; registry-find msg+0x34;
|
||||||
|
case 1 = `(ownTonnage/senderTonnage) × role->CalcDamageReceivedScore(basis)` + the
|
||||||
|
console VTVDamaged post (ConsoleHost && !suppressConsole@0x258; **arg 6 = Round(AWARD)** — an
|
||||||
|
ST0-arg __ftol @0x4dcd94 the decompiler rendered argless; the old "Now()" read was wrong); case
|
||||||
|
2 = kill: `CalcKillScore@0x4c052c` × (senderTonnage/ownTonnage), self-kill `fchs`, dual
|
||||||
|
`killCount@0x27c++` (shooter + victim's player — the phantom partner increment, masked by
|
||||||
|
replication), StatusMessage{type 0 Destroyed, victim player, 6.0s} from pool @0x512f6c with
|
||||||
|
vtable @0x513344, clear-target @0x4b04d8 via `playerVehicle+0x128` roster head when the dead
|
||||||
|
mech == `objectiveMech@0x284`. `CalcKillScore@0x4c052c` [T1]: same-team (strcmp
|
||||||
|
`teamName@0x20c`, gate `freeForAll@0x250==0`) → `-friendlyFirePenalty(role+0x14)`; else
|
||||||
|
`victimMech->avgZoneDamage@0x354 × damageBias(role+0x18) + 1.0` (const @0x4c05c0); result ×
|
||||||
|
`(basis + tally) × damageInflictedModifier(role+0xC)`. **ScenarioRole offsets** (Node base):
|
||||||
|
+0xC inflictedMod, +0x10 receivedMod, +0x14 ffPenalty, +0x18 damageBias, +0x1C killBonus,
|
||||||
|
+0x20 specialCaseDeathPenalty. **mech+0x354** = average zone damageLevel (computed with the
|
||||||
|
subsystem average @0x358 by the telemetry refresher, part_012.c:9515). **Wire layouts**
|
||||||
|
(static_assert-locked, btplayer.hpp): ScoreMessage 0x3C {+0x1c scoreAward=tally, +0x20 type,
|
||||||
|
+0x24 basis, +0x28 vitalHit, +0x2c zone, +0x30 subsysID(=TakeDamageMessage+0x5c,
|
||||||
|
inflictingSubsystemID [T0]), +0x34 senderMechID}; BT VehicleDeadMessage 0x38 {engine 0x28 +
|
||||||
|
0x28=0, +0x2c killed-by PLAYER EntityID, +0x34 kill zone}. Senders: the TakeDamage report tail
|
||||||
|
— see [[combat-damage]].
|
||||||
- **HeatableSubsystem** @0x50E41C: {3, "ToggleCooling"→@004ad6f8}. **Disassembled 2026-07-20**
|
- **HeatableSubsystem** @0x50E41C: {3, "ToggleCooling"→@004ad6f8}. **Disassembled 2026-07-20**
|
||||||
(`tools/disas2.py 0x4ad6f8`): a per-subsystem coolant on/off TOGGLE (NOT a multi-level "cycle
|
(`tools/disas2.py 0x4ad6f8`): a per-subsystem coolant on/off TOGGLE (NOT a multi-level "cycle
|
||||||
priority" — that's the emergent effect, not the mechanism):
|
priority" — that's the emergent effect, not the mechanism):
|
||||||
@@ -319,6 +363,100 @@ From the weapon `.SUB` records + the charge-curve `.data` constants (PE-parsed a
|
|||||||
Loaded ("full == the gear's seek voltage" is the arcade's own discharge algebra). Repro +
|
Loaded ("full == the gear's seek voltage" is the arcade's own discharge algebra). Repro +
|
||||||
verify: BT_SEEKTEST seek-abuse -- pre-fix deadlocks at pct=0/alarm=3; post-fix 38 rescues,
|
verify: BT_SEEKTEST seek-abuse -- pre-fix deadlocks at pct=0/alarm=3; post-fix 38 rescues,
|
||||||
ends Loaded/pct=1. `[seek]` log prints the per-gear table + rescue events.
|
ends Loaded/pct=1. `[seek]` log prints the per-gear table + rescue events.
|
||||||
|
- **EJECT/PANIC cluster DECODED + WIRED 2026-08-02 [T1 raw disasm]:** `@0049f854`
|
||||||
|
**Mech::EjectPilot** (table id 0x19; sat in the 0x49f854-0x49fa00 export gap) — gates
|
||||||
|
{msg+0xc > 0, ejectPermitted@0x414, !IsDisabled@0049fb54}, console eject notice
|
||||||
|
(`FUN_004c198c` ctor from ownerID@0x18c → network sender, code 5 — port tail),
|
||||||
|
player `suppressConsole`@0x258 = 1 (the following death must not double-notify),
|
||||||
|
`graphicAlarm → 10` (EJECT state; **kills via the ≥9 predicate** — IsMechDestroyed),
|
||||||
|
self TakeDamageMessage {inflicting=SELF, zone −1, Explosive, amount =
|
||||||
|
`ScenarioRole::killBonus` role+0x1c, burst 0}. Role layout byte-settled via the
|
||||||
|
reader `@00429bec` dest offsets + ctor record copy: +0xc inflicted, +0x10 received,
|
||||||
|
+0x14 ffPenalty, +0x18 damageBias, **+0x1c killBonus**, **+0x20
|
||||||
|
SpecialCaseDeathPenalty (4.10-only key)**, +0x28 returnFromDeath. `KillBonus`
|
||||||
|
authors NOWHERE in shipped content → charge 0, alarm does the killing — authentic.
|
||||||
|
`@0049fa1c` **EvaluateEjectPermission** (writes @0x414; unexported caller): roster
|
||||||
|
walk — 0xBBE bank coolant fraction (+0x12c/+0x128) < 0.05 (`@0049fb50`) | zero live
|
||||||
|
Generators (0xBC1, +0x40 ≠ 1 && stateAlarm@0x210 ≠ 4) | live MechWeapons
|
||||||
|
(0x511830; ProjectileWeapon family 0x5121a8 also needs weaponAlarm@0x364 ≠ 7 NoAmmo)
|
||||||
|
< mech+0x448 (**no exported writer — zero, clause inert** [T3]) | leg-gimped
|
||||||
|
(mode 3/4) novice (simLive 0). Port: mech.cpp handler+evaluator, bridges
|
||||||
|
BTWeaponCountsForEject / BTGeneratorCountsForEject / BTHeatSinkBankCoolantFraction /
|
||||||
|
BTPlayerEjectBookkeeping; input = binding-engine `Eject` action (Backspace /
|
||||||
|
pad LeftThumb; BENCH `BT_EJECT_AT=<frame>`, `BT_KILL_SUBSYS` now comma-list).
|
||||||
|
- **EJECT ARM CHAIN closed 2026-08-03 [T1 E8-scan / T2 lamp verified]:** the
|
||||||
|
evaluator's "unexported caller" FOUND — one relative CALL from
|
||||||
|
`0x004a9b6c` = **`FUN_004a9b5c` (the Mech MASTER PERFORMANCE) + 0x10: its
|
||||||
|
FIRST act each frame is EvaluateEjectPermission** (masters only; the fn sits
|
||||||
|
in the 004a977x-004ab188 export hole, located by E8 rel32 scan of the CODE
|
||||||
|
section). Downstream: `@004d196c` L4MechControlsMapper::InterpretControls
|
||||||
|
edge-detects @0x414 (cache @mapper+0x1a8) → Add/RemoveModeMask(**0x200000 =
|
||||||
|
the PANIC-ARMED mode** — the old "missionReviewMode" reading of mech+0x414
|
||||||
|
was a MISLABEL, swept 2026-08-03; the real review mode is the GLOBAL
|
||||||
|
`DAT_004fd550`, btl4pb) → `MakeLinkedLamp(ButtonPanic 0x3d, 0x200000)`
|
||||||
|
(`FUN_00476fc0`) lights the pod PANIC button; on glass the pad panel shades
|
||||||
|
the miniconsole Panic button from the same PadRIO lamp state
|
||||||
|
(L4PADPANEL:295, verified lit dark→bright on generator kill). NO gauge in
|
||||||
|
the shipped L4GAUGE.CFG rides mode 0x200000 — the eject indication is the
|
||||||
|
BUTTON LAMP, not an MFD element ("weapon-eng MFD eject lamp" reports = the
|
||||||
|
weapon page's UNJAM/EJECT ammo-jam indicator, a different feature). Lamp is
|
||||||
|
SOLID on arm (engine linked-lamp semantic); FLASH = the GaugeAlarm
|
||||||
|
`SetAlertState` overlay (RIO::flashFast), not authored for panic. Port fix:
|
||||||
|
per-frame `EvaluateEjectPermission()` at the top of Mech::PerformAndWatch
|
||||||
|
(master-gated) + `GetEjectPermitted()` accessor; `BT_EJECT_LOG=1` logs the
|
||||||
|
arm edges. Adjacent table fact: id 0x1a `DuckRequest@0049fa00` latches
|
||||||
|
mech+0x398 = 1 — that latch has NO reader anywhere in the image (vestigial).
|
||||||
|
- **THE TECHSTATUS / GAUGE-ALARM MODEL decoded 2026-08-04 [T1]:** the gauge-alarm
|
||||||
|
"condition" = a **STATUS-FLAG BIT INDEX** (0..6, TechStatusTypeCount). MechTech's
|
||||||
|
`TechnicalAssistance` edge-scans every monitored subsystem's `GetStatusFlags()`
|
||||||
|
(vtable +0x30) and on a bit flip calls `ReportStatusSet/Cleared(sink, owner, sub,
|
||||||
|
bitIndex, alarmModel)` → `GaugeAlarmManager::Activate(condition=bitIndex)` → the
|
||||||
|
SHARED 'mechalrm' stream's items matching that condition flash their lamps
|
||||||
|
(SetAlertState → RIO flashFast). The bit table, all byte-grounded overrides:
|
||||||
|
**bit 0** structure ≥ StatusThreshold (@004ac18c) | **bit 1** structure >
|
||||||
|
StatusFloor | **bit 2** coolantActive && HeatModelActive (CoolantLeaking) |
|
||||||
|
**bit 3** heatAlarm ≠ 0 | **bit 4** linked AmmoBin cookOffArmed (**AmmoBurning**)
|
||||||
|
| **bit 5** weaponAlarm == 5 (**Jammed**) | **bit 6** !HasVoltage (no usable
|
||||||
|
voltage; `HasVoltage` = source stateAlarm==2 Ready && |outputVoltage| > ε,
|
||||||
|
@004b0b5c). **Invite semantics:** conds 4/5 → gotoEngineering 0x80 + engEject
|
||||||
|
0x85 = the AMMO purge/unjam invite (flashes the very key whose streamed function
|
||||||
|
is EjectAmmo msg 0xB) — NEVER pilot eject; cond 6 → 0x80 + engBusMode 0x86 =
|
||||||
|
the bus-switch invite; cond 2 → the coolant loop/placement lamps (#98). The
|
||||||
|
PANIC LAMP (mode-linked, solid) is the SOLE pilot-eject indicator. **Electrical
|
||||||
|
subtlety [T1, @0049c9a8 + FUN_004b1f7c]:** the crit distributor touches nothing
|
||||||
|
electrical, and the generator sim has NO Ready-case recompute — a generator
|
||||||
|
destroyed IN PLACE keeps stale Ready voltage (no bit-6 invite) until any
|
||||||
|
transition (thermal trip → stateAlarm 4 GeneratorOut, short, switch-off)
|
||||||
|
recomputes output via `(1 − damage) × rated` and zeroes it. stateAlarm 4's
|
||||||
|
producer = the THERMAL BREAKER in GeneratorSimulation itself; a 2026-08-03
|
||||||
|
destruction→state-4 bridge was removed as unfounded.
|
||||||
|
- **DEATH SCORE COST decoded 2026-08-02 (#118 tail) [T1]:** the death handler tail
|
||||||
|
`@004c07cd-0x4c0828` (inside the @004c05c4 export gap — missing from the #52
|
||||||
|
reconstruction) gates on `advancedDamageOn`(+0x264) and hands the ENGINE base
|
||||||
|
`Player::ScoreMessageHandler` (`@0042da20` == PLAYER.cpp:138) a type-1 ScoreMessage
|
||||||
|
with `scoreAward = −ScenarioRole::specialCaseDeathPenalty` (role+0x20, the 4.10-only
|
||||||
|
key; restored to SCNROLE.h/.cpp — record slot [1] after KillBonus, fail-hard read).
|
||||||
|
Direct call, NOT dispatched — bypasses the BT handlers' type Verifies. NUANCE [T4]:
|
||||||
|
the 1995 engine adds at Player+0x1c8 while every BT scoreboard reads +0x278 — the
|
||||||
|
pod's death cost may never have displayed; our single-cell port shows it. Shipped
|
||||||
|
content authors NO role keys, so the cost is 0 in the field. Other role+0x1c reader:
|
||||||
|
`@0x4a0506` inside the deferred id-0x16 report tail (#45) reads killBonus.
|
||||||
|
- **THE SIM TIME MODEL — CLOSED 2026-08-02 (issue #96) [T1]:** the arcade
|
||||||
|
`Simulation::PerformAndWatch` is **`FUN_0041c018`** (part_002.c:5101):
|
||||||
|
`slice = (till.ticks − this[4].ticks) / DAT_0052140c; this[4] = till;
|
||||||
|
(*this[7])(this, slice)` then watchers (`FUN_0041c08c`) + WriteSimulationUpdate
|
||||||
|
(`FUN_0041bd98`) — **VARIABLE-STEP, slice in real SECONDS**, line-for-line the
|
||||||
|
WinTesla SIMULATE.cpp body. Corollaries: every Performance's `time_slice` is
|
||||||
|
seconds in BOTH binary and port (no hidden tick/second unit gap); a dt-LESS
|
||||||
|
per-frame term (myomer kinetic @4b8d18's middle term; the Mover gravity
|
||||||
|
`-= **(mover+0x250)` @0x421e77) fires at the FRAME cadence = the tick rate,
|
||||||
|
28 Hz nominal, sagging under load. Mover velocity is u/s (the replicant
|
||||||
|
dead-reckoner @0x421f7c does `pos += vel × ticksΔ/DAT_0052140c`). The myomer
|
||||||
|
heat term's ONE caller is the **unexported MyomersSimulation body
|
||||||
|
@0x4b8b9a-0x4b8d0d** (E8-scan + raw disasm; the export gap hid it): calls
|
||||||
|
@4b8d18 once per Perform, raw slice, gated `measuredVoltage > 0` only — plus
|
||||||
|
a −0.011/Perform repair trickle at alarm 1 and `speedEffect(+0x31c) =
|
||||||
|
AvailableOutput(min(measuredV, seek gear, cap)) / owner->runSpeedBase(+0x34c)`.
|
||||||
- **AMMO COOK-OFF cluster DECODED 2026-07-25 (issue #46, raw disasm `scratchpad/disammo.py`) [T1]:**
|
- **AMMO COOK-OFF cluster DECODED 2026-07-25 (issue #46, raw disasm `scratchpad/disammo.py`) [T1]:**
|
||||||
`@004bd394` AmmoBinSimulation (arm on heatAlarm FAILURE / detonate / cancel-on-Empty);
|
`@004bd394` AmmoBinSimulation (arm on heatAlarm FAILURE / detonate / cancel-on-Empty);
|
||||||
`@004bdb94` HandleMessage(1) = crit-induced arm; the FUSE = `Now().ticks +
|
`@004bdb94` HandleMessage(1) = crit-induced arm; the FUSE = `Now().ticks +
|
||||||
@@ -409,6 +547,13 @@ default-ON (`'0'` disables).
|
|||||||
| Var | Effect |
|
| Var | Effect |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `BT_FORCE_THROTTLE` | auto-walk forward (no key) |
|
| `BT_FORCE_THROTTLE` | auto-walk forward (no key) |
|
||||||
|
| `BT_BTNTEST=addr,on,off` / `BT_BTNTEST2=...` | scripted screen-button press/release cycles at poll counts (the REAL RIO seam; 2nd cycle for e.g. squat→rise) |
|
||||||
|
| `BT_DUCK_LOG` | crouch diagnostics: RIO press mode-mask, SetLegAnimation re-arm tracer (+ra), 1 Hz joint probe, posture/squat events |
|
||||||
|
| `BT_TREE_LOG` | dump the built render-tree topology (segment/joint/parent, SITE rows) at mech build |
|
||||||
|
| `BT_SPOT_SELF` | bench-only: build the searchlight cone on the OWN-cockpit tree so `BT_CAM=face` can eyeball it solo |
|
||||||
|
| `BT_CAM=face` | chase camera in FRONT looking back (the old animation view); `BT_CAM_Y/Z` offsets |
|
||||||
|
| `BT_FOG_LOG` | log SetFogStyle transitions + fog page resolution (searchlight swap forensics) |
|
||||||
|
| `BT_GIMP_SPEED` | override the gimp-gait demand scale (1.0 = authentic) |
|
||||||
| `BT_SPAWN_ENEMY` | spawn a target mech 120u ahead along the spawn facing |
|
| `BT_SPAWN_ENEMY` | spawn a target mech 120u ahead along the spawn facing |
|
||||||
| `BT_AUTOFIRE=1` | hold the trigger (headless walk→fire→death harness; supersedes the dead `BT_FORCE_FIRE` — btl4main.cpp:310 set `fireForced` once at startup, but mech4.cpp:1567 unconditionally overwrites it every frame) |
|
| `BT_AUTOFIRE=1` | hold the trigger (headless walk→fire→death harness; supersedes the dead `BT_FORCE_FIRE` — btl4main.cpp:310 set `fireForced` once at startup, but mech4.cpp:1567 unconditionally overwrites it every frame) |
|
||||||
| `BT_ASSERT_TO_DEBUGGER` | route CRT asserts to the debugger, not a modal box |
|
| `BT_ASSERT_TO_DEBUGGER` | route CRT asserts to the debugger, not a modal box |
|
||||||
@@ -431,7 +576,7 @@ default-ON (`'0'` disables).
|
|||||||
| `BT_AIM="x y"` | pin the reticle crosshair (reticle coords) — headless aim harness |
|
| `BT_AIM="x y"` | pin the reticle crosshair (reticle coords) — headless aim harness |
|
||||||
| `BT_AIM_LOG` | `[pick]` ray/box/hit diagnostics (Mech::PickRayHit) |
|
| `BT_AIM_LOG` | `[pick]` ray/box/hit diagnostics (Mech::PickRayHit) |
|
||||||
| `BT_FIRE_ARC=<deg>` | OPT-IN external-camera fire-arc clamp (unset = authentic no-arc) |
|
| `BT_FIRE_ARC=<deg>` | OPT-IN external-camera fire-arc clamp (unset = authentic no-arc) |
|
||||||
| `BT_START_INSIDE` | begin in the cockpit view (V toggles) |
|
| `BT_START_INSIDE` | begin in the cockpit view (BACKTICK toggles; 'V' = the pod rear-view hold since #68 — the toggle skips bound keys, L4PADRIO.cpp:681) |
|
||||||
| `BT_FORCE_MODEL=<name>` | force the player mech (`madcat`/`owens`/…; btl4mssn.cpp — per-mech cockpit bring-up) |
|
| `BT_FORCE_MODEL=<name>` | force the player mech (`madcat`/`owens`/…; btl4mssn.cpp — per-mech cockpit bring-up) |
|
||||||
| `BT_HIDE_COCKPIT` | hide the `*_cop` canopy shell (SHOWS by default — see [[cockpit-view]]) |
|
| `BT_HIDE_COCKPIT` | hide the `*_cop` canopy shell (SHOWS by default — see [[cockpit-view]]) |
|
||||||
| `BT_COP_*` / `BT_EYE_FWD=<f>` | cockpit canopy + eye diagnostics (`FRAME` (unlit frame colour override, default 0.13,0.12,0.15) / `SINGLE` (single-sided diag; double-sided is default) / `PLATES=1` (disable the punch stencil-cut kit) / `FLIP` / `DEBUG` / `DUMP`; eye forward-push probe) — [[cockpit-view]] |
|
| `BT_COP_*` / `BT_EYE_FWD=<f>` | cockpit canopy + eye diagnostics (`FRAME` (unlit frame colour override, default 0.13,0.12,0.15) / `SINGLE` (single-sided diag; double-sided is default) / `PLATES=1` (disable the punch stencil-cut kit) / `FLIP` / `DEBUG` / `DUMP`; eye forward-push probe) — [[cockpit-view]] |
|
||||||
@@ -501,7 +646,9 @@ ex-"clipLoadGuard"). Byte tooling: `scratchpad/dis_4b2980.py` / `dis_range.py`
|
|||||||
`reference/decomp/section_dump.txt`.
|
`reference/decomp/section_dump.txt`.
|
||||||
|
|
||||||
**ARMOUR-DARKENING addresses** [T1] (#87; full story: [[rendering]]): `.DZM` res compiler
|
**ARMOUR-DARKENING addresses** [T1] (#87; full story: [[rendering]]): `.DZM` res compiler
|
||||||
`FUN_0041e4e0` (part_002.c:7341, reads the `material` key list per `[dz_*]` section); runtime
|
address UNKNOWN — the old citation `FUN_0041e4e0` was WRONG (that function, part_002.c:7337,
|
||||||
|
is `DamageZone::TakeDamage`: `level += amount×scale[type]`, byte-matches DAMAGE.cpp:379 —
|
||||||
|
verified 2026-08-02, swept here + [[rendering]]); runtime
|
||||||
consumer inside `BTL4VideoRenderer::MakeMechRenderables` **`FUN_004cef28`** (part_014.c:5190-5272);
|
consumer inside `BTL4VideoRenderer::MakeMechRenderables` **`FUN_004cef28`** (part_014.c:5190-5272);
|
||||||
per-zone node ctor `FUN_00455c7c(...,0.04f)`; the per-material watcher
|
per-zone node ctor `FUN_00455c7c(...,0.04f)`; the per-material watcher
|
||||||
**`FUN_004573e4(material, &zone->damageLevel, 0.1f)`** (part_007.c:9133, vtable `PTR_LAB_004f1730`,
|
**`FUN_004573e4(material, &zone->damageLevel, 0.1f)`** (part_007.c:9133, vtable `PTR_LAB_004f1730`,
|
||||||
@@ -538,6 +685,22 @@ Respawn SIM path: `DropZoneReply` `FUN_004bffd0` → `Mech::Reset` @0x4009fb74;
|
|||||||
---
|
---
|
||||||
|
|
||||||
## 7. Tools
|
## 7. Tools
|
||||||
|
- ⚠ **CITATION POLICY (since the 2026-08-06 re-export): cite `@ADDR`, never `part_0NN.c:LINE`.**
|
||||||
|
Addresses are stable across exports; shard membership and line numbers are NOT. Pre-08-06
|
||||||
|
part/line citations resolve only against `reference/decomp/archive_2025export/`.
|
||||||
|
- ⚠ **Ghidra renders member access as int-ARRAY indices** (`param_1[0xfe]` == byte offset 0x3f8,
|
||||||
|
`[0x1e7]` == 0x79c): grepping the export for an offset STRING misses every such site. Divide
|
||||||
|
by 4 (or grep both forms) — this is a live instance of gotcha §20.
|
||||||
|
- `tools/gapcensus.py [dir]` — the #60 dark-region census: index/export coverage, function-start
|
||||||
|
discovery, TU attribution, repo-citation flags → `<dir>/GAP_CENSUS.md` + `gap_census.tsv`.
|
||||||
|
Consult BEFORE any "absent from the export" claim (gotcha §20).
|
||||||
|
- `tools/ghidra_reexport.sh [reprocess]` — headless re-export: Ghidra 12.1.2 + JDK 21 (installed
|
||||||
|
in `%LOCALAPPDATA%\bt411-tools` beside DXSDK/cmake) run `reference/ghidra_scripts/ExportGaps.java`
|
||||||
|
(ExportAll's output contract + the dark-region gap fill). ⚠ the runner MUST use 8.3 SHORT paths
|
||||||
|
(`JOELAP~1`, `BT411-~1`): Ghidra's launcher .bat expands `%JAVA_HOME%` UNQUOTED and the user
|
||||||
|
profile has a space. `reprocess` re-runs the script on the saved project (skips re-analysis).
|
||||||
|
- `tools/gapdiff.py [old] [new]` — score two censused exports (coverage/dark deltas, which
|
||||||
|
previously-dark addresses the KB already cites, celebrity check).
|
||||||
|
|
||||||
- `tools/disas2.py <VA> [len]` — capstone disassembly of BTL4OPT.EXE at a VA (recovers x87 math
|
- `tools/disas2.py <VA> [len]` — capstone disassembly of BTL4OPT.EXE at a VA (recovers x87 math
|
||||||
Ghidra drops; folds known call targets + float constants). THE tool for any `FUN_` the
|
Ghidra drops; folds known call targets + float constants). THE tool for any `FUN_` the
|
||||||
|
|||||||
@@ -42,7 +42,9 @@ then unconditionally `+0x264 = +0x268 = mission->advancedDamageOn(+0xf0)` (both
|
|||||||
|
|
||||||
## What each flag gates (binary consumers, all reached via `mech+0x190` → BTPlayer) [T1]
|
## What each flag gates (binary consumers, all reached via `mech+0x190` → BTPlayer) [T1]
|
||||||
|
|
||||||
- **+0x25c — "sim live", off only for novice.** Consumers: ballistic jam roll `CheckForJam`
|
- **+0x25c — "sim live", off only for novice.** Consumers: the CROUCH posture selector
|
||||||
|
(master-perf @0x4a9f70: novice → mapPosture 0 → squat/rise refused — 2026-08-06,
|
||||||
|
[[locomotion]] §CROUCH); ballistic jam roll `CheckForJam`
|
||||||
@4bbfcc early-returns NO-JAM when 0 (projweap.cpp calls this `LiveFireEnabled`); **ThermalSight**
|
@4bbfcc early-returns NO-JAM when 0 (projweap.cpp calls this `LiveFireEnabled`); **ThermalSight**
|
||||||
`ToggleLamp` @4b860c (thermalsight.hpp `ControlsAllowLights`) — ⚠ **CORRECTED 2026-07-25 (#61):
|
`ToggleLamp` @4b860c (thermalsight.hpp `ControlsAllowLights`) — ⚠ **CORRECTED 2026-07-25 (#61):
|
||||||
this was listed as *Searchlight's* gate. It is not. Raw disassembly of Searchlight's real handler
|
this was listed as *Searchlight's* gate. It is not. Raw disassembly of Searchlight's real handler
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ open_questions:
|
|||||||
- "SeekVoltageGraph RECONSTRUCTED 2026-07-19 (Gitea #11, was #10 finding A): full widget landed (see §SeekVoltageGraph below) -- ghosts gone steady-state (BT_PRESET_HOLD verification); remaining polish: a 1-frame transition artifact when a BT_SHOT lands on the exact page-switch frame (label BecameActive vs the graph's next rated Execute -- same lag class as the binary; self-heals next frame)"
|
- "SeekVoltageGraph RECONSTRUCTED 2026-07-19 (Gitea #11, was #10 finding A): full widget landed (see §SeekVoltageGraph below) -- ghosts gone steady-state (BT_PRESET_HOLD verification); remaining polish: a 1-frame transition artifact when a BT_SHOT lands on the exact page-switch frame (label BecameActive vs the graph's next rated Execute -- same lag class as the binary; self-heals next frame)"
|
||||||
- "Secondary-view cycling RESOLVED 2026-07-19 (Gitea #6): the selector is the DISPLAY mode (CycleDisplayMode -> vtbl+0x4C override @4d1ae4), NOT CycleControlMode; desktop 'N' / pad RightThumb wired; pixel-verified dama->crit->heat"
|
- "Secondary-view cycling RESOLVED 2026-07-19 (Gitea #6): the selector is the DISPLAY mode (CycleDisplayMode -> vtbl+0x4C override @4d1ae4), NOT CycleControlMode; desktop 'N' / pad RightThumb wired; pixel-verified dama->crit->heat"
|
||||||
- "Upper-MFD PRESET pages RESOLVED 2026-07-19 (Gitea #9): SetPresetMode table @0051dbf0 re-decoded (little-endian -> ModeMFD bits 0-14), per-MFD pod button banks identified from the .CTL dump, desktop J/K/L cycle wired"
|
- "Upper-MFD PRESET pages RESOLVED 2026-07-19 (Gitea #9): SetPresetMode table @0051dbf0 re-decoded (little-endian -> ModeMFD bits 0-14), per-MFD pod button banks identified from the .CTL dump, desktop J/K/L cycle wired"
|
||||||
- "Always-active msg-4 records IDENTIFIED 2026-07-20 (glass input audit): 0x2C = Reservoir InjectCoolant (the flush button), 0x2F/0x2E/0x2D/0x2B/0x2A/0x29 = Condenser1-6 MoveValve, 0x1A-0x1D = GeneratorA-D ToggleGeneratorOnOff (@0050fb90, unreconstructed); plus 0x13 = Mech DuckRequest (crouch), 0x28 = Mech BalanceCoolant, 0x12/0x14 = ThermalSight/Searchlight toggles -- see pod-hardware.md + docs/GLASS_COCKPIT.md 2026-07-20"
|
- "Always-active msg-4 records IDENTIFIED 2026-07-20 (glass input audit): 0x2C = Reservoir InjectCoolant (the flush button), 0x2F/0x2E/0x2D/0x2B/0x2A/0x29 = Condenser1-6 MoveValve, 0x1A-0x1D = GeneratorA-D ToggleGeneratorOnOff (@0050fb90; wired 2026-07-25, powersub.cpp); plus 0x13 = Mech DuckRequest (CROUCH -- COMPLETE 2026-08-06, [[locomotion]]), 0x28 = Mech BalanceCoolant, 0x12/0x14 = ThermalSight/Searchlight toggles (searchlight visuals done 2026-08-05) -- see pod-hardware.md + docs/GLASS_COCKPIT.md; statuses re-swept 2026-08-06"
|
||||||
- "MP DEATHS resolved 2026-07-12 (observed-death tally + display clamp); remaining: verify multi-death tallies stay in sync across a long session (GAUGE_COMPOSITE.md)"
|
- "MP DEATHS resolved 2026-07-12 (observed-death tally + display clamp); remaining: verify multi-death tallies stay in sync across a long session (GAUGE_COMPOSITE.md)"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ that one answer. Precedence, highest first:
|
|||||||
| Env | Layout | Buttons live in |
|
| Env | Layout | Buttons live in |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `BT_GLASS_PANELS`≠0 | per-display cockpit windows (the "exploded" view, `L4GLASSWIN`) | each display's own window |
|
| `BT_GLASS_PANELS`≠0 | per-display cockpit windows (the "exploded" view, `L4GLASSWIN`) | each display's own window |
|
||||||
|
| `BT_POD_SURFACES`=1 | **POD MODE** (2026-08-06): crop each panel window to its SURFACE (MFD 640×480, radar 480×640), drop the on-screen button banks + the Flight Controls pad, frameless. For REAL pod glass, where the buttons are physical — see [[pod-hardware]] §MFD PANELS. Per-window: `,bare` in `glass_layout.cfg`. | 6 bare surfaces |
|
||||||
| `BT_DEV_GAUGES_WINDOW` | the legacy separate MFD window | the single combined pad panel |
|
| `BT_DEV_GAUGES_WINDOW` | the legacy separate MFD window | the single combined pad panel |
|
||||||
| `BT_DEV_GAUGES_DOCK` | the docked bottom gauge strip | the single combined pad panel |
|
| `BT_DEV_GAUGES_DOCK` | the docked bottom gauge strip | the single combined pad panel |
|
||||||
| `BT_COCKPIT=0` | ...also the docked strip (the documented opt-out) | the single combined pad panel |
|
| `BT_COCKPIT=0` | ...also the docked strip (the documented opt-out) | the single combined pad panel |
|
||||||
@@ -155,6 +156,26 @@ want bare. From then on those windows are frameless **and pinned** — with no c
|
|||||||
nothing to drag them by, which is the point: the arrangement is finished. Delete the flag to get
|
nothing to drag them by, which is the point: the arrangement is finished. Delete the flag to get
|
||||||
the frame back.
|
the frame back.
|
||||||
|
|
||||||
|
### `monitor:<name|index>` — bind a window to a DISPLAY, not to pixels (2026-08-06) [T2 live]
|
||||||
|
|
||||||
|
Instead of an `x,y,w,h` rect, a line may name a display device:
|
||||||
|
|
||||||
|
```
|
||||||
|
Secondary / Radar=monitor:DISPLAY4,bare
|
||||||
|
VGA Port A=monitor:DISPLAY2,bare
|
||||||
|
```
|
||||||
|
|
||||||
|
The resolver (`ResolveMonitorSpec` + `MonScanProc`, `L4GLASSWIN`) matches the full device name OR
|
||||||
|
its tail, so `monitor:DISPLAY4` works without typing `\\.\`; a bare integer is a 0-based index in
|
||||||
|
enumeration order. The surface is CENTRED on that monitor. This is what makes a fixed rig
|
||||||
|
survivable: **desktop rects move** when a display is re-detected, a USB graphics adapter
|
||||||
|
re-enumerates or someone changes a resolution, and a pixel rect then silently puts a panel on the
|
||||||
|
wrong glass — a device binding does not. Required for [[pod-hardware]] §ALPHA-MR, where a
|
||||||
|
mis-bound window means a picture on the wrong CRT with no error anywhere.
|
||||||
|
|
||||||
|
`,bare` **implies frameless** (it used to need `,noframe` too, and a "bare" window came back
|
||||||
|
656×519 WITH a caption — a real bug found on the cab).
|
||||||
|
|
||||||
Options are comma-separated after the numbers and **unknown ones are ignored**, so an older build
|
Options are comma-separated after the numbers and **unknown ones are ignored**, so an older build
|
||||||
reading a newer file loses the option but never the line — the `bindings.txt` grammar rule
|
reading a newer file loses the option but never the line — the `bindings.txt` grammar rule
|
||||||
applied here.
|
applied here.
|
||||||
@@ -182,6 +203,15 @@ appears in the cfg like any panel and honours `,noframe`. Verified: a drag wrote
|
|||||||
at 321,222. NB the plasma window blits directly every frame (`GetDC`+`StretchDIBits`), so unlike
|
at 321,222. NB the plasma window blits directly every frame (`GetDC`+`StretchDIBits`), so unlike
|
||||||
the panels it has no `WM_TIMER` focus-throttle to worry about.
|
the panels it has no `WM_TIMER` focus-throttle to worry about.
|
||||||
|
|
||||||
|
**Turning it OFF: `L4PLASMA=NONE` (also `OFF`/`0`, 2026-08-06) [T2].** `L4GREND` creates a
|
||||||
|
marquee whenever `L4PLASMA` is set at all (`SCREEN` → the desktop window, anything else → a real
|
||||||
|
`PlasmaDisplay` on that serial port), and the GLASS profile force-defaults it to `SCREEN` — so on
|
||||||
|
a rig with no marquee wired, leaving the variable unset is NOT enough; the profile fills it back
|
||||||
|
in and a plasma window lands on the cab's glass. The `NONE`/`OFF`/`0` spelling nulls the string
|
||||||
|
before the branch, so no external display is constructed at all. The boot banner reads
|
||||||
|
`GLASS (PadRIO; plasma off [L4PLASMA])` when it takes, and the absence of the
|
||||||
|
`[plasmawin] desktop plasma display up` line is the runtime proof.
|
||||||
|
|
||||||
**Verified 2026-07-26 [T2]:** `BT_RIOBANK_LOG=1` dumps every bank's rects;
|
**Verified 2026-07-26 [T2]:** `BT_RIOBANK_LOG=1` dumps every bank's rects;
|
||||||
`scratchpad/checkbank.py` reports the per-bank census and proves no address is SHADOWED (has a
|
`scratchpad/checkbank.py` reports the per-bank census and proves no address is SHADOWED (has a
|
||||||
point no earlier button covers); `scratchpad/clickbank.py` then posts a real click at every
|
point no earlier button covers); `scratchpad/clickbank.py` then posts a real click at every
|
||||||
@@ -213,7 +243,10 @@ window.
|
|||||||
- **World aspect** under the letterbox is the view rect's OWN aspect — the client no longer
|
- **World aspect** under the letterbox is the view rect's OWN aspect — the client no longer
|
||||||
enters into it.
|
enters into it.
|
||||||
- **`-fit`** (alias `-windowed-fullscreen`): borderless `WS_POPUP` over the monitor, canvas
|
- **`-fit`** (alias `-windowed-fullscreen`): borderless `WS_POPUP` over the monitor, canvas
|
||||||
letterboxed inside. Verified on a 3440×1440 ultrawide.
|
letterboxed inside. Verified on a 3440×1440 ultrawide. **`BT_FIT=1` is the env spelling**
|
||||||
|
(2026-08-06) — same flag, settable from `environ.ini`, so a fixed rig keeps its borderless main
|
||||||
|
view however it is launched instead of depending on one launcher's command line. Targets
|
||||||
|
`MONITOR_DEFAULTTOPRIMARY`, i.e. the PRIMARY display. [[pod-hardware]] §ALPHA-MR
|
||||||
|
|
||||||
⚠ **Ordering trap found live:** the letterbox flag was first set at device creation, but the
|
⚠ **Ordering trap found live:** the letterbox flag was first set at device creation, but the
|
||||||
first `WM_SIZE` arrives BEFORE the device exists — a `-fit` boot logged `aspect=3.14` (the
|
first `WM_SIZE` arrives BEFORE the device exists — a `-fit` boot logged `aspect=3.14` (the
|
||||||
|
|||||||
@@ -311,6 +311,48 @@ resolved with gait v5, no shadow code changed — though (a) may have contribute
|
|||||||
T4]; (c) genuine slope burial when the quad is flat/mis-tilted (the depth-bias margin is finite).
|
T4]; (c) genuine slope burial when the quad is flat/mis-tilted (the depth-bias margin is finite).
|
||||||
Check `[shadowobj]` tag lines and `[sync]`/`BT_SYNC_LOG` before touching bias/tilt.
|
Check `[shadowobj]` tag lines and `[sync]`/`BT_SYNC_LOG` before touching bias/tilt.
|
||||||
|
|
||||||
|
## CROUCH -- COMPLETE 2026-08-06 [T1 decode / T2 full-cycle + MP benched]
|
||||||
|
The manual's CROUCH button (streamed 0x13; glass key F4), end to end: DuckRequest (mech id 0x1a
|
||||||
|
@0049fa00) latches duckState@0x398 -> the master posture block (mech4, from the dark master-perf
|
||||||
|
@0x4a9cf0-0x4aa0af): posture selector @0x3f8 (mode 9/10/2/3/4 block, NOVICE simLive lockout,
|
||||||
|
myomer-alive gate) -> standing: SetLegAnimation(2) ('sqd', squat down; LegClipFinished case-2
|
||||||
|
PARKS the pose + leg alarm 1) / ducked: SetLegAnimation(3) ('squ', rise; case-3 alarm 0);
|
||||||
|
stability alarm flips; ForceUpdate(8)+(1) ships the type-3 state record -- peers pose the squat
|
||||||
|
for FREE (screenshot receipt: the crouched crimson madcat from the observer's cockpit,
|
||||||
|
duckmpA_031). Clip data verified from BTL4.RES (root joint drops -2.22, knees +-1.14; squ/sqd
|
||||||
|
are exact mirrors; interior 'squi/sqdi' variants ship for all 8 chassis). Full solo cycle
|
||||||
|
benched (BT_BTNTEST + BT_BTNTEST2): squat -> hold (joints steady for minutes) -> rise ->
|
||||||
|
standing zeros. Myomer factor: LIVE, not deferred -- the 2026-08-05 "feeder unreconstructed" claim was an
|
||||||
|
export-gap-blind grep (the members are NAMED: speedEffect@0x31C, myomers.cpp; the chain walk +
|
||||||
|
speedDemand scale + turn freeze = mechmppr.cpp:990, the 2026-07-31 seek audit, same bytes).
|
||||||
|
mechmppr now publishes the chain MAX into mech->myomerEffectiveness (the binary's +0x79C home)
|
||||||
|
so the crouch gate reads the live value: dead/overheated myomers (the Oracle seek-4 freeze)
|
||||||
|
refuse the squat AND the rise.
|
||||||
|
The pilot's EYE drops too -- the task-#15 bob channel (gBTEyeBobY <- jointlocal.y each frame)
|
||||||
|
carries the parked -2.22 into DPLEyeRenderable (log receipt bob=-2.2187 through the hold; frame
|
||||||
|
receipt duckcp_030 = grass-level view). A false "eye does not drop" residual was briefly filed
|
||||||
|
2026-08-06 -- measurement error (canopy-dominant diff crops; the canopy drops WITH the eye, so
|
||||||
|
the interior looks unchanged and only the through-window ground shifts). Bench traps for reruns: MP button delivery can miss if pressed
|
||||||
|
during round-start jitter (press >= poll 900 after GO); diags all BT_DUCK_LOG-gated.
|
||||||
|
|
||||||
|
### CROUCH addendum -- the #60 re-export pass (2026-08-06) [T1]
|
||||||
|
The rebuilt export (93.5% coverage) produced pseudocode for the master-perf region this block
|
||||||
|
was hand-disassembled from, and it **confirms the transcription field for field**:
|
||||||
|
`param_1[0xfe]`=mapPosture(0x3f8), `[0xe6]`=duckState(0x398), `[0x161]`=squatCapable,
|
||||||
|
`[0x1e7]`=myomerEffectiveness(0x79c), `[0x10]`=movementMode, `player+0x25c` novice gate,
|
||||||
|
`FUN_004a7fc4(2|3)`=SetLegAnimation sqd/squ, `FUN_004a4c54(8)+(1)`=ForceUpdate, `+0x131`
|
||||||
|
stability alarm, duckState cleared after. (Ghidra renders these as int-ARRAY indices --
|
||||||
|
`[0xfe]` == byte 0x3f8 -- which is why an offset-string grep of the export misses them; see
|
||||||
|
[[reconstruction-gotchas]] §20.)
|
||||||
|
|
||||||
|
**It also caught a branch the raw pass missed** (the hand disassembly stopped at `0x4aa0af`):
|
||||||
|
an **AIRBORNE AUTO-RISE** immediately after the duck consumer --
|
||||||
|
`if ((movementMode == 3 || movementMode == 4) && legState == 1) -> SetLegAnimation(3) +
|
||||||
|
ForceUpdate(8)+(1) + stability 1`. A mech that leaves the ground (jump/knockback) while PARKED
|
||||||
|
in the squat is forced upright; unconditional on duckState/squatCapable, i.e. a safety
|
||||||
|
un-crouch rather than a request. Implemented in mech4.cpp the same day; crouch re-benched
|
||||||
|
un-regressed (squat still holds on flat ground, no spurious rises).
|
||||||
|
|
||||||
## Key Relationships
|
## Key Relationships
|
||||||
- Detail: `docs/P3_LOCOMOTION.md`. Uses: [[asset-formats]] (SKL/ANI), [[decomp-reference]] (offsets).
|
- Detail: `docs/P3_LOCOMOTION.md`. Uses: [[asset-formats]] (SKL/ANI), [[decomp-reference]] (offsets).
|
||||||
- Feeds: [[combat-damage]] (collision→damage), [[rendering]] (shadow/visual-conform).
|
- Feeds: [[combat-damage]] (collision→damage), [[rendering]] (shadow/visual-conform).
|
||||||
|
|||||||
+14
-2
@@ -70,6 +70,13 @@ emulator** (⚠ `NotationFile::ReadText` expects NUL-SEPARATED lines). [T2]
|
|||||||
console LAUNCH). SIX bugs fixed to get here (dead-reckoner install, replicant-motion DeadReckon,
|
console LAUNCH). SIX bugs fixed to get here (dead-reckoner install, replicant-motion DeadReckon,
|
||||||
master emission threshold, emission gated on RunningMission, the console-must-LAUNCH fact, replicant
|
master emission threshold, emission gated on RunningMission, the console-must-LAUNCH fact, replicant
|
||||||
validity). [T2]
|
validity). [T2]
|
||||||
|
- **Zone-level replication (#87, 2026-08-03):** an observer's copy of a mech learns zone damageLevels
|
||||||
|
ONLY from zone update records, and the master sends those on BAND-THRESHOLD CROSSINGS (binary effect
|
||||||
|
watcher `FUN_0042aa2c`: `DescriptorCrossed` + master gate → `ForceUpdate(DamageZoneUpdateModelFlag)`)
|
||||||
|
plus every graphic-state change — never per hit. Damage messages do NOT echo locally (replicant
|
||||||
|
`Dispatch` forwards to the master and returns). The port had only the gstate branch (observers saw
|
||||||
|
0.0 until destruction); the level branch was restored + 2-node verified (replicant peaks == master
|
||||||
|
finals to 4 decimals). Full mechanism: [[combat-damage]] §Zone-LEVEL replication. [T1/T2]
|
||||||
- **Wire-format bug class found+fixed:** MakeMessages replicate RAW over TCP, so string payload must be
|
- **Wire-format bug class found+fixed:** MakeMessages replicate RAW over TCP, so string payload must be
|
||||||
INLINE (`char[N]` at the binary offsets), not a `const char*` pointer (garbage cross-pod). Check
|
INLINE (`char[N]` at the binary offsets), not a `const char*` pointer (garbage cross-pod). Check
|
||||||
EVERY MakeMessage for pointer payloads. [T2]
|
EVERY MakeMessage for pointer payloads. [T2]
|
||||||
@@ -874,8 +881,13 @@ transition, HUD all landed since P6): console egg → mesh → RunningMission on
|
|||||||
REMAINING note is closed.
|
REMAINING note is closed.
|
||||||
5. ✅ **2-window driving — RESOLVED (tasks #48/#51).** Keyboard MP turning was restored at the root
|
5. ✅ **2-window driving — RESOLVED (tasks #48/#51).** Keyboard MP turning was restored at the root
|
||||||
(the `pilotArray[1]` shrunk-span overrun fix, task #51); `BT_KEY_NOFOCUS` exists for automated
|
(the `pilotArray[1]` shrunk-span overrun fix, task #51); `BT_KEY_NOFOCUS` exists for automated
|
||||||
harnesses. DEATHS scoring lights via the existing BTPostKillScore MP branch. Respawn is
|
harnesses. DEATHS scoring lights via the kill/death report flow (2026-08-05: now the AUTHENTIC
|
||||||
reconstructed (task #52 — see item 3b).
|
report tail in the victim's TakeDamage handler — BTMechPostCombatReports, replacing
|
||||||
|
BTPostKillScore; same cross-node reroute, see [[combat-damage]] §report tail). Respawn is
|
||||||
|
reconstructed (task #52 — see item 3b; the VehicleDead sender moved to the same tail).
|
||||||
|
CROUCH and the SEARCHLIGHT replicate with ZERO new wire code (2026-08-05/06): the squat rides
|
||||||
|
the existing type-3 state record (legState) and the lamp rides subsystem record 0x14
|
||||||
|
(lightState) — [[locomotion]] §CROUCH / [[rendering]] §SEARCHLIGHT.
|
||||||
|
|
||||||
## Key Relationships
|
## Key Relationships
|
||||||
- Base: [[wintesla-port]] (L4NET). Depends on: [[locomotion]] (update writer), [[combat-damage]]
|
- Base: [[wintesla-port]] (L4NET). Depends on: [[locomotion]] (update writer), [[combat-damage]]
|
||||||
|
|||||||
+102
-19
@@ -22,6 +22,20 @@ authentic path scoped.
|
|||||||
further assets exist; the runtime `BTL4.RES` with 8 maps + full anim set is the master we have).
|
further assets exist; the runtime `BTL4.RES` with 8 maps + full anim set is the master we have).
|
||||||
- Pod specifics for Phase 8: the 7-monitor driver setup, the RIO cockpit I/O protocol, current
|
- Pod specifics for Phase 8: the 7-monitor driver setup, the RIO cockpit I/O protocol, current
|
||||||
Win10+wrapper pod config. Status: OPEN.
|
Win10+wrapper pod config. Status: OPEN.
|
||||||
|
- **The i860 DIVISION-CARD firmware / DIV render docs (the material→pixel response curve).**
|
||||||
|
Established 2026-08-03 (#87 dig): the HOST binary lerps 13 material colour floats linearly
|
||||||
|
toward ×0.1 by zone damageLevel (watcher @004573e4/@00457784, 0.1 literal at the
|
||||||
|
MakeMechRenderables call site) and then just **marshals the material to the card**
|
||||||
|
(`FUN_0048d4d4` = "flush_material", dpl command opcode 100 — siblings flush_ramp/flush_texture).
|
||||||
|
The card-side shading (how material colour selected/scaled the 256-row material ramps → final
|
||||||
|
texels) is i860 firmware, NOT in BTL4OPT.EXE — so the PERCEPTUAL damage-darkening curve of the
|
||||||
|
1995 pods cannot be recovered from this binary. Our port applies the host's linear factor at
|
||||||
|
draw (TFACTOR modulate, L4D3D). Field testimony ("a single missile leaves visible armor
|
||||||
|
damage") is arithmetically consistent with the linear curve — one 25-pt hit snaps a 68-90-pt
|
||||||
|
zone by 0.28-0.37 (measured live: amt=25 → rarm 0→0.3247) — but if the card's ramp mapping was
|
||||||
|
steeper than linear, mid-level creep damage read stronger on the pods than in the port.
|
||||||
|
ASK: div-card firmware image, DIV/dpl render docs, or period screenshots showing a
|
||||||
|
known-damage-level mech. Status: OPEN.
|
||||||
- **Did the pod throttle quadrant have MECHANICAL detents** (how many / lever positions)? The
|
- **Did the pod throttle quadrant have MECHANICAL detents** (how many / lever positions)? The
|
||||||
software path is notch-free [T1, [[pod-hardware]]], but the gait SM's walk/run dead band
|
software path is notch-free [T1, [[pod-hardware]]], but the gait SM's walk/run dead band
|
||||||
([[locomotion]]) only makes sense if the hardware discouraged parking the lever mid-band —
|
([[locomotion]]) only makes sense if the hardware discouraged parking the lever mid-band —
|
||||||
@@ -210,16 +224,22 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR
|
|||||||
⚠ This entry was stale in BOTH directions for weeks (`docs/INPUT_PATH_AUDIT.md` flagged it) —
|
⚠ This entry was stale in BOTH directions for weeks (`docs/INPUT_PATH_AUDIT.md` flagged it) —
|
||||||
most of the list had been reconstructed while the prose still called it dead. Check the code,
|
most of the list had been reconstructed while the prose still called it dead. Check the code,
|
||||||
not the census, before reconstructing anything here.
|
not the census, before reconstructing anything here.
|
||||||
- **`DuckState` has no CODE consumer, and that is authentic [T1].** `Mech::DuckRequest` sets
|
- **❌ RETRACTED (2026-08-06): "`DuckState` has no CODE consumer, and that is authentic [T1]".**
|
||||||
`duckState`(attr 0x37, binary `mech+0x398`) to 1 and nothing else: the flag has exactly two
|
That verdict — and its corollary "no SQUAT clip survives; do not invent a crouch pose" — was
|
||||||
writers in the whole binary (that handler and the mech reset) and ZERO readers. Its consumer is
|
EXPORT-GAP BLINDNESS ([[reconstruction-gotchas]] §20, incident 2), and the [T1] tag was
|
||||||
a DATABINDING — `content/GAUGE/L4GAUGE.CFG` drives a 3-frame `bduck.pcc` widget off `DuckState`
|
unearned: "ZERO readers" was true of the *export*, not the binary. The reader is the master-perf
|
||||||
on the map's legend column, verified live (the crouch icon lights grey→orange on a press). So
|
posture/duck block in the un-exported region (@0x4aa011-0x4aa0af, raw disasm), and the squat
|
||||||
the button is COMPLETE as a request flag + indicator. What is NOT known: whether the 1995 game
|
clips DID ship — `<pfx>squ/sqd` + interior `squi/sqdi` for all 8 chassis live in **BTL4.RES**
|
||||||
ever consumed it for posture/collision (no SQUAT clip name survives in the decomp or in
|
(the old claim searched decomp strings + loose `content/` files, never the RES TOC;
|
||||||
`content/`, only `DuckServo01.wav` in AUDIO1.RES). Do not invent a crouch pose to "finish" it.
|
`DuckServo01.wav` was the tell). CROUCH is now fully reconstructed — request latch → posture
|
||||||
- **Searchlight-driven fog swap — STILL DEFERRED, but the "ORIGINAL 1995 LATENT BUG" premise is
|
arbiter (novice + myomer + mode gates) → squat/hold/rise, MP-replicated on the type-3 record
|
||||||
❌ RETRACTED (2026-07-25, #61).** The arcade swaps fog between `fog=` (lights on) and
|
([[locomotion]] §CROUCH). The `bduck.pcc` legend databinding observation stands and still works.
|
||||||
|
- **✅ Searchlight-driven fog swap — DONE 2026-08-05 (412053d/b75bb4a; [[rendering]] §SEARCHLIGHT).**
|
||||||
|
The port's `TickSearchlight` (btl4vid) is the `PullFogRenderable` equivalent: LightOn →
|
||||||
|
`SetFogStyle(searchLightOn/OffFogStyle)` with the binary's inverted-cache seed (authentic DARK
|
||||||
|
night start), plus the external spot.bgf beam cone (btfx `brighten` additive veil) on the
|
||||||
|
searchlight site joint, MP-replicated. Historical attribution notes kept below — the
|
||||||
|
"ORIGINAL 1995 LATENT BUG" premise was ❌ RETRACTED 2026-07-25 (#61).** The arcade swaps fog between `fog=` (lights on) and
|
||||||
`nosearchlightfog=` (off) via `PullFogRenderable` watching the Searchlight's `lightState`.
|
`nosearchlightfog=` (off) via `PullFogRenderable` watching the Searchlight's `lightState`.
|
||||||
**The old entry claimed the 1995 binary itself could never light the lamp. That was wrong** — it
|
**The old entry claimed the 1995 binary itself could never light the lamp. That was wrong** — it
|
||||||
compared Searchlight's Performance (@004b841c, reads `requestedOn`@0x1E0) against **ThermalSight's**
|
compared Searchlight's Performance (@004b841c, reads `requestedOn`@0x1E0) against **ThermalSight's**
|
||||||
@@ -229,15 +249,10 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR
|
|||||||
un-pooled `"ToggleLamp"` strings adjacent to their own class names. Body: @004b838c sits in a Ghidra
|
un-pooled `"ToggleLamp"` strings adjacent to their own class names. Body: @004b838c sits in a Ghidra
|
||||||
export gap (#60) but was **recovered by raw disasm** (`scratchpad/dis838c.py`) — it toggles 0x1E0 and
|
export gap (#60) but was **recovered by raw disasm** (`scratchpad/dis838c.py`) — it toggles 0x1E0 and
|
||||||
carries no novice gate. All T1.]
|
carries no novice gate. All T1.]
|
||||||
Searchlight's handler set is now WIRED and **verified live**: pad `0x14` → `requestedOn 0→1` →
|
Searchlight's handler set is WIRED and **verified live**: pad `0x14` → `requestedOn 0→1` →
|
||||||
`lightState 0→1`. So the sim needs **no repair** and the previous "DECISION: faithful to the buggy
|
`lightState 0→1`; the fog watcher + beam cone landed 2026-08-05 (see the header above), so no
|
||||||
original" is void — a working fog swap is now plain FAITHFUL reconstruction. Remaining work is a
|
remaining work rides this entry. (NB the 1995 searchlight carries NO novice gate — that
|
||||||
single item: construct `PullFogRenderable` at btl4vid.cpp `MakeMechRenderables` reticle-build/inside
|
lockout is ThermalSight's; raw disasm @004b838c.)
|
||||||
pass (== arcade part_014.c:5173, Dynamic, bound per Searchlight `lightState` via a new
|
|
||||||
`LightStatePtr()` accessor). `ControlsAllowLights()` is WIRED since issue #2 to the `player+0x25c`
|
|
||||||
not-novice experience flag via the BTPlayerExperienceSimLive bridge ([[experience-levels]]).
|
|
||||||
See [[rendering]] fog section. (The pre-#61 "verified inert live: BT_FOG_LOG zero `SetFogStyle(2/3)`"
|
|
||||||
observation still holds — reason (1), the un-constructed renderable, remains.)
|
|
||||||
- **`HandleMessage` is vtable slot 8/9 in the binary but NON-virtual across the reconstruction --
|
- **`HandleMessage` is vtable slot 8/9 in the binary but NON-virtual across the reconstruction --
|
||||||
FILED AS GITEA #65
|
FILED AS GITEA #65
|
||||||
(2026-07-25, found via #46).** Ten classes declare it (ammobin/heat×2/hud/mechsub/myomers/
|
(2026-07-25, found via #46).** Ten classes declare it (ammobin/heat×2/hud/mechsub/myomers/
|
||||||
@@ -694,6 +709,22 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR
|
|||||||
destroyed-subsystem dark-panel look.
|
destroyed-subsystem dark-panel look.
|
||||||
|
|
||||||
## Locomotion / combat polish (non-gating)
|
## Locomotion / combat polish (non-gating)
|
||||||
|
- **Myomer climb term is DEAD in the port: the gravity operand samples 0** (found in the #96
|
||||||
|
cruise benches, `[myoheat] g=0` every line — `BTMechMyomerMotionSample` reads
|
||||||
|
`GetEnvironment()->gravityConstant` and gets 0; the binary reads `**(mover+0x250)`, a POINTER
|
||||||
|
the Mover holds to the live gravity cell). Harmless on flat ground (|vy|≈0 anyway) but the
|
||||||
|
climb-work heat never accrues on slopes. Find where our Environment gravity actually lives
|
||||||
|
and re-point the bridge. [T2 measured]
|
||||||
|
- **Engine Mover gravity is dt-LESS per-frame in the binary** (`@0x421e77: vy -= **(+0x250)`,
|
||||||
|
raw, once per Perform — same class as the myomer kinetic term). If our linked WinTesla
|
||||||
|
MOVER.cpp kept that form, gravity acceleration in the port scales with OUR ~59Hz frame rate
|
||||||
|
(≈2.1× the pod's 28) — jump arcs, falls, missile droop all stiffer than the pod. CHECK the
|
||||||
|
WinTesla source's gravity line; if per-frame, it needs the same 28Hz reference-rate
|
||||||
|
normalization the myomer kinetic term got. [T1 binary side / T4 port impact until read]
|
||||||
|
- **Gait-noise phantom acceleration feeds the myomer accel term** (`[myoheat] a=4..33` at
|
||||||
|
steady cruise — our per-frame velocity re-derivation jitters; the binary read the same
|
||||||
|
`AccelerationLastFrame` cell off ITS OWN gait, noise level unknown). Bounded contributor
|
||||||
|
(~25-40% of cruise heat-in); revisit only if the 28Hz-faithful feel is contested. [T3]
|
||||||
- **✅ Interior vs exterior gait CLIP SET — DONE 2026-07-13 (task #59) [T2].** The local cockpit
|
- **✅ Interior vs exterior gait CLIP SET — DONE 2026-07-13 (task #59) [T2].** The local cockpit
|
||||||
mech was leaning −8° into every walk because the authentic ctor clip-set gate
|
mech was leaning −8° into every walk because the authentic ctor clip-set gate
|
||||||
(@part_012.c:10308-10320) was a no-op (`LoadLowDetailBody`/`LoadHighDetailBody` mislabeled the
|
(@part_012.c:10308-10320) was a no-op (`LoadLowDetailBody`/`LoadHighDetailBody` mislabeled the
|
||||||
@@ -901,6 +932,28 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR
|
|||||||
but is never CALLED, like the salvo mirror was thought to be, or a missing ForceUpdate). A
|
but is never CALLED, like the salvo mirror was thought to be, or a missing ForceUpdate). A
|
||||||
LIVENESS audit (does a live call path reach each reconstructed fn?) would catch this class.
|
LIVENESS audit (does a live call path reach each reconstructed fn?) would catch this class.
|
||||||
|
|
||||||
|
## Score/death report tail residuals (2026-08-05, #45/#134 reconstruction — the tail itself is DONE + benched, [[combat-damage]])
|
||||||
|
- **Mech-table 0x17 "SetBurningState" label/body MISMATCH.** The recovered name row says
|
||||||
|
Set/ClearBurningState for 0x17/0x18, but @0x49f674's body randomizes the mech's position from
|
||||||
|
two scaled randoms, rebuilds localToWorld, sets graphicAlarm level 2, ForceUpdates and clears
|
||||||
|
the not-simulated flag — nothing burns. Possibly a shifted name row (cf. the factory ClassID
|
||||||
|
mislabels). Neither handler is reconstructed (nothing in the port sends mech-0x17/0x18);
|
||||||
|
identify the real bodies before wiring. The DEATH-tail id-0x17 message that was once labeled
|
||||||
|
"SetBurningState dispatch" is RESOLVED: it is Player::VehicleDeadMessage (player table, not mech).
|
||||||
|
- **Does @0x4c05c4 (VehicleDead handler) CONSUME the BT extension fields** (+0x2c killed-by
|
||||||
|
player, +0x34 kill zone)? The function is not in the export; our #52 reconstruction predates
|
||||||
|
the fields. If it reads them, the likely consumer is a killed-by console post
|
||||||
|
(ConsolePlayerMechDeathWithoutHonor / a killed-by line) + maybe an RP-style "DestroyedBy"
|
||||||
|
status banner on the victim. The port SENDS them faithfully; raw-disasm the handler to close.
|
||||||
|
- **Collision-death tail fallthrough is inspection-tier [T3]:** the divert now falls to the death
|
||||||
|
tail (VehicleDead + blast) per @0x4a0375, but no live wall-death bench has run; field
|
||||||
|
wall-deaths exercise it nightly (watch for DEAD_NOTIFY on collision deaths).
|
||||||
|
- **The 1995 type-0 uninit-award bug** (documented in [[decomp-reference]] §BTPlayer table): the
|
||||||
|
shipped binary folds an uninitialized stack float into the shooter's score on every non-lethal
|
||||||
|
hit's type-0 report. Era scoreboards looked sane — worth understanding WHY (dispatch path may
|
||||||
|
deterministically leave ~0 at [ebp-0xc]; or replicant-player reroute delivery differs). Port
|
||||||
|
deviates safely (award=0). Curiosity, not a blocker.
|
||||||
|
|
||||||
## Rendering follow-ups (non-blocking)
|
## Rendering follow-ups (non-blocking)
|
||||||
- ~~Per-pilot mech PAINT (color/badge/patch)~~ — **DONE 2026-07-17, verified live** (crimson MadCat +
|
- ~~Per-pilot mech PAINT (color/badge/patch)~~ — **DONE 2026-07-17, verified live** (crimson MadCat +
|
||||||
yellow VGL emblems + hip hazard stripes). Mechanics + the vehicletable color/badge/patch name
|
yellow VGL emblems + hip hazard stripes). Mechanics + the vehicletable color/badge/patch name
|
||||||
@@ -967,6 +1020,25 @@ stability problems or massive lag spikes"*. Relevant to the carried cross-fire-s
|
|||||||
silent-node-crash opens: neither reproduced at 5 players this session. Not evidence of a fix
|
silent-node-crash opens: neither reproduced at 5 players this session. Not evidence of a fix
|
||||||
(neither was addressed), but it bounds how often they occur.
|
(neither was addressed), but it bounds how often they occur.
|
||||||
|
|
||||||
|
## Post-re-export leads (2026-08-06, #60 closed — [[source-completeness]], `phases/phase-04-gap-census.md`)
|
||||||
|
- **`@0x4c0904` is the MASTER BTPlayer Performance — our `@0x4c083c` attribution needs a
|
||||||
|
re-check.** Newly lit by the re-export: it resolves the player's Team by name out of the
|
||||||
|
`Teams` registry (error string "Team ... does not exist"), calls `@0x4c083c` and the engine
|
||||||
|
`Player::PlayerSimulation` (`@0x42e100`), then at mission-end (state 4, past a threshold,
|
||||||
|
once) posts "Sending EndMission" to the console host, and on a timer posts the periodic
|
||||||
|
console SCORE update (`Round(currentScore)` — corroborating our hand-built scoreboard
|
||||||
|
heartbeat). btplayer.cpp currently documents `@0x4c083c` as PlayerSimulation; read both and
|
||||||
|
fix the attribution + fold in the team resolution (we have no Team-by-name resolve today).
|
||||||
|
- **`0x480a2a-0x487ca0` (~9 KB, l4splr|btmssn boundary) stayed dark THROUGH the re-export** —
|
||||||
|
the gap fill reached it with neither a call nor a data reference, which is itself a finding:
|
||||||
|
dead code, data misparsed as code, or reachable only through a jump table. The earlier
|
||||||
|
spot-check showed a real `enter 0x3c` function driven by a global-index dispatch table
|
||||||
|
(`[0x4fef88]` → table `0x501588`), so "dead code" is unlikely. Byte-scan for the table's
|
||||||
|
writers to find the entry path.
|
||||||
|
- **~41 KB is still dark overall**, mostly munga/munga_l4 (we compile the real engine source, so
|
||||||
|
low value). Current map: `reference/decomp/GAP_CENSUS.md`; re-run the pipeline after any
|
||||||
|
future re-export.
|
||||||
|
|
||||||
## Key Relationships
|
## Key Relationships
|
||||||
- Feeds from: every subsystem/render topic (their deferral notes collect here).
|
- Feeds from: every subsystem/render topic (their deferral notes collect here).
|
||||||
- Gating master: [[source-completeness]] (the missing BT source).
|
- Gating master: [[source-completeness]] (the missing BT source).
|
||||||
@@ -1079,3 +1151,14 @@ tuning from taste into arithmetic.
|
|||||||
Conn Man saw heat build-up in Standard while Rajel expected none there. See
|
Conn Man saw heat build-up in Standard while Rajel expected none there. See
|
||||||
Gitea #116 and `experience-levels.md`.
|
Gitea #116 and `experience-levels.md`.
|
||||||
|
|
||||||
|
|
||||||
|
## Searchlight residuals (2026-08-05 — feature LIVE, [[rendering]] §SEARCHLIGHT)
|
||||||
|
- **Cone look pass pending**: spot.bgf hangs on the site joint with the site's rest transform;
|
||||||
|
the binary's per-case offset (local_2f0 at @004cef28 case 0xbd8) is believed to be the same
|
||||||
|
site offset [T3] — eyeball the size/aim on madcat/loki at night, then clear this.
|
||||||
|
- **BTL4VideoRenderer::LoadObject (@00498448) is a btstubs no-op** — the MechMarker beacon case
|
||||||
|
(MakeEntityRenderables 0xBBA) silently gets NULL and has never drawn. The searchlight went
|
||||||
|
around it (d3d_OBJECT::LoadObject direct). Reconstruct the wrapper or migrate the marker.
|
||||||
|
- **des_night-family pages have subtle fog deltas** (both sets near-black); arena/polar night
|
||||||
|
pages carry the dramatic 5↔40 near-plane swing. If testers report "searchlight does nothing"
|
||||||
|
on some maps, it's page authoring, not the mechanism.
|
||||||
|
|||||||
+218
-6
@@ -54,9 +54,10 @@ button** (Reservoir InjectCoolant, hold-to-flush — works), **0x2F/0x2E/0x2D/0x
|
|||||||
the per-condenser VALVE buttons** (MoveValve, Cond1-6 — work), **0x1A-0x1D = Generator A-D
|
the per-condenser VALVE buttons** (MoveValve, Cond1-6 — work), **0x1A-0x1D = Generator A-D
|
||||||
ON/OFF** (`ToggleGeneratorOnOff` id 4, binary table @0050fb90 fn @004b1ed0 — ✅ **WIRED**,
|
ON/OFF** (`ToggleGeneratorOnOff` id 4, binary table @0050fb90 fn @004b1ed0 — ✅ **WIRED**,
|
||||||
`powersub.cpp`). Newly decoded from the binary message tables: **0x13 → Mech `DuckRequest`
|
`powersub.cpp`). Newly decoded from the binary message tables: **0x13 → Mech `DuckRequest`
|
||||||
(0x1a @0049fa00 — the manual's CROUCH button)** — ✅ **WIRED 2026-07-26**, the last handler in
|
(0x1a @0049fa00 — the manual's CROUCH button)** — ✅ **COMPLETE 2026-08-06**: handler (07-26) +
|
||||||
this census; press-only, sets `duckState` and the map legend's `bduck.pcc` widget lights (see
|
the master-perf posture CONSUMER + squat/rise clips + MP replication ([[locomotion]] §CROUCH;
|
||||||
[[open-questions]] for why it has no code consumer) —, **0x28 → Mech `BalanceCoolant` (0x16
|
glass key F4); the map legend's `bduck.pcc` widget still lights. (The old "no code consumer"
|
||||||
|
verdict was export-gap blindness — [[reconstruction-gotchas]] §20) —, **0x28 → Mech `BalanceCoolant` (0x16
|
||||||
@0049f728)** ✅ **WIRED 2026-07-21 (#20)**, **0x12 → ThermalSight `ToggleLamp` (id 3, table @0x51120C fn @004b860c)** and **0x14 →
|
@0049f728)** ✅ **WIRED 2026-07-21 (#20)**, **0x12 → ThermalSight `ToggleLamp` (id 3, table @0x51120C fn @004b860c)** and **0x14 →
|
||||||
Searchlight + Searchlight2 `ToggleLamp` (id 3, table @0x51117C fn @004b838c)** — ✅ **BOTH WIRED
|
Searchlight + Searchlight2 `ToggleLamp` (id 3, table @0x51117C fn @004b838c)** — ✅ **BOTH WIRED
|
||||||
2026-07-25 (#61)**, previously default-constructed blackholes; verified live (0x14 →
|
2026-07-25 (#61)**, previously default-constructed blackholes; verified live (0x14 →
|
||||||
@@ -183,6 +184,212 @@ All cockpit surfaces are bit-plane MASKS over ONE shared `SVGA16` pixelBuffer: `
|
|||||||
byte; `Heat`=0x4000, `Mfd2`=0x0400, `Comm`=0x8000, `Mfd1`=0x0100, `Mfd3`=0x1000; `Eng1-3` =
|
byte; `Heat`=0x4000, `Mfd2`=0x0400, `Comm`=0x8000, `Mfd1`=0x0100, `Mfd3`=0x1000; `Eng1-3` =
|
||||||
engineering-mode alt planes; `overlay`=0x00C0 (shares the sec surface). See [[gauges-hud]]. [T2]
|
engineering-mode alt planes; `overlay`=0x00C0 (shares the sec surface). See [[gauges-hud]]. [T2]
|
||||||
|
|
||||||
|
## ⭐ THE RGB SPLIT — how ONE VGA port drives THREE mono MFDs (decoded 2026-08-06) [T0 engine source + T1 authentic pod config]
|
||||||
|
The five monochrome MFDs are NOT five video outputs. Each VGA port's **R, G and B analog lines
|
||||||
|
are split to three separate monochrome monitors**, and the software puts a different MFD in each
|
||||||
|
colour channel of one shared palettized framebuffer. Mechanism, end to end:
|
||||||
|
|
||||||
|
1. **Every surface is a bit-plane + a CHANNEL.** `content/GAUGE/L4GAUGE.CFG` (the authentic 1996
|
||||||
|
pod config) configures each port as `configure(idx, port, rotation, bitMask, clut, COLOUR, palette)`:
|
||||||
|
| port | panel | mask | clut | channel |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `Comm` | upper right | 0x8000 | clut2 | **red** |
|
||||||
|
| `Mfd2` (Engineering) | upper centre | 0x0400 | clut2 | **green** |
|
||||||
|
| `Heat` | upper left | 0x4000 | clut2 | **blue** |
|
||||||
|
| `Mfd1` | lower left | 0x0100 | clut1 | **red** |
|
||||||
|
| `Mfd3` | lower right | 0x1000 | clut1 | **green** |
|
||||||
|
| `sec` (+`overlay` 0x00C0) | secondary/radar | 0x003F | clut0 | **rgb** (full colour, rotation 270 — the physically ROTATED portrait CRT) |
|
||||||
|
`Eng1/2/3` are the engineering-page twins of Mfd1/2/3: same monitor, second bit-plane, switched
|
||||||
|
by `reconfigure(...)` giving one plane the channel and the other `blank`.
|
||||||
|
2. **The channel assignment is literally a palette write.** `L4GraphicsPort::BuildSecondaryColor`
|
||||||
|
(L4VB16.cpp) walks the palette entries owned by the port's bit group (`BitWrangler(byteMask,8)`)
|
||||||
|
and writes ONE component: `RedChannel -> triplet->Red`, `GreenChannel -> ->Green`,
|
||||||
|
`BlueChannel -> ->Blue`, `AllChannels -> the whole triplet`. `BlankColor` blanks the group
|
||||||
|
(`BlankPalette()`), which is how a page swap silences the plane it replaces. The
|
||||||
|
`*TransparentZero` variants skip colour 0 so zero reads as transparent for that group.
|
||||||
|
3. **So the DAC output carries three independent pictures**, one per analog line, and the splitter
|
||||||
|
hands each line to its own mono monitor. Three MFDs per VGA port; the pod's two MFD ports are
|
||||||
|
the **1280x480 "horizontally spanned" surface** (2 x 640x480 halves) the Displays section
|
||||||
|
describes — clut2 = the upper row (Comm/Mfd2/Heat), clut1 = the lower row (Mfd1/Mfd3, blue
|
||||||
|
spare). The radar rides its own port in real colour.
|
||||||
|
|
||||||
|
**Why this matters for the port [T2]:** our modern path renders each surface as its own
|
||||||
|
mono-tinted window on its own Windows display (see §MFD PANELS ON REAL HARDWARE), which is right
|
||||||
|
when every panel has its own output. **On splitter-wired glass it is wrong** — three monitors
|
||||||
|
would share one Windows display and each would show only its channel's share of a single tinted
|
||||||
|
image.
|
||||||
|
|
||||||
|
**`BT_POD_RGB=1` — the CHANNEL-COMPOSITE mode (built + field-verified 2026-08-06) [T2].** It
|
||||||
|
collapses the six panel windows into **three**: two VGA-port windows plus the radar. Each port
|
||||||
|
window composites its group's planes into ONE 640×480 BGRA image, each member ORed into its own
|
||||||
|
channel mask (`0x00FF0000` / `0x0000FF00` / `0x000000FF`), so the analog splitter downstream hands
|
||||||
|
each mono CRT exactly its picture. Grouping follows `L4GAUGE.CFG`: *VGA Port A* = Heat + Comm +
|
||||||
|
Mfd2, *VGA Port B* = Mfd1 + Mfd3; the radar keeps real colour on its own port.
|
||||||
|
`SVGA16::ExpandPlaneToBGRA` does the per-plane extraction, and `BT_POD_RGB` implies
|
||||||
|
`BT_POD_SURFACES`. Two companion gates came out of the bring-up: **`BT_POD_CHANMAP=<Port>=<colour>,…`**
|
||||||
|
remaps a surface's colour line when a cab's harness disagrees with the 1995 config (it did — see
|
||||||
|
§ALPHA-MR), and **`BT_POD_IDENT`** / **`BT_POD_CHANTEST`** paint identifying/cycling test patterns
|
||||||
|
so you can tell which physical CRT is which without trusting the desktop layout.
|
||||||
|
**SETTLED:** Nick's cart IS splitter-wired — its three 640×480 "displays" are VGA outputs, each
|
||||||
|
feeding a three-monitor splitter, and the composite is what lit the glass correctly.
|
||||||
|
|
||||||
|
## MFD PANELS ON REAL HARDWARE — the bring-up path (2026-08-06) [T2 local / T4 on-pod]
|
||||||
|
Nick's crash cart (pod hardware + Chrome Remote Desktop on a burner account) is the first chance
|
||||||
|
to drive the real panels. **The 1995 display path is NOT the way in.** That rig spanned the five
|
||||||
|
MFDs as ONE 1280×480 surface via **NVIDIA Horizontal Span, which every driver after XP dropped**;
|
||||||
|
`SVGA16::BuildWindows` also wants an exclusive-fullscreen D3D device per adapter, which is
|
||||||
|
fragile on modern drivers and over a remote session. Both are still in-tree and still the
|
||||||
|
authentic reference — they are just not the bring-up route.
|
||||||
|
|
||||||
|
**The route is the glass per-display windows** ([[glass-cockpit]]): one window per surface, which
|
||||||
|
maps 1:1 onto one physical panel per MFD, needs no special driver, and is already field-proven on
|
||||||
|
desktops. Two pieces were added for the cab:
|
||||||
|
- **`BT_POD_SURFACES=1` (pod surface mode, L4GLASSWIN)** — crops every window to its SURFACE
|
||||||
|
(MFDs exactly 640×480, radar 480×640 portrait), drops the on-screen RIO button banks (the cab's
|
||||||
|
buttons are PHYSICAL — drawing fake ones over a real panel is precisely wrong), goes frameless,
|
||||||
|
and does not create the Flight Controls pad at all (6 windows, not 7). Per-window equivalent:
|
||||||
|
append `,bare` to a line in `glass_layout.cfg` (mixed rigs).
|
||||||
|
- **Placement receipts** — every window logs `[glasswin] '<title>' surface=<port> at X,Y WxH
|
||||||
|
bare -> monitor \.\DISPLAYn (origin WxH, PRIMARY)`. On a cab nobody can see seven surfaces at
|
||||||
|
once, and over CRD you cannot see the panels at all: the log IS the confirmation that a picture
|
||||||
|
landed on the right glass.
|
||||||
|
|
||||||
|
**Runbook** (`tools/podprobe.ps1`, PowerShell, no install/admin — run it ON the pod PC):
|
||||||
|
1. Probe: GPUs, every monitor's virtual-desktop rect, EDID make/model (identifies the original
|
||||||
|
panels), serial ports (the RIO board), session type, and a PROPOSED `glass_layout.cfg` that
|
||||||
|
assigns the six surfaces to the non-primary monitors top-to-bottom/left-to-right, centred.
|
||||||
|
2. Drop the cfg in the game's working directory; run with `BT_GLASS_PANELS=1
|
||||||
|
BT_POD_SURFACES=1 BT_GLASS_LAYOUT=load` (`=save` to persist drags instead).
|
||||||
|
3. Read the receipts; re-assign titles to monitors in the cfg until each picture is on its panel.
|
||||||
|
Surface→panel roles: Heat MFD = upper left (coolant), Engineering = upper centre, Comm MFD =
|
||||||
|
upper right (hot box), Left/Right Weapons = lower left/right, Secondary/Radar = the secondary
|
||||||
|
screen (portrait). Main 3D view stays the game's own window on the main-view monitor.
|
||||||
|
|
||||||
|
**Known constraints / open on-pod questions:** whether the panels are attached to this PC at all
|
||||||
|
(the probe answers it); whether Windows offers a 640×480 mode on them (if not, the surface renders
|
||||||
|
at native size CENTRED, not scaled — a scale-to-fit option is the obvious follow-up); whether
|
||||||
|
Chrome Remote Desktop holds the CONSOLE session (it should — an RDP session would get a virtual
|
||||||
|
display and light nothing); and the RIO serial input, which is a separate task from the displays.
|
||||||
|
|
||||||
|
## ALPHA-MR — the verified cart config, FROZEN (2026-08-06) [T2, field-verified by eye]
|
||||||
|
The bring-up above landed: real pictures on real pod glass, then user-confirmed correct
|
||||||
|
("ok this is correct"). What that took, and where it now lives permanently.
|
||||||
|
|
||||||
|
**The rig.** Win10 IoT Enterprise LTSC on the cart; Intel HD630 + a "Trigger 6 External Graphics"
|
||||||
|
USB adapter. Four console displays: **DISPLAY3 800×600 PRIMARY at 0,0** (main view), plus
|
||||||
|
**DISPLAY4 @800,122**, **DISPLAY2 @1440,123**, **DISPLAY1 @2080,124** (each 640×480). Physical
|
||||||
|
cab: five mono MFD CRTs (two left stacked, two right stacked, one centre loose on the desk) plus
|
||||||
|
the colour radar LCD in the yellow-button frame.
|
||||||
|
|
||||||
|
**The mapping** (`glass_layout.cfg`, `monitor:<name>` binding — see [[glass-cockpit]]):
|
||||||
|
```
|
||||||
|
Secondary / Radar = monitor:DISPLAY4,bare # colour LCD, yellow-button frame
|
||||||
|
VGA Port A = monitor:DISPLAY2,bare # RGB triple: Heat + Comm + Mfd2
|
||||||
|
VGA Port B = monitor:DISPLAY1,bare # RGB triple: Mfd1 + Mfd3
|
||||||
|
```
|
||||||
|
Three windows, not six — under `BT_POD_RGB` each VGA-port window carries three MFDs on its three
|
||||||
|
colour planes (§THE RGB SPLIT). Desktop-coordinate order says NOTHING about physical cab position:
|
||||||
|
the first assignment, made by sorting monitors left-to-right, was wrong. Only eyes on the glass
|
||||||
|
settle it.
|
||||||
|
|
||||||
|
**Two deviations from the 1995 config, both real, both this cab's wiring:**
|
||||||
|
- **`BT_POD_CHANMAP=Comm=blue,Heat=red`** — kills/deaths and coolant loops came up on each
|
||||||
|
other's panel. `L4GAUGE.CFG` has Comm on red and Heat on blue; THIS cab's splitter harness
|
||||||
|
wires them the other way. The env override remaps a port's colour channel without touching the
|
||||||
|
authentic config file. [T2]
|
||||||
|
- **`BT_GAUGE_SEC_ROT=0`** — the radar here is a LANDSCAPE LCD, so the 640×480 source needs no
|
||||||
|
rotation. The 1995 pod's 270° rotation existed because its secondary CRT was mounted PORTRAIT;
|
||||||
|
it is a mounting fact, not a format fact. (En route: 180° looked plausible and was upside down.)
|
||||||
|
|
||||||
|
**Frozen in `content/environ.ini`**, not in a launcher — `scratchpad/pod/podprofile.ini` is the
|
||||||
|
block, `scratchpad/pod/mergeprofile.ps1` merges it idempotently between markers. environ.ini is
|
||||||
|
read before anything touches the environment ([[build-and-run]]), and the real environment still
|
||||||
|
WINS, so a .bat can override any of it for a one-off. The cart's `runpod.bat` now sets only
|
||||||
|
`BT_LOG`/`BT_GLASS_LOG` and the `-egg`; `runpod_env.bat` keeps the all-inline version as the
|
||||||
|
fallback. Two gates were added for this:
|
||||||
|
- **`BT_FIT=1`** — the env spelling of `-fit`, so the borderless main view survives however the
|
||||||
|
game is started (shortcut, scheduled task, autostart) instead of riding on one launcher's
|
||||||
|
command line. It fits `MONITOR_DEFAULTTOPRIMARY` = DISPLAY3, which is the main-view panel.
|
||||||
|
- **`L4PLASMA=NONE`** (also `OFF`/`0`) — no marquee at all. Needed because the GLASS profile
|
||||||
|
force-defaults `L4PLASMA=SCREEN`, so simply leaving it unset still puts a desktop plasma window
|
||||||
|
on the pod's glass. The boot banner reports `plasma off [L4PLASMA]` when it takes.
|
||||||
|
Verified on build 4.11.813 with the launcher carrying none of it: `[boot] environ.ini: 9
|
||||||
|
setting(s) applied`, `[cockpit] -fit: borderless 800x600`, `[glasswin] radar rotation 0 (none)`,
|
||||||
|
and all three surfaces on their intended `\\.\DISPLAYn`.
|
||||||
|
|
||||||
|
**Surviving a NEW BUILD, not just a reboot.** Both frozen files live INSIDE the versioned install
|
||||||
|
(`content\environ.ini`, `content\glass_layout.cfg`) and NEITHER ships in the zip — environ.ini is
|
||||||
|
generated on first run. So extracting a new build gives a cab that comes up wrong with no error
|
||||||
|
anywhere. The masters therefore live at the stable `C:\bt411\` (`podprofile.ini`,
|
||||||
|
`glass_layout.cfg`, `podkit.ps1`), and **`setup_pod.bat` pushes them into the newest `BT411_*`
|
||||||
|
folder** — run it once after each extract. `runpod.bat` resolves the newest install and applies
|
||||||
|
the kit itself before launching, so the remote path needs no hand-editing per build. Re-tuning the
|
||||||
|
cab means editing the MASTER at `C:\bt411\`, not the install's copy: the apply overwrites it, on
|
||||||
|
purpose, so there is one place to look when a panel moves. Repo copies: `scratchpad/pod/`.
|
||||||
|
|
||||||
|
**The tester launchers work on the cab unchanged (verified 2026-08-06) [T2].** `play_solo.bat`,
|
||||||
|
`join.bat`, `join_lan.bat`, `play_steam.bat` and `joyconfig.bat` set `BT_PLATFORM=glass`,
|
||||||
|
`BT_START_INSIDE=1`, `BT_DEV_GAUGES=1` plus their own mode flag — and **not one pod key** between
|
||||||
|
them. So each inherits the whole rig from environ.ini without knowing the pod exists. This is the
|
||||||
|
argument against a separate pod-only settings file: a second file would need every launcher to opt
|
||||||
|
in, which is the shape of the "competing keymaps" bug that made glass the default profile.
|
||||||
|
Observed: `play_solo.bat` on the cart logs `environ.ini: 7 setting(s) applied` — 7 not 9, because
|
||||||
|
the bat sets `BT_PLATFORM` and `BT_START_INSIDE` itself and the real environment wins.
|
||||||
|
Steam MP needs the **client installed and logged in** (AppID 480/Spacewar, so any account, no
|
||||||
|
purchase); installed on the cart 2026-08-06. ⚠ **#68: a failed JOIN/HOST exits the process
|
||||||
|
SILENTLY** — "Steam not running" and a real crash look identical to a player.
|
||||||
|
|
||||||
|
### The HARDWARE RIO is live on the cart (2026-08-06) [T2]
|
||||||
|
`L4CONTROLS=RIO:COM1,KEYBOARD` in the frozen profile — the real cockpit board instead of PadRIO.
|
||||||
|
The whole stock path above the seam (mapper, lamps, streamed `.CTL` mappings) runs unchanged; the
|
||||||
|
109-mapping `L4` control table installs exactly as it does on a desktop. **This is independent of
|
||||||
|
the platform profile** — the cab keeps the GLASS display stack (panels + RGB split) and only the
|
||||||
|
input device changes, which is what the glass/PadRIO seam was built for. Do NOT reach for
|
||||||
|
`BT_PLATFORM=pod` to get it.
|
||||||
|
|
||||||
|
- **Wiring:** `RIO:COMn` → `\\.\COMn` at **9600 8N1** (`L4SERIAL`); bare `RIO` defaults to COM1.
|
||||||
|
Reads are non-blocking (`ReadIntervalTimeout=MAXDWORD`, totals 0), so an absent or dead board
|
||||||
|
cannot hang the game — it just goes quiet.
|
||||||
|
- **RIO and PAD are MUTUALLY EXCLUSIVE** — both assign `rioPointer` in the `L4CONTROLS` parser, so
|
||||||
|
the last token wins. Selecting the hardware RIO turns the XInput pad OFF on that machine.
|
||||||
|
`KEYBOARD` is safe alongside: it only sets `flags.keyboardExists` and never touches
|
||||||
|
`primaryControlType`.
|
||||||
|
- **The board's own self-test is the diagnostic.** `RIO::RIO` runs it (`perform_tests = True` by
|
||||||
|
default) and writes `content\FAILURE.LOG` pages `RIOBoardErrors` / `RIODeadLamps` / `RIOErrors`,
|
||||||
|
deleting them first so the file is always current. On the cart, reproducibly: **4 missing boards**
|
||||||
|
(`Slot=3:Address=0`, `3:2`, `4:0`, `5:0`) and **16 dead lamps** (`AuxUpperRight1-8`,
|
||||||
|
`AuxUpperCenter1`, `TeslaRelay1/3`, `PanicButton`, `IcomAmpEnableRelay`, `IcomIncRelay`,
|
||||||
|
`FloorEntry`, `undefined_0x17`) — the crash cart is a PARTIAL cab, and those are the boards it
|
||||||
|
does not have. **A specific 4-of-many inventory is itself the proof the link is good**: a dead
|
||||||
|
serial line reports the whole address space missing, not four slots.
|
||||||
|
- **Input reaching the mech** was seen live as `[ctrlmap] push stick x=0.0595238 y=0` with nobody
|
||||||
|
at the cab — the physical stick sitting just outside the 5% deadband. NB when the stick is
|
||||||
|
centred this reads `x=0`, which is indistinguishable from no data: a centred-stick zero proves
|
||||||
|
nothing either way. ⚠ **Not yet checked by hand:** that stick/throttle/pedal travel and the
|
||||||
|
physical buttons drive the mech correctly, and whether the cab's pots match the `Ranger`
|
||||||
|
calibration baked into `RIO::RIO` (`JoystickX -96..96`, `JoystickY -97..108`, `Throttle 0..800`,
|
||||||
|
pedals `0..470`). A mismatch shows up as drift or short travel, not as an error.
|
||||||
|
- The boot banner names the resolved device — `GLASS (hardware RIO; plasma off [L4PLASMA])`. It
|
||||||
|
used to hardcode "PadRIO", which is a lie on a wired cab and exactly the line you read to check.
|
||||||
|
- ⚠ **THE COUPLING BUG this exposed (fixed 2026-08-06) [T2].** `BTGlassPanels_Create()` was called
|
||||||
|
ONLY from the end of the **PadRIO constructor** — the panels began life as the frames around the
|
||||||
|
on-screen RIO button banks, so "the buttons ride the device". Selecting the hardware RIO means
|
||||||
|
PadRIO is never constructed, so **every MFD window silently failed to appear**: the mission came
|
||||||
|
up on the main view with all the pod glass dark, and not one `[glasswin]` line in the log. The
|
||||||
|
MFD panels are a DISPLAY concern, so creation moved to `LBE4ControlsManager` after the
|
||||||
|
`L4CONTROLS` parse, where every device branch converges — plus a symmetric `BTGlassPanels_Destroy()`
|
||||||
|
in its destructor, because `~PadRIO` tore them down and `~RIO` knows nothing about them (windows
|
||||||
|
outliving the surfaces they blit would crash on the next mission cycle). Both calls are
|
||||||
|
idempotent (`Create` returns on `gWinCount != 0`), so the PadRIO path is unchanged and simply
|
||||||
|
arrives first.
|
||||||
|
|
||||||
|
**Working on the cart, remotely.** SSH over Tailscale lands in **session 0**, which has a dummy
|
||||||
|
"WinDisc" display and CANNOT see or enumerate session 1's windows — GUI work must go through
|
||||||
|
`schtasks /run /tn BT411Run` (task registered `/IT`), and window enumeration over SSH silently
|
||||||
|
returns nothing rather than failing. Kill `btl4.exe` BEFORE scp'ing a new exe or the file is
|
||||||
|
locked. The receipts in `podrun.log` are the remote eyes; a missing `DEBUG_STREAM` line in an
|
||||||
|
otherwise-logging run is real evidence that code path did not execute.
|
||||||
|
|
||||||
## The 1995 player manual — alignment audit (2026-07-18) [T1, primary source]
|
## The 1995 player manual — alignment audit (2026-07-18) [T1, primary source]
|
||||||
`reference/manual/Tesla40_BT_manual.pdf` (34pp, from Nick). CONFIRMS the reconstruction on
|
`reference/manual/Tesla40_BT_manual.pdf` (34pp, from Nick). CONFIRMS the reconstruction on
|
||||||
every checked control behavior:
|
every checked control behavior:
|
||||||
@@ -231,9 +438,14 @@ every checked control behavior:
|
|||||||
"Top Speed Gimped 40" also confirms the limp-gait speed cap as an authored spec. The port
|
"Top Speed Gimped 40" also confirms the limp-gait speed cap as an authored spec. The port
|
||||||
follows the BINARY (house rule); resurrecting manual-4.0 supercharge would be an opt-in
|
follows the BINARY (house rule); resurrecting manual-4.0 supercharge would be an opt-in
|
||||||
deviation for the operator to decide.
|
deviation for the operator to decide.
|
||||||
NEW LEADS (manual describes, port lacks input/UI): **CROUCH** (button by the secondary screen;
|
NEW LEADS (manual describes, port lacks input/UI): ~~CROUCH~~ ✅ **CROUCH COMPLETE 2026-08-06**
|
||||||
`Mech::duckState` attr 0x37 + SQUAT clips exist, nothing drives them), **EJECT** (sounds exist:
|
([[locomotion]] §CROUCH), ~~EJECT~~ **EJECT WIRED
|
||||||
EjectButton01_z*.wav), hot-box viewscreen framing (the deferred PNAME marker chain). Per-mech
|
2026-08-02** (core: `Mech::EjectPilotMessageHandler` id 0x19 @0049f854 + the crippled-mech
|
||||||
|
permission evaluator @0049fa1c; input = binding-engine "Eject" action, Backspace / pad
|
||||||
|
LeftThumb; the punch-out kills via graphicAlarm 10 ≥ 9 — KillBonus authors 0 in ALL shipped
|
||||||
|
content, so the zero charge is authentic; tails: console relay notice, RIO 0x38 panic control,
|
||||||
|
alarm-10 audio/canopy presentation, `SpecialCaseDeathPenalty` role+0x20 consumer), hot-box
|
||||||
|
viewscreen framing (the deferred PNAME marker chain). Per-mech
|
||||||
stat tables = a systematic cross-check source for our streamed subsystem resources.
|
stat tables = a systematic cross-check source for our streamed subsystem resources.
|
||||||
|
|
||||||
### Coolant-loop cross-check RESULT (2026-07-18) [T1] -- STRUCTURE strongly faithful
|
### Coolant-loop cross-check RESULT (2026-07-18) [T1] -- STRUCTURE strongly faithful
|
||||||
|
|||||||
+18
-13
@@ -39,19 +39,24 @@ sections in `docs/PROGRESS_LOG.md` cite old `C:/git/nick-games/...` paths — tr
|
|||||||
they map into this repo (reconstructed BT → `game/reconstructed/`, engine → `engine/MUNGA{,_L4}/`,
|
they map into this repo (reconstructed BT → `game/reconstructed/`, engine → `engine/MUNGA{,_L4}/`,
|
||||||
content → `content/`, raw decomp → `reference/decomp/`). [T2]
|
content → `content/`, raw decomp → `reference/decomp/`). [T2]
|
||||||
|
|
||||||
## Current state (2026-07)
|
## Current state (2026-08-06) — core gameplay reconstruction COMPLETE; polish phase
|
||||||
`btl4.exe` boots, renders the world + a skinned mech, and runs a full **drive → animate → target →
|
Field-tested nightly at 4-8 pods over Steam (builds 4.11.622→801). Authentic + benched: the full
|
||||||
fire → damage → destroy → respawn** single-player loop (task #52). **2-node MP is verified
|
SP+MP loop; per-panel mesh-true targeting/damage (+ crits, cylinder lottery, armour darkening);
|
||||||
end-to-end** — replication, cross-pod targeting/damage/kill, beam visuals, replicant gait (tasks
|
the 1995 SCORING model (kill awards + received penalties + panic cost — the id-0x16 report tail);
|
||||||
#46-#51, [[multiplayer]]). All 8 cockpit canopies are authentic + the horizontal-FOV fix (task #55,
|
death (blast/splash, wreck, burial) + respawn (full re-arm audit); locomotion (two-channel gait,
|
||||||
[[cockpit-view]]); the Gyroscope is live byte-exact with hit-bounce (task #56); the **gauge system
|
gimp limp, knockdowns, CROUCH F4); the night kit (fog-swap SEARCHLIGHT F5 + beam cone); heat/
|
||||||
is complete** ([[gauges-hud]]). The engine/renderer/HAL are done (WinTesla); AUDIO backend is now REAL
|
power/myomers (incl. the seek-4 freeze); replication (masters/replicants, kill/death columns,
|
||||||
(the repo's OpenAL32.dll + libsndfile-1.dll were both no-op STUBS — replaced with real OpenAL Soft +
|
ghost+skate field detectors); the Steam wire seam + build gate; the operator console/relay
|
||||||
an in-tree WAV loader, 2026-07-15) and the 241-sample soundbank is cracked from AUDIO1/2.RES + loading;
|
lifecycle; gauges/HUD; audio (real OpenAL backend + the AUDIO_FIDELITY trigger waves — footsteps/
|
||||||
the ONLY remaining audio gap is game-triggering (no AudioEntities fire PlayNote yet), so gameplay is
|
gait/alarms/impacts live in the field). [T2]
|
||||||
still silent apart from a proof-of-life hook (see [[wintesla-port]] Audio). The active work
|
**Infra ✅ 2026-08-06:** the #60 export gap census AND re-export shipped — coverage 87.3% →
|
||||||
is reconstructing each BT subsystem's authentic behavior from the binary. Remaining: pod-LAN
|
93.5%, dark code 90 → 41 KB, +205 functions, and the re-export independently confirmed the
|
||||||
config, Mech-level update records, per-subsystem waves. [T2]
|
week's raw-disasm reconstructions ([[source-completeness]], `phases/phase-04-gap-census.md`).
|
||||||
|
**Remaining (polish):** VehicleDead killed-by consumption (operator "X killed by Y" — the sender
|
||||||
|
already ships the fields); burning-wreck handler (mech-0x17 label mismatch); `@0x4c0904` master
|
||||||
|
BTPlayer Performance vs our `@0x4c083c` attribution; the id-0x16 type-0 curiosity; MechRIOMapper
|
||||||
|
Keypress @004d2514; marker beacon (LoadObject wrapper stub); DIV firmware intersection routine
|
||||||
|
(deep-cut); deferred ledger items in [[open-questions]]. [T2]
|
||||||
|
|
||||||
## Key Relationships
|
## Key Relationships
|
||||||
- Full detail: `docs/PROGRESS_LOG.md`.
|
- Full detail: `docs/PROGRESS_LOG.md`.
|
||||||
|
|||||||
@@ -278,6 +278,12 @@ Suspect ANY reconstructed per-frame code with narrow equality/window tests or `x
|
|||||||
state transitions: charge/seek loops, snap comparisons, timers compared with `==`.
|
state transitions: charge/seek loops, snap comparisons, timers compared with `==`.
|
||||||
|
|
||||||
## 13. Verification gotchas (don't fool yourself)
|
## 13. Verification gotchas (don't fool yourself)
|
||||||
|
- **Capture the viewpoint that can SEE the change.** The crouch "pose does not hold" and the
|
||||||
|
false "eye does not drop" residual (2026-08-05/06) were BOTH capture errors: cockpit-view
|
||||||
|
screenshots cannot show your own legs, and canopy-dominant diff crops mask eye-relative motion
|
||||||
|
(the canopy drops WITH the eye — only the through-window ground shifts). Anchor pixel-diff
|
||||||
|
crops on a region the effect MUST change, pair them with a state probe (joint values), and
|
||||||
|
READ one frame with your own eyes before declaring a visual regression.
|
||||||
|
|
||||||
- **Lazy gauge build:** `GaugeRenderer::BuildConfigurationFile` runs LAZILY. A too-early process
|
- **Lazy gauge build:** `GaugeRenderer::BuildConfigurationFile` runs LAZILY. A too-early process
|
||||||
kill shows `[gskip]=0` / "not built" even though the widget is fine — **wait for the gauge
|
kill shows `[gskip]=0` / "not built" even though the widget is fine — **wait for the gauge
|
||||||
@@ -543,6 +549,34 @@ segment tables) — the same class of latent overflow.
|
|||||||
0xFEEEFEEE=freed).
|
0xFEEEFEEE=freed).
|
||||||
5. For exhaustive multi-function analysis: a read-only Workflow (understand), then implement hands-on.
|
5. For exhaustive multi-function analysis: a read-only Workflow (understand), then implement hands-on.
|
||||||
|
|
||||||
|
## 20. Export-gap blindness — absence in the EXPORT is not absence in the BINARY (4 incidents)
|
||||||
|
The Ghidra export (`reference/decomp/`) has coverage gaps (#60), and BTL4.RES content never
|
||||||
|
appears in it at all — so "no readers", "no caller", "no such clip/asset", and "unreconstructed"
|
||||||
|
claims made by grepping the export or the port ALONE are structurally unsound. This class has
|
||||||
|
produced four wrong conclusions, two of them [T1]-tagged at the time:
|
||||||
|
1. **2026-07-25 searchlight "1995 latent bug"** — compared Searchlight's Performance against
|
||||||
|
THERMALSIGHT's handler (wrong class) and invented a missing bridge; retracted (#61).
|
||||||
|
2. **2026-07-2x `DuckState` "has no CODE consumer, authentic [T1]" + "no SQUAT clip survives"** —
|
||||||
|
the consumer was the un-exported master-perf posture block (@0x4aa011), and the clips lived in
|
||||||
|
BTL4.RES (`squ/sqd/squi/sqdi` × 8 chassis); the search covered decomp strings + loose files,
|
||||||
|
never the RES TOC. Disproven by the CROUCH reconstruction (2026-08-06).
|
||||||
|
3. **2026-07-31 morning: the myomer drive-scale DELETION** ("the binary has NO dynamic
|
||||||
|
myomer→speed coupling") — the consumer was un-exported; restored same day by raw capstone.
|
||||||
|
4. **2026-08-05 "the myomer factor FEEDER (@004b8be3) is unreconstructed"** — it had been fully
|
||||||
|
reconstructed since 07-31 under NAMED members (`speedEffect`, `AvailableOutput`); the grep
|
||||||
|
searched raw offsets. Cost: a duplicate multiply + a dead crouch gate until 08-06.
|
||||||
|
**The rule — before claiming "X does not exist / is not reconstructed":**
|
||||||
|
- byte-scan `content/BTL4OPT.EXE` for the offset/immediate (disp32 patterns), never just the export;
|
||||||
|
- grep the port for NAMED members (check the .hpp for the offset's name) — offsets rot after promotion;
|
||||||
|
- for content claims, walk the **BTL4.RES TOC** (`tools/resscan.py` pattern), not the loose tree;
|
||||||
|
- check `reference/BT410_SOURCE_MANIFEST.md` + `game/reconstructed/CLASSMAP.md`;
|
||||||
|
- and tag the claim's tier by the WEAKEST source consulted — an export-only sweep caps at [T4].
|
||||||
|
The #60 gap census + RE-EXPORT is the systemic fix, and BOTH shipped 2026-08-06: the export
|
||||||
|
now covers 93.5% of .text (dark code 90 KB → 41 KB) and `reference/decomp/GAP_CENSUS.md` maps
|
||||||
|
what is still dark. **So the rule is cheap now: look the address up in GAP_CENSUS.md first.**
|
||||||
|
If it is NOT in a dark region, the export is authoritative and "absent" means absent. If it IS,
|
||||||
|
byte-scan before claiming anything. (Toolchain + method: `phases/phase-04-gap-census.md`.)
|
||||||
|
|
||||||
## Key Relationships
|
## Key Relationships
|
||||||
- Applies to: every topic that reconstructs a class ([[subsystems]], [[combat-damage]], [[gauges-hud]], [[locomotion]]).
|
- Applies to: every topic that reconstructs a class ([[subsystems]], [[combat-damage]], [[gauges-hud]], [[locomotion]]).
|
||||||
- Uses: [[decomp-reference]] (offsets/ClassIDs), [[reconstruction-method]] (the loop).
|
- Uses: [[decomp-reference]] (offsets/ClassIDs), [[reconstruction-method]] (the loop).
|
||||||
@@ -795,3 +829,37 @@ grep its helper types for `return 0` bodies FIRST; (b) a bring-up shim must
|
|||||||
either Fail() loudly or log its own emptiness, never silently iterate nothing;
|
either Fail() loudly or log its own emptiness, never silently iterate nothing;
|
||||||
(c) before shimming an engine-side structure, check whether the engine already
|
(c) before shimming an engine-side structure, check whether the engine already
|
||||||
has it -- the offsets in the decomp comment ARE the lookup key.
|
has it -- the offsets in the decomp comment ARE the lookup key.
|
||||||
|
|
||||||
|
## 27. Drawing where the design says DON'T: under-surface buttons, shared planes, and inherited D3D state (#118, 2026-08-03)
|
||||||
|
|
||||||
|
Three MFD-corruption incidents in one evening, all mine, all the same class:
|
||||||
|
adding a DRAW to a compositor whose layout invariants I had not read.
|
||||||
|
|
||||||
|
1. **The eject-wipe gauge** (BitMapInverseWipeScalar) drew into a weapon's ENG
|
||||||
|
graphics port -- but the Eng1-3 planes SHARE composite dest cells with the
|
||||||
|
weapon pages (`ckSlotOf` in L4VB16), so its mis-geometry scribbled striped
|
||||||
|
garbage OVER the weapon MFDs whenever an eject cycle redrew it (= at
|
||||||
|
overheat-jam onset, which the operator correctly correlated with "when the
|
||||||
|
coolant warning starts").
|
||||||
|
2. **The "flash overlay"** re-drew flashing buttons' full faces ON TOP of the
|
||||||
|
surfaces. But the side buttons are BIG rects deliberately tucked UNDER the
|
||||||
|
displays -- the protruding edge IS the lamp (the pod's backlit keys sat
|
||||||
|
beside the CRT; there is no in-display face). A full-face redraw is wrong
|
||||||
|
BY DESIGN: it covers gauge imagery with button faces.
|
||||||
|
3. **The same overlay, drawn after `DrawDevSurface`, inherited its bound
|
||||||
|
texture** -- an un-reset `CkFill` renders a TEXTURED quad sampling the
|
||||||
|
gauge atlas: green diagonal-striped rectangles. (With the state reset the
|
||||||
|
bug became honest: solid flashing faces covering the MFD -- which is how
|
||||||
|
the operator's "the panel buttons took over the MFD" report finally
|
||||||
|
identified the author.)
|
||||||
|
|
||||||
|
Rules: (a) before ADDING a draw to an existing render pass, read the pass's
|
||||||
|
layering comments -- "UNDER the surfaces", "masks the part inside", "shares
|
||||||
|
its sibling's dest cell" are INVARIANTS, not descriptions; (b) any D3D draw
|
||||||
|
inserted after another subsystem's draws must re-assert its own texture/color
|
||||||
|
state (CkColorState-equivalent) -- the previous draw's bindings are live;
|
||||||
|
(c) when a visual corruption tracks an EVENT ("starts with the coolant
|
||||||
|
warning"), list what STARTS DRAWING at that event -- alarm-driven redraws,
|
||||||
|
state-change repaints -- before suspecting the event's logic; (d) the
|
||||||
|
operator's screenshot is worth ten theories: the red-faces capture identified
|
||||||
|
in one frame what three log-side hypotheses missed.
|
||||||
|
|||||||
+47
-1
@@ -335,7 +335,9 @@ the limb swap ("the actual enemy mech in external view is not showing darkened a
|
|||||||
- **`.DZM`** (`VIDEO\<mech>SKIN.DZM`, 40 shipped) lists, per damage zone, the materials that zone
|
- **`.DZM`** (`VIDEO\<mech>SKIN.DZM`, 40 shipped) lists, per damage zone, the materials that zone
|
||||||
paints: `[dz_ltorso] material=avaskin:avat2_dz_ltorso_mtl`. One file per SKIN VARIANT — the
|
paints: `[dz_ltorso] material=avaskin:avat2_dz_ltorso_mtl`. One file per SKIN VARIANT — the
|
||||||
`dzm`/`dzms`/`dzmt`/`dzmo`/`dzma`/`dzmb`/`dzmc`/`dzmd` key family (same suffix set as the
|
`dzm`/`dzms`/`dzmt`/`dzmo`/`dzma`/`dzmb`/`dzmc`/`dzmd` key family (same suffix set as the
|
||||||
`destroyed`…`destroyedd` video variants); the res compiler is `FUN_0041e4e0`.
|
`destroyed`…`destroyedd` video variants); the res-compiler address is UNKNOWN (the old
|
||||||
|
`FUN_0041e4e0` citation was WRONG — that is `DamageZone::TakeDamage`; corrected 2026-08-02,
|
||||||
|
swept with [[decomp-reference]]).
|
||||||
- Compiled into BTL4.RES and parsed by **MUNGA's own `DamageZone` stream ctor** into
|
- Compiled into BTL4.RES and parsed by **MUNGA's own `DamageZone` stream ctor** into
|
||||||
`materialTable`, keyed by `EntitySegment::SkeletonType` (`engine/MUNGA/DAMAGE.cpp:311-336`,
|
`materialTable`, keyed by `EntitySegment::SkeletonType` (`engine/MUNGA/DAMAGE.cpp:311-336`,
|
||||||
`GetMaterialList(skl_type)`) — real engine source [T0]. **This data was loaded and unused in the
|
`GetMaterialList(skl_type)`) — real engine source [T0]. **This data was loaded and unused in the
|
||||||
@@ -377,6 +379,50 @@ mech, brightness tracking `1 - 0.9*level` exactly, hull 0.35x vs terrain control
|
|||||||
The `damcolor_`/`gen*gry_` arm+gun names go unmatched until the DESTROYED limb variant loads —
|
The `damcolor_`/`gen*gry_` arm+gun names go unmatched until the DESTROYED limb variant loads —
|
||||||
correct: those materials live on the `*D*.BGF` damaged meshes.
|
correct: those materials live on the `*D*.BGF` damaged meshes.
|
||||||
|
|
||||||
|
## SEARCHLIGHT -- the pod's night kit, RECONSTRUCTED 2026-08-05 [T1 decode, T2 both benches]
|
||||||
|
The subsystem (searchlight.cpp: sim @4b841c, ToggleLamp @4b838c toggles requestedOn@0x1E0 --
|
||||||
|
NO novice gate, that's ThermalSight's; lightState = requested && powered(4) && heat<2 && alive,
|
||||||
|
published as the "LightOn" attribute + replicated as subsystem record 0x14) was already complete;
|
||||||
|
the VISUALS were the gap, decoded from MakeMechRenderables @004cef28 `case 0xbd8` [T1]:
|
||||||
|
- **Cockpit = a FOG SWAP, not a light.** The @00456778/@00456814 watcher polls up to two LightOn
|
||||||
|
attrs; either-on -> `SetFogStyle(searchLightOnFogStyle)` (the authored `fog=` set), off ->
|
||||||
|
`searchLightOffFogStyle` (the `nosearchlightfog=` set). Both sets are authored per
|
||||||
|
map/time/weather page in BTDPL.INI (arena_night 40/400 lit vs 5/400 dark; des_night 100/1100
|
||||||
|
vs 30/700; arnitclear 90/1100 vs 5/1000). The 2007 engine kept the whole system under its real
|
||||||
|
names (searchLightFog*/noSearchLightFog*, the FogStyle enum); the port completed the stubbed
|
||||||
|
plane application (currentFogNear/Far now swap with the style) and transcribed the watcher into
|
||||||
|
`TickSearchlight` -- including the ctor's INVERTED cache seed, whose first-tick "change"
|
||||||
|
normalizes a night start onto the DARK set. Fidelity consequence: every prior build rendered
|
||||||
|
the lit fog permanently (nothing called SetFogStyle(3)); authentic night starts darker.
|
||||||
|
Glass key: F5 -> streamed button 0x14.
|
||||||
|
- **PERCEIVED STRENGTH [T2 field eyeball 2026-08-05]: authentically SUBTLE.** With LINEAR fog the
|
||||||
|
near-plane move barely changes close-range haze (at 30u the arena pair differs ~5%); the swap
|
||||||
|
reads as a modest push-back of the night murk at mid/long range, NOT a flashlight. Geometry
|
||||||
|
brightness comes from the page's ambient= + ramps and never changes with the lamp -- a close
|
||||||
|
wall is identical lit/unlit. First eyeball matched exactly ("kinda sorta illuminates the
|
||||||
|
area... didn't illuminate the wall" -- that IS the 1995 behavior). No illuminating dynamic
|
||||||
|
light exists anywhere in BT's render path: ChildLightRenderable has ZERO instantiations
|
||||||
|
engine-wide, and the FUN_0048d060 cluster (@4568f8/@456a0c/@456b4c/@4586d0) is light-POINT
|
||||||
|
sprites (beacons/nav strobes, palette-tinted), not geometry lights. Set player expectations
|
||||||
|
in the release notes accordingly.
|
||||||
|
- **External = the spot.bgf beam-cone mesh** (VIDEO/GEO + per-time variants), hung as a child of
|
||||||
|
the searchlight SITE joint (mount = subsystem `mountSegment`@0x1DC == resource segmentIndex;
|
||||||
|
madcat 20, loki 21), shown/hidden per frame from the replicated LightOn (@0045612c watcher ->
|
||||||
|
the SetDrawObj swap). Site segments now build geometry-less DCS children (posed + parentable --
|
||||||
|
the 1995 graph gave every site a DCS node; our tree used to skip sites entirely; this also
|
||||||
|
enables future site-hung effects). Wrecks go dark through the authentic gate (host shutdown
|
||||||
|
forces lightState 0). Cross-node verified: B presses, A logs `[spot] cone SHOWN (seg 20)`.
|
||||||
|
- Benches: `scratchpad/night12/searchfog.sh` (solo cockpit chain + red-fog end-to-end probe),
|
||||||
|
`spotcone.sh` (2-node cone + replication). BEAM MATERIAL decoded 2026-08-06 (b75bb4a) [T1]:
|
||||||
|
SPOT.BGF is a 7-vert cone ~50u forward and ~35° DOWN (a ground-pool lamp, not an air beam),
|
||||||
|
verts tinted cyan-white; its material class `btfx:brighten.25` smuggles the ADDITIVE factor
|
||||||
|
in DIFFUSE.r (0.25) with a warm emissive on the night page — the loader draws brighten*
|
||||||
|
batches as an additive veil (dest += vertexRGB × factor, blend pass, unlit); tint compose
|
||||||
|
(vertex cyan vs night emissive warm) [T3], flagged in the L4D3D draw branch. Look pass
|
||||||
|
ACCEPTED by field eyeball 2026-08-06 (translucent beam reads correctly; solo rig
|
||||||
|
BT_SPOT_SELF=1 + BT_CAM=face). Still open ([[open-questions]]): the stubbed LoadObject
|
||||||
|
wrapper + dormant marker case, the subtle authored deltas.
|
||||||
|
|
||||||
## Key Relationships
|
## Key Relationships
|
||||||
- Geometry/LOD: [[bgf-format]]. Base: [[wintesla-port]] (L4D3D). Shadow/visual-conform: [[locomotion]].
|
- Geometry/LOD: [[bgf-format]]. Base: [[wintesla-port]] (L4D3D). Shadow/visual-conform: [[locomotion]].
|
||||||
- Renders on: [[pod-hardware]] (main 3D view).
|
- Renders on: [[pod-hardware]] (main 3D view).
|
||||||
|
|||||||
@@ -94,6 +94,32 @@ its recorded line 400 exactly. [T1/T3 per the sidecar]
|
|||||||
Plus `BTL4OPT.EXE` as a behavioral **oracle** (run under cdb / read the decomp). Result =
|
Plus `BTL4OPT.EXE` as a behavioral **oracle** (run under cdb / read the decomp). Result =
|
||||||
behavior-equivalent **reconstruction**, NOT original source. [T2]
|
behavior-equivalent **reconstruction**, NOT original source. [T2]
|
||||||
|
|
||||||
|
## THE GAP CENSUS + RE-EXPORT (2026-08-06, #60) — BOTH HALVES DONE [T1]
|
||||||
|
**The export was REBUILT 2026-08-06** (Ghidra 12.1.2 headless + `ExportGaps.java`'s gap-fill
|
||||||
|
pass): index coverage **87.3% → 93.5%**, dark real code **90.4 KB → 40.8 KB (54.8% recovered)**,
|
||||||
|
game-side dark 53.1 → 21.1 KB, functions 6,267 → **6,472** (+205, zero decompile failures).
|
||||||
|
Every historically-bitten dark function now has pseudocode — including `@0x4c05c4` VehicleDead,
|
||||||
|
the absence that opened #60. The re-export CONFIRMED the crouch reconstruction field-for-field
|
||||||
|
and revealed one branch the raw pass missed (airborne auto-rise, fixed same day).
|
||||||
|
⚠ **The old export is archived at `reference/decomp/archive_2025export/`** — addresses are
|
||||||
|
stable across both, but `part_0NN.c:LINE` citations only resolve against the archive. Cite
|
||||||
|
`@ADDR`, not part/line. Pipeline: `tools/ghidra_reexport.sh` → `tools/gapcensus.py` →
|
||||||
|
`tools/gapdiff.py`. Toolchain (JDK 21 + Ghidra 12.1.2) lives in `%LOCALAPPDATA%\bt411-tools`;
|
||||||
|
the runner MUST use 8.3 short paths (Ghidra's .bat expands %JAVA_HOME% unquoted and the user
|
||||||
|
profile has a space). Full log: `phases/phase-04-gap-census.md`.
|
||||||
|
|
||||||
|
### The original inventory (part 1, same day) [T1]
|
||||||
|
`tools/gapcensus.py` → `reference/decomp/GAP_CENSUS.md` + `gap_census.tsv` (regenerate after
|
||||||
|
any re-export; it censuses whatever dir you point it at). As found BEFORE the re-export: .text
|
||||||
|
892 KB, index 87.3%, **428 dark regions = 90 KB of real code** (indexed-but-unexported = 0 —
|
||||||
|
the gap class is purely "not indexed"), game-side dark ≈ 54 KB with 159 regions never touched.
|
||||||
|
Current numbers are in the header above. Validation: all six historically-bitten dark
|
||||||
|
addresses (VehicleDead, ToggleLamp, death tail, master-perf, myomer integrator, duck consumer)
|
||||||
|
land inside census regions. Top uncharted leads + method: `phases/phase-04-gap-census.md`.
|
||||||
|
**RULE (gotcha §20):** consult the census BEFORE any "absent from the export/binary" claim —
|
||||||
|
if the address falls in a dark region, the claim needs byte-scan evidence, not export silence.
|
||||||
|
(The second half — the re-export — landed the same day; see the header.)
|
||||||
|
|
||||||
## Key Relationships
|
## Key Relationships
|
||||||
- Feeds: [[reconstruction-method]] (the loop that fills the gap), [[decomp-reference]].
|
- Feeds: [[reconstruction-method]] (the loop that fills the gap), [[decomp-reference]].
|
||||||
- Route A closed 2026-07-19 (Nick holds nothing further); gated now only on reconstruction labor + the engine-closure backfill.
|
- Route A closed 2026-07-19 (Nick holds nothing further); gated now only on reconstruction labor + the engine-closure backfill.
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ Making a base byte-exact GROWS every subclass — they must be re-based TOGETHER
|
|||||||
- **WAVE 3** — power bus (Generator/PoweredSubsystem) + Emitter/PPC fire-path (end-to-end fire; heat
|
- **WAVE 3** — power bus (Generator/PoweredSubsystem) + Emitter/PPC fire-path (end-to-end fire; heat
|
||||||
conducts to the central sink via the linked-sink roster).
|
conducts to the central sink via the linked-sink roster).
|
||||||
- **WAVE 4** — standalone readouts: Sensor/Searchlight/ThermalSight/AmmoBin (de-shim, gate fixes).
|
- **WAVE 4** — standalone readouts: Sensor/Searchlight/ThermalSight/AmmoBin (de-shim, gate fixes).
|
||||||
|
- ✅ **Searchlight VISUALS complete 2026-08-05** — cockpit fog swap + external spot.bgf beam
|
||||||
|
cone, both MP-replicated; mountSegment@0x1DC identified (= resource segmentIndex, the cone's
|
||||||
|
mount joint — was "commandedOn, role unidentified"). Full story: [[rendering]] §SEARCHLIGHT.
|
||||||
- ✅ **Searchlight + ThermalSight buttons WIRED 2026-07-25 (#61)** — both classes' handler sets were
|
- ✅ **Searchlight + ThermalSight buttons WIRED 2026-07-25 (#61)** — both classes' handler sets were
|
||||||
default-constructed blackholes (systemic cause #1, `docs/INPUT_PATH_AUDIT.md`). Each now chains
|
default-constructed blackholes (systemic cause #1, `docs/INPUT_PATH_AUDIT.md`). Each now chains
|
||||||
`PowerWatcher::GetMessageHandlers()` with its own id-3 `ToggleLamp`. **Verified live**: pad `0x14`
|
`PowerWatcher::GetMessageHandlers()` with its own id-3 `ToggleLamp`. **Verified live**: pad `0x14`
|
||||||
|
|||||||
@@ -84,6 +84,56 @@ The vehicle sed must replace the WHOLE line (`s/^vehicle=.*/`), or you mint
|
|||||||
`vehicle=thr1bhk1` and the spawn FATALs. Chassis codes: ava1 bhk1 lok1/2 mad1/2
|
`vehicle=thr1bhk1` and the spawn FATALs. Chassis codes: ava1 bhk1 lok1/2 mad1/2
|
||||||
own1 snd1 thr1 vul1 (own1 has NO arm zones — rack zones instead).
|
own1 snd1 thr1 vul1 (own1 has NO arm zones — rack zones instead).
|
||||||
|
|
||||||
|
**Weapon/combat benches need `map=grass time=day`.** `MP.EGG` authors
|
||||||
|
`map=cavern time=night`; a bench that copies it without the map sed (e.g. via
|
||||||
|
`bt_expert_egg` alone — mp_double.sh has this gap) parks the GOTO-driven mechs
|
||||||
|
against cavern rock and they shoot terrain for the whole window. Always sed the
|
||||||
|
map/time on the COPY, single-node and two-node alike.
|
||||||
|
|
||||||
|
### THE ZONE-WALK MATRIX (`scratchpad/night10/zonewalk.sh`) — systematic
|
||||||
|
### per-panel hit-location testing, operator-watchable (built for #124)
|
||||||
|
The precision-targeting rig: **two nodes**. Node B (launched first, back
|
||||||
|
window) is the TARGET — `BT_SPIN_SELF=15` rotates its own viewpoint mech in
|
||||||
|
place (deg/s; the BT_SPAWN_AT write pattern, applied per frame) so every
|
||||||
|
aspect passes the shooter's boresight; its window shows its own paper doll
|
||||||
|
taking the hits (`BT_DMG_LOG=1` prints every `[dmghit]` with zone + level).
|
||||||
|
Node A (foreground — it owns the pick focus) runs `BT_ZONE_WALK=<secs/zone>`:
|
||||||
|
walks the target's damage zones in index order, resolves each zone's CARRIER
|
||||||
|
SEGMENT's live world position (`BTResolveSegmentWorld` — the same resolver the
|
||||||
|
attached smoke rides; zone→segment via the `BTMechZoneSegAndName` bridge),
|
||||||
|
SERVOS the torso twist + aim elevation until the centered reticle's pick ray
|
||||||
|
(`BTGetAimRay(0,0)`) points at the segment, then pulses the laser trigger 3×
|
||||||
|
and advances. `[walk] ZONE/FIRE/HOLD` lines on A pair with `[dmghit]` on B.
|
||||||
|
Launch: `bash scratchpad/night10/zonewalk.sh` (relay included; NO kill timer —
|
||||||
|
the session stays up for observation; teardown = taskkill btl4).
|
||||||
|
Node A also carries `BT_ARMOR_LOG=1` + `BT_SHOT_EVERY=300 BT_SHOT_PREFIX=zwA`
|
||||||
|
(added for #87): the armour watcher's `[armor] ... level P -> Q` lines on A are
|
||||||
|
the OBSERVER-side receipt that B's zone levels replicated (the walk is the
|
||||||
|
standing zone-replication bench — A-side peaks must match B's `[dmghit]`
|
||||||
|
finals), and the numbered captures are the pixel record of the enemy hull
|
||||||
|
darkening. Digest: `python scratchpad/night10/digest_walk.py` (per-zone hits,
|
||||||
|
amt/hit, level first→last on B; replicant peaks seen by A; zone-repl lines).
|
||||||
|
Companion scalpels: `BT_ASPECT_TEST=1` (the zero-premise frame probe — 4
|
||||||
|
world-cardinal self-impacts + one at each weapon's physical MUZZLE, the
|
||||||
|
left/right anchors that caught the #124 reflection-vs-rotation error);
|
||||||
|
`BT_AIM_SWEEP=<period>` (blind elevation sweep — superseded by the walker).
|
||||||
|
Hard-won constraints baked into the walker (do not relearn these):
|
||||||
|
- **Engage gate**: servo only when the aim ray is live AND range < 150 AND
|
||||||
|
|bearing| < 1.1 rad; otherwise RELAX the twist toward 0 and hold the zone
|
||||||
|
clock. Without it the servo winds the twist to its limit while the goto
|
||||||
|
marches (limit-clamp fight = the mech visibly SHAKES) — and the gate must
|
||||||
|
sit well above the goto's resting bearing error (~0.55 rad at STOP=90) or
|
||||||
|
it deadlocks in HOLD.
|
||||||
|
- **Yaw polarity**: the twist cell's angular sense is OPPOSITE atan2(x,−z)
|
||||||
|
world yaw (operator-observed live: `+=` turned the torso AWAY — the second
|
||||||
|
witness for the #124 SelectSlice twist-sign flip). The servo starts at −1
|
||||||
|
and carries a growth WATCHDOG that self-flips if the error diverges.
|
||||||
|
- **Damped servo**: gain 0.40, step cap 0.025 rad/frame, deadband — the aim
|
||||||
|
ray lags the twist write by a frame; full-error correction oscillates.
|
||||||
|
- **Approach port**: dropzones spawn cross-map; the walker one-shot teleports
|
||||||
|
A to 100 u off the target (BT_SPAWN_AT pattern incl. the +500 y ground-snap
|
||||||
|
lift) instead of minutes of marching.
|
||||||
|
|
||||||
### Two-node bench skeleton (the MP pattern)
|
### Two-node bench skeleton (the MP pattern)
|
||||||
```bash
|
```bash
|
||||||
bt_assert_player_env
|
bt_assert_player_env
|
||||||
@@ -139,6 +189,21 @@ sleep <duration>; kill $relay; bt_kill_ours
|
|||||||
(one line, ungated) answer questions retroactively; per-frame traces stay
|
(one line, ungated) answer questions retroactively; per-frame traces stay
|
||||||
gated.
|
gated.
|
||||||
|
|
||||||
|
## Bench-script gotchas (each has burned a session)
|
||||||
|
- **Write benches CLEAN, never sed-derive a chain** — sed-derived copies silently dropped envs
|
||||||
|
twice (the [replgimp] silence); export envs INSIDE the per-node subshell.
|
||||||
|
- **Absolute paths everywhere; `cd /c/git/bt411` first** — the cwd trap (heredoc python + `cd
|
||||||
|
scratchpad/nightN`) has broken later builds/appends with `fatal: pathspec`/`cannot stat`.
|
||||||
|
- **No apostrophes/backticks through bash heredocs** — write a `.py` file and run it (quoting has
|
||||||
|
mangled posted tracker comments and killed scripts mid-parse).
|
||||||
|
- **Drain stale watchdogs before relaunching** — a leftover waiter's `taskkill` has killed a live
|
||||||
|
bench mid-run.
|
||||||
|
- **Scripted input goes through the REAL seam**: `BT_BTNTEST`/`BT_BTNTEST2` (RIO queue → mode-mask
|
||||||
|
drain). In 2-node runs press at poll ≥900 — round-start jitter can eat earlier presses.
|
||||||
|
- **Capture the viewpoint that can see the change** (gotcha #13 bullet): cockpit view can't show
|
||||||
|
your own legs; pair screenshots with a state probe and READ a frame before concluding.
|
||||||
|
- **Build ritual first** — see [[build-and-run]] §Build ritual (stale exe = phantom results).
|
||||||
|
|
||||||
## Key Relationships
|
## Key Relationships
|
||||||
- Uses: [[build-and-run]] (parity, env gates, BT_SHOT capture) · [[experience-levels]] (expert vs novice gating)
|
- Uses: [[build-and-run]] (parity, env gates, BT_SHOT capture) · [[experience-levels]] (expert vs novice gating)
|
||||||
- Informs: [[reconstruction-method]] (step 4 "verify honestly" — this file is the how)
|
- Informs: [[reconstruction-method]] (step 4 "verify honestly" — this file is the how)
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ Ranked by impact. **Reverse thrust `0x3F` (key LALT / pad B) is FIXED** and conf
|
|||||||
| 3 | **pad LT / RT (turn pedals)** — and every pad-only session | Steer the legs | Not a wiring gap: the pedals are live (`L4PADRIO.cpp:710-715` → `L4CTRL.cpp:1438-1446` → `btl4mppr.cpp:1337-1340` → `:1399 pedalsPosition`) but only *Standard/Veteran* read them (`mechmppr.cpp:1034`, `:1061`); the mapper boots in **Basic** (`mechmppr.cpp:361`) — and **the pad has no binding for `0x18` CycleControlMode**. Verified: `content/bindings.txt:74-89` binds only `0x40 0x3F 0x47 0x42 0x44 0x43 0x41`; no `0x18`, no `0x15` | **S** | `BT_MPPR_TRACE=1`, squeeze LT on a pad-only launch: `pedals=` moves, `turn=0`. Add `pad BACK button 0x18`, press it → `[mode] control mode -> 1`, then LT yaws |
|
| 3 | **pad LT / RT (turn pedals)** — and every pad-only session | Steer the legs | Not a wiring gap: the pedals are live (`L4PADRIO.cpp:710-715` → `L4CTRL.cpp:1438-1446` → `btl4mppr.cpp:1337-1340` → `:1399 pedalsPosition`) but only *Standard/Veteran* read them (`mechmppr.cpp:1034`, `:1061`); the mapper boots in **Basic** (`mechmppr.cpp:361`) — and **the pad has no binding for `0x18` CycleControlMode**. Verified: `content/bindings.txt:74-89` binds only `0x40 0x3F 0x47 0x42 0x44 0x43 0x41`; no `0x18`, no `0x15` | **S** | `BT_MPPR_TRACE=1`, squeeze LT on a pad-only launch: `pedals=` moves, `turn=0`. Add `pad BACK button 0x18`, press it → `[mode] control mode -> 1`, then LT yaws |
|
||||||
| 4 | **Fire dropout: releasing one alias of a held fire button** (`1`+`SPACE` on `0x40`; `3`+`LCTRL`+`RCTRL` on `0x47`; pad A/X on the same) | Two keys on one address = two wires to one button | Edge state is per **binding**, not per **address**: `previousKeyHeld[k]` is indexed by binding index and `EmitButton(addr, held)` fires on every per-binding edge (`engine/MUNGA_L4/L4PADRIO.cpp:580-595`; same shape in the pad loop `:665-685`). The release writes `-(addr+1)` (`L4CTRL.cpp:2635-2647`) and `ControlsUpdateManager::Update` then suppresses the redundant press, so fire stays **off** until the held key is released and re-pressed | **M** (shared plumbing — **defer**) | `BT_FIRE_LOG=1`: hold LCTRL (missiles firing), tap `3`, release `3` → fire stops with LCTRL still down |
|
| 4 | **Fire dropout: releasing one alias of a held fire button** (`1`+`SPACE` on `0x40`; `3`+`LCTRL`+`RCTRL` on `0x47`; pad A/X on the same) | Two keys on one address = two wires to one button | Edge state is per **binding**, not per **address**: `previousKeyHeld[k]` is indexed by binding index and `EmitButton(addr, held)` fires on every per-binding edge (`engine/MUNGA_L4/L4PADRIO.cpp:580-595`; same shape in the pad loop `:665-685`). The release writes `-(addr+1)` (`L4CTRL.cpp:2635-2647`) and `ControlsUpdateManager::Update` then suppresses the redundant press, so fire stays **off** until the held key is released and re-pressed | **M** (shared plumbing — **defer**) | `BT_FIRE_LOG=1`: hold LCTRL (missiles firing), tap `3`, release `3` → fire stops with LCTRL still down |
|
||||||
| 5 | **`0x1A`-`0x1D`** Generator A-D ON/OFF (4 radar-rail buttons) | Take a generator off line (heat/power management) | `Generator : HeatSink` (`powersub.hpp:320-322`) and **no `Generator::GetMessageHandlers()` exists** — `powersub.cpp:1014` resolves to HeatSink's set `{id 3 ToggleCooling}`, so streamed `msg 4` (`ctrlmap.log:44-47`) finds nothing. `ToggleGeneratorOnOff` @`004b1ed0` is unreconstructed (0 hits in `game/`) | **M** | Click `0x1A`: today zero log. After: generator output voltage → 0 and its ENG-page bar follows |
|
| 5 | **`0x1A`-`0x1D`** Generator A-D ON/OFF (4 radar-rail buttons) | Take a generator off line (heat/power management) | `Generator : HeatSink` (`powersub.hpp:320-322`) and **no `Generator::GetMessageHandlers()` exists** — `powersub.cpp:1014` resolves to HeatSink's set `{id 3 ToggleCooling}`, so streamed `msg 4` (`ctrlmap.log:44-47`) finds nothing. `ToggleGeneratorOnOff` @`004b1ed0` is unreconstructed (0 hits in `game/`) | **M** | Click `0x1A`: today zero log. After: generator output voltage → 0 and its ENG-page bar follows |
|
||||||
| 6 | **`0x13`** CROUCH (manual p8, a full section) | Duck to present a smaller target | Streamed `subsys -1 msg 0x1a` (`ctrlmap.log:40`) resolves to the Mech (`ENTITY.cpp:608-615`), but `Mech::MessageHandlerEntries` has exactly three entries — `TakeDamage`, `PlayerLink`, `BalanceCoolant` (`mech.cpp:463-467`). No id `0x1a`. Assets exist (`Mech::duckState` attr 0x37 `mech.cpp:861`, SQUAT clips, `DuckServo01.wav`) | **M** (reconstruct @`0049fa00`) | Click `0x13` on a stationary mech: expect the squat clip + `duckState 0→1` |
|
| 6 | **`0x13`** CROUCH (manual p8, a full section) | Duck to present a smaller target | Streamed `subsys -1 msg 0x1a` (`ctrlmap.log:40`) resolves to the Mech (`ENTITY.cpp:608-615`), but `Mech::MessageHandlerEntries` has exactly three entries — `TakeDamage`, `PlayerLink`, `BalanceCoolant` (`mech.cpp:463-467`). No id `0x1a`. Assets exist (`Mech::duckState` attr 0x37 `mech.cpp:861`, SQUAT clips, `DuckServo01.wav`) | ~~M~~ ✅ **COMPLETE** (handler 07-26; consumer + clips + MP 2026-08-06 — [[locomotion]] §CROUCH) | Verified: squat clip plays, holds, rises; peers replicate it |
|
||||||
| 7 | **Torso recenter from the keyboard** (`0x42` is pad/mouse only) | Center a twisted torso — the manual's anti-disorientation control (p9) | `0x42` itself is **LIVE** (streamed `subsys 17 attr 14 -> +0x208` = `Torso::centerCommand`, consumed `torso.cpp:636-640`) but `content/bindings.txt:83,86` bind it only on `pad Y`/`DPAD_UP`. The port's keyboard path (`gBTTorsoRecenter`, `mech4.cpp:2875`) is fed from the zeroed `gBTInput` and its consumer sits inside the stood-down bridge (`mechmppr.cpp:854-863`, gate `:655-660`) | **S** | Twist with `E`, press `X`: `currentTwist` does not return to 0. Add `key X button 0x42` (two rows on one VK both fire — `L4PADRIO.cpp:580-629` iterates all bindings) |
|
| 7 | **Torso recenter from the keyboard** (`0x42` is pad/mouse only) | Center a twisted torso — the manual's anti-disorientation control (p9) | `0x42` itself is **LIVE** (streamed `subsys 17 attr 14 -> +0x208` = `Torso::centerCommand`, consumed `torso.cpp:636-640`) but `content/bindings.txt:83,86` bind it only on `pad Y`/`DPAD_UP`. The port's keyboard path (`gBTTorsoRecenter`, `mech4.cpp:2875`) is fed from the zeroed `gBTInput` and its consumer sits inside the stood-down bridge (`mechmppr.cpp:854-863`, gate `:655-660`) | **S** | Twist with `E`, press `X`: `currentTwist` does not return to 0. Add `key X button 0x42` (two rows on one VK both fire — `L4PADRIO.cpp:580-629` iterates all bindings) |
|
||||||
| 8 | **Any clickable button latches if the mouse-up is lost** (worst on `0x3F` reverse, `0x40` trigger) | A momentary click always releases | Neither WndProc handles `WM_CAPTURECHANGED` or `WM_KILLFOCUS` — grep over `engine/` + `game/` returns **zero** hits. Alt-tab or a system dialog between press and release leaves `gCkPressed` set and `SetScreenButton(a,0)` never runs | **M** — trap: `btl4main.cpp:137` calls `ReleaseCapture()` itself, which re-enters `WM_CAPTURECHANGED`; guard on `gCkPressed != -1` (it is cleared before `ReleaseCapture`) | Click-hold `0x3F`, alt-tab, release outside the window: `BT_PAD_LOG=1` shows no release line and `rev` stays 1 |
|
| 8 | **Any clickable button latches if the mouse-up is lost** (worst on `0x3F` reverse, `0x40` trigger) | A momentary click always releases | Neither WndProc handles `WM_CAPTURECHANGED` or `WM_KILLFOCUS` — grep over `engine/` + `game/` returns **zero** hits. Alt-tab or a system dialog between press and release leaves `gCkPressed` set and `SetScreenButton(a,0)` never runs | **M** — trap: `btl4main.cpp:137` calls `ReleaseCapture()` itself, which re-enters `WM_CAPTURECHANGED`; guard on `gCkPressed != -1` (it is cleared before `ReleaseCapture`) | Click-hold `0x3F`, alt-tab, release outside the window: `BT_PAD_LOG=1` shows no release line and `rev` stays 1 |
|
||||||
| 9 | **Every HOTAS / flight stick / rudder** (whole `joydev/joyaxis/joybutton/joyhat` grammar) | Drive the pod channels from a DirectInput device | `content/bindings.txt` is 89 lines and has **zero** joy rows; the joy poll is gated on `joyAxisBindingCount>0 \|\| …` (`L4PADRIO.cpp:775-777`) so `BTJoyInit/BTJoyPoll` are never called. `PadBindingProfile::Load` writes the default only when the file is **absent** (`L4PADBINDINGS.cpp:708-713`), and the file is gitignored (`.gitignore:23`) — so the zip ships **this machine's stale file** | **S** now (ship without it / refresh it) · **M** later (version marker + additive merge) | `content/jstest.log`: `bindings loaded: 40 keys, 10 pad buttons, 5 pad axes, 0 joy axes…`; no `[joy]` line exists in any log in `content/` |
|
| 9 | **Every HOTAS / flight stick / rudder** (whole `joydev/joyaxis/joybutton/joyhat` grammar) | Drive the pod channels from a DirectInput device | `content/bindings.txt` is 89 lines and has **zero** joy rows; the joy poll is gated on `joyAxisBindingCount>0 \|\| …` (`L4PADRIO.cpp:775-777`) so `BTJoyInit/BTJoyPoll` are never called. `PadBindingProfile::Load` writes the default only when the file is **absent** (`L4PADBINDINGS.cpp:708-713`), and the file is gitignored (`.gitignore:23`) — so the zip ships **this machine's stale file** | **S** now (ship without it / refresh it) · **M** later (version marker + additive merge) | `content/jstest.log`: `bindings loaded: 40 keys, 10 pad buttons, 5 pad axes, 0 joy axes…`; no `[joy]` line exists in any log in `content/` |
|
||||||
|
|||||||
@@ -404,3 +404,21 @@ design change for its own pass. `killCount`/`deathTally` are still zeroed only i
|
|||||||
column should read, `+0x280` vs `deathCount`), §Headline-5 (last-hitter-takes-all and
|
column should read, `+0x280` vs `deathCount`), §Headline-5 (last-hitter-takes-all and
|
||||||
ram kills), and §Headline-6's fidelity question (the wrong-column slip is still
|
ram kills), and §Headline-6's fidelity question (the wrong-column slip is still
|
||||||
reproduced; it is merely no longer visible).
|
reproduced; it is merely no longer visible).
|
||||||
|
|
||||||
|
## Addendum 2026-08-05 — the AUTHENTIC report tail lands (#45/#134)
|
||||||
|
The kill/damage report sender is no longer the BTPostDamageScore/BTPostKillScore stand-in pair:
|
||||||
|
`Mech::TakeDamageMessageHandler`'s dark-gap tail (@0x4a02f4-0x4a0890) is reconstructed as
|
||||||
|
`BTMechPostCombatReports` + `BTMechPostVehicleDead` (btplayer.cpp), dispatching the binary's three
|
||||||
|
id-0x16 reports (kill to shooter / type-0 wire-fidelity / received to victim) and the BT-extended
|
||||||
|
0x38 VehicleDead {killed-by player, kill zone} from the victim's handler at the binary's exact
|
||||||
|
positions. Consequences for this plan:
|
||||||
|
- KILLS credit path unchanged in shape (victim node -> replicant-player Dispatch reroute ->
|
||||||
|
killer's master ++killCount) — re-benched cross-node (scorekill.sh: kills=1, award=4.88).
|
||||||
|
- Suicides now dispatch and the handler negates the award (#134 panic penalty live;
|
||||||
|
scoreself.sh: type=2 award=-39.00 kills=0).
|
||||||
|
- Per-hit INFLICTED credit is REMOVED — it never existed in 1995 (@0x4c0200 is bound in no
|
||||||
|
handler-table entry; scoring = kill awards + received penalties + death costs). SCORE column
|
||||||
|
behavior changes accordingly; KILLS/DEATHS columns unaffected.
|
||||||
|
- §Headline-6's wrong-column slip (dual +0x27c increment) remains faithfully reproduced and
|
||||||
|
replication-masked. The type-1 received report also fixes the console VTVDamaged
|
||||||
|
points_transfered field (was a Now() misread; it is Round(award), raw disasm @0x4c04a7).
|
||||||
|
|||||||
@@ -255,3 +255,26 @@ Keep all three unconditional (matchlog only arms on `-net`, `matchlog.cpp:66-76`
|
|||||||
8. **Don't reconstruct dtor glue** while touching the heat/power chain terminus; the trailing base-dtor calls run the chain twice.
|
8. **Don't reconstruct dtor glue** while touching the heat/power chain terminus; the trailing base-dtor calls run the chain twice.
|
||||||
9. **`AmmoBin +0x228`** is written to 0 by `@004bd26c` alongside `feedTimer`/`cookOffArmed` and is the object's last dword (`sizeof == 0x22C`); our `ammobin.hpp:270` calls it `reserved // unused`. It is a live per-feed latch — identify it from `AmmoBinSimulation @004bd394` / `FeedAmmo @004bd4f4` before shipping the refill move, or copy the write blind and flag it [T3].
|
9. **`AmmoBin +0x228`** is written to 0 by `@004bd26c` alongside `feedTimer`/`cookOffArmed` and is the object's last dword (`sizeof == 0x22C`); our `ammobin.hpp:270` calls it `reserved // unused`. It is a live per-feed latch — identify it from `AmmoBinSimulation @004bd394` / `FeedAmmo @004bd4f4` before shipping the refill move, or copy the write blind and flag it [T3].
|
||||||
10. **Roster-order sensitivity is a non-risk.** `PoweredSubsystem::RTIS @004b0e6c` gates the electrical restore on the source Generator's `stateAlarm == 2`, but `Generator @004b215c` sets level 2 **unconditionally** (`0x4b2194`) and the death sweep already ran `DeathReset(0)` over the whole roster with no index skip — so no authored segment order can produce a partial re-arm. Do not add ordering logic to `Mech::Reset`.
|
10. **Roster-order sensitivity is a non-risk.** `PoweredSubsystem::RTIS @004b0e6c` gates the electrical restore on the source Generator's `stateAlarm == 2`, but `Generator @004b215c` sets level 2 **unconditionally** (`0x4b2194`) and the death sweep already ran `DeathReset(0)` over the whole roster with no index skip — so no authored segment order can produce a partial re-arm. Do not add ordering logic to `Mech::Reset`.
|
||||||
|
---
|
||||||
|
|
||||||
|
# ADDENDUM 2026-08-04 — the full-reset audit (operator request): valves, gensel, MFD modes, spawn smoke
|
||||||
|
|
||||||
|
Steps 0-5 above all landed via #55 and are byte-consistent (re-audited today).
|
||||||
|
Four operator/field reports were then grounded and dispositioned:
|
||||||
|
|
||||||
|
| report | binary ground truth | disposition |
|
||||||
|
|---|---|---|
|
||||||
|
| coolant valves survive respawn | `@004ae534` (MISSING from the Ghidra export -- raw disasm): chains **HeatSink** `0x4ad760` (coolant refill RUNS), then arg-gated `valveState=1` + `massScale=refrigerationFactor`; `Mech::Reset` tail (`call 0x49f788`) re-runs RecomputeCondenserValves | **PORT GAP -> FIXED** (heat.cpp authentic body + mech4.cpp Reset tail call). Bench: detent 5 -> death -> `[respawn] Condenser1 valve detent 5 -> 1` |
|
||||||
|
| generator switching survives | `@004b215c` sets `generatorOn=1` (landed, #55); `@004b0e6c` only RESOLVES the source link -- **the weapon->generator taps are never rewritten** | gens re-light = already correct; **taps persisting = AUTHENTIC** |
|
||||||
|
| MFD display mode / control mode survive | mapper vtables `0050f45c` + `0051e440` slots 8-11 read from the shipped exe = plain root bodies -- **displayMode/controlMode have NO reset anywhere** | **AUTHENTIC persist** -- do not "fix" |
|
||||||
|
| #129 fresh spawns smoke briefly | the pod's respawn ran the per-entity effect cleanup `@004d0c14` | **PORT GAP -> FIXED**: only the master's Mech::Reset ran the cleanup; the REPLICANT un-wreck edge (mechdmg.cpp MechDeathHandler::Tick) rebuilt without it, so the observer's last 10s plume window rode the teleport (peers-only symptom). BTStopEntityPfx now runs on the un-wreck edge |
|
||||||
|
|
||||||
|
Bench: `scratchpad/night11/respawnreset.sh` (BT_VALVE_TEST press -> BT_MP_FORCE_DMG
|
||||||
|
kill -> read `[valve]`/`[respawn]` on A, plume/un-wreck ordering on B).
|
||||||
|
|
||||||
|
## Addendum 2026-08-05 (#45 report tail): the VehicleDead SENDER moved
|
||||||
|
The death-transition dispatch site (mech4) is retired: the decoded sender is the TakeDamage
|
||||||
|
handler's death tail (@0x4a07d4-0x4a0890), now BTMechPostVehicleDead (btplayer.cpp) with the
|
||||||
|
BT 0x38-byte extension {killed-by player, kill zone}. The #55 NULL-playerLink fallback + the
|
||||||
|
DEAD_NOTIFY forensics moved into it intact; the #81 single-dispatch rule is unchanged. The
|
||||||
|
flow diagram above predates this -- read BTPostKillScore rows as historical (retired 08-05).
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ ScenarioRole::ScenarioRole(const CString &role_name, const CString &model_file)
|
|||||||
resourceAddress;
|
resourceAddress;
|
||||||
|
|
||||||
killBonus = player_data->killBonus;
|
killBonus = player_data->killBonus;
|
||||||
|
specialCaseDeathPenalty = player_data->specialCaseDeathPenalty; // BT 4.10 (restored)
|
||||||
returnFromDeath = player_data->returnFromDeath;
|
returnFromDeath = player_data->returnFromDeath;
|
||||||
damageReceivedModifier = player_data->damageReceivedModifier;
|
damageReceivedModifier = player_data->damageReceivedModifier;
|
||||||
damageInflictedModifier = player_data->damageInflictedModifier;
|
damageInflictedModifier = player_data->damageInflictedModifier;
|
||||||
@@ -64,6 +65,7 @@ ScenarioRole::ScenarioRole(const CString &role_name, const CString &model_file)
|
|||||||
damageBias = 0.0f;
|
damageBias = 0.0f;
|
||||||
friendlyFirePenalty = 0.0f;
|
friendlyFirePenalty = 0.0f;
|
||||||
killBonus = 0.0f;
|
killBonus = 0.0f;
|
||||||
|
specialCaseDeathPenalty = 0.0f; // BT 4.10 (restored)
|
||||||
returnFromDeath = 0;
|
returnFromDeath = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,6 +153,20 @@ Dump_And_Die:
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BT 4.10: read right after KillBonus, fail-hard like every other key
|
||||||
|
// (binary role reader @00429bec, dest = record slot [1]).
|
||||||
|
if(
|
||||||
|
!model_file->GetEntry(
|
||||||
|
"gamedata",
|
||||||
|
"SpecialCaseDeathPenalty",
|
||||||
|
&local_model->specialCaseDeathPenalty
|
||||||
|
)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
std::cerr << model_name << "Missing SpecialCaseDeathPenalty" << std::endl;
|
||||||
|
goto Dump_And_Die;
|
||||||
|
}
|
||||||
|
|
||||||
if(
|
if(
|
||||||
!model_file->GetEntry(
|
!model_file->GetEntry(
|
||||||
"gamedata",
|
"gamedata",
|
||||||
|
|||||||
+13
-1
@@ -9,7 +9,12 @@ class NotationFile;
|
|||||||
|
|
||||||
struct ScenarioRole__ModelResource
|
struct ScenarioRole__ModelResource
|
||||||
{
|
{
|
||||||
Scalar killBonus, damageReceivedModifier, damageInflictedModifier, damageBias, friendlyFirePenalty;
|
// BT 4.10 carries specialCaseDeathPenalty as record slot [1], right after
|
||||||
|
// killBonus (role reader @00429bec dest offsets; the 2007 WinTesla source
|
||||||
|
// dropped the BT-only field -- restored 2026-08-02, the DefaultRendererRate
|
||||||
|
// pattern). Consumed by the BT death handler tail (@004c07cd): every death
|
||||||
|
// with advancedDamageOn costs -specialCaseDeathPenalty score.
|
||||||
|
Scalar killBonus, specialCaseDeathPenalty, damageReceivedModifier, damageInflictedModifier, damageBias, friendlyFirePenalty;
|
||||||
int returnFromDeath;
|
int returnFromDeath;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -20,6 +25,7 @@ public:
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
Scalar damageInflictedModifier, damageReceivedModifier, friendlyFirePenalty, damageBias, killBonus;
|
Scalar damageInflictedModifier, damageReceivedModifier, friendlyFirePenalty, damageBias, killBonus;
|
||||||
|
Scalar specialCaseDeathPenalty; // BT 4.10 role+0x20 (restored 2026-08-02)
|
||||||
CString roleName;
|
CString roleName;
|
||||||
|
|
||||||
int returnFromDeath;
|
int returnFromDeath;
|
||||||
@@ -61,6 +67,12 @@ public:
|
|||||||
return killBonus;
|
return killBonus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Scalar GetSpecialCaseDeathPenalty() const // BT 4.10 role+0x20
|
||||||
|
{
|
||||||
|
Check(this);
|
||||||
|
return specialCaseDeathPenalty;
|
||||||
|
}
|
||||||
|
|
||||||
CString GetRoleName() const
|
CString GetRoleName() const
|
||||||
{
|
{
|
||||||
Check(this);
|
Check(this);
|
||||||
|
|||||||
@@ -9,6 +9,10 @@
|
|||||||
#include "dxutils.h"
|
#include "dxutils.h"
|
||||||
#ifdef BT_GLASS
|
#ifdef BT_GLASS
|
||||||
#include "l4padrio.h"
|
#include "l4padrio.h"
|
||||||
|
#ifdef BT_GLASS
|
||||||
|
#include "l4glasswin.h" // the per-display MFD windows -- created below for
|
||||||
|
// EITHER RIO (they used to ride the PadRIO ctor)
|
||||||
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -865,6 +869,29 @@ LBE4ControlsManager::LBE4ControlsManager():
|
|||||||
while (temp[0] != '\0');
|
while (temp[0] != '\0');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//--------------------------------------------------
|
||||||
|
// The per-display MFD windows (2026-08-06)
|
||||||
|
//--------------------------------------------------
|
||||||
|
// These used to be created ONLY at the end of the PadRIO constructor,
|
||||||
|
// because they began life as the frames around the on-screen RIO button
|
||||||
|
// banks -- "the buttons ride the device". On a real cab that coupling is
|
||||||
|
// backwards: selecting the HARDWARE RIO means PadRIO is never built, so
|
||||||
|
// the MFD panels never appeared at all. Found on the crash cart, where
|
||||||
|
// the mission came up on the main view with every MFD dark.
|
||||||
|
//
|
||||||
|
// The windows are a DISPLAY concern, so create them here -- after the
|
||||||
|
// L4CONTROLS parse, where every device branch converges -- and let them
|
||||||
|
// exist whichever RIO won. In pod surface mode there are no on-screen
|
||||||
|
// buttons on them at all. Create() is idempotent (`gWinCount != 0`
|
||||||
|
// returns), so the PadRIO call site stays valid and simply arrives first.
|
||||||
|
//
|
||||||
|
#ifdef BT_GLASS
|
||||||
|
if (rioPointer != NULL && BTGlassPanelsActive())
|
||||||
|
{
|
||||||
|
BTGlassPanels_Create();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
// Clear all lamps (and reset analog I/O)
|
// Clear all lamps (and reset analog I/O)
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
@@ -1089,6 +1116,17 @@ LBE4ControlsManager::~LBE4ControlsManager()
|
|||||||
rioPointer = NULL;
|
rioPointer = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef BT_GLASS
|
||||||
|
//
|
||||||
|
// Symmetric with the Create above (2026-08-06). ~PadRIO tears the panels
|
||||||
|
// down itself, so this is a no-op there -- but the HARDWARE RIO's dtor
|
||||||
|
// knows nothing about them, and windows that outlive the surfaces they
|
||||||
|
// blit are a crash waiting for the next mission cycle. Destroy is safe
|
||||||
|
// if they were never created.
|
||||||
|
//
|
||||||
|
BTGlassPanels_Destroy();
|
||||||
|
#endif
|
||||||
|
|
||||||
Check_Fpu();
|
Check_Fpu();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2624,12 +2662,28 @@ void
|
|||||||
Verify(rio_event.Data.Unit < ButtonCount);
|
Verify(rio_event.Data.Unit < ButtonCount);
|
||||||
temp = rio_event.Data.Unit + 1;
|
temp = rio_event.Data.Unit + 1;
|
||||||
|
|
||||||
|
// DIAG (BT_DUCK_LOG): the mode mask a press dispatches under -- the
|
||||||
|
// crouch-button (0x13) MP-delivery hunt (mode-gated streamed rows).
|
||||||
|
if (getenv("BT_DUCK_LOG"))
|
||||||
|
DEBUG_STREAM << "[duck] RIO press unit=0x" << std::hex
|
||||||
|
<< (int)rio_event.Data.Unit << " modeMask=0x"
|
||||||
|
<< (unsigned)mode_mask << std::dec << "\n" << std::flush;
|
||||||
|
|
||||||
Check(&buttonGroup[rio_event.Data.Unit]);
|
Check(&buttonGroup[rio_event.Data.Unit]);
|
||||||
buttonGroup[rio_event.Data.Unit].Update(&temp, mode_mask);
|
buttonGroup[rio_event.Data.Unit].Update(&temp, mode_mask);
|
||||||
//----------------------------------------
|
//----------------------------------------
|
||||||
// Save 'pressed' mode mask for 'release'
|
// Save 'pressed' mode mask for 'release'
|
||||||
//----------------------------------------
|
//----------------------------------------
|
||||||
buttonActivateModeMask[rio_event.Data.Unit] = mode_mask;
|
buttonActivateModeMask[rio_event.Data.Unit] = mode_mask;
|
||||||
|
|
||||||
|
// (NO eng-page eject-key hook: REMOVED 2026-08-03 for strict binary
|
||||||
|
// fidelity. The soft keys 0x0B/0x23/0x03 never pilot-eject in the
|
||||||
|
// binary -- every page routes them to authored functions (weapon eng
|
||||||
|
// pages: EjectAmmo 0xB -- a hook here HIJACKED the unjam while armed,
|
||||||
|
// self-destructing a pilot trying to clear a jam). The flashing
|
||||||
|
// engEject lamp is the INVITE; the authored press is the pilot KEYPAD
|
||||||
|
// bank / PANIC key, both live on the desktop.)
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case RIO::ButtonReleasedEvent:
|
case RIO::ButtonReleasedEvent:
|
||||||
@@ -2670,6 +2724,16 @@ void
|
|||||||
);
|
);
|
||||||
|
|
||||||
Check(&keyboardGroup[rio_event.Data.Keyboard.Unit]);
|
Check(&keyboardGroup[rio_event.Data.Keyboard.Unit]);
|
||||||
|
{
|
||||||
|
static int s_klog = -1;
|
||||||
|
if (s_klog < 0) s_klog = (getenv("BT_PAD_LOG") != 0) ? 1 : 0;
|
||||||
|
if (s_klog)
|
||||||
|
DEBUG_STREAM << "[lbe4key] unit "
|
||||||
|
<< (int)rio_event.Data.Keyboard.Unit
|
||||||
|
<< " key " << (int)new_key
|
||||||
|
<< " mode 0x" << std::hex << (unsigned)mode_mask
|
||||||
|
<< std::dec << "\n" << std::flush;
|
||||||
|
}
|
||||||
keyboardGroup[rio_event.Data.Keyboard.Unit].
|
keyboardGroup[rio_event.Data.Keyboard.Unit].
|
||||||
ForceUpdate(&new_key, mode_mask);
|
ForceUpdate(&new_key, mode_mask);
|
||||||
|
|
||||||
|
|||||||
@@ -443,8 +443,13 @@ d3d_OBJECT* d3d_OBJECT::LoadObjectBGF(LPDIRECT3DDEVICE9 device, char *fileName)
|
|||||||
// drawAsSky) and vanishes -- DECLOUDS carries vertex alpha but must
|
// drawAsSky) and vanishes -- DECLOUDS carries vertex alpha but must
|
||||||
// stay in the sky pass.
|
// stay in the sky pass.
|
||||||
const bool vtxAlphaOp = data.batches[i].vertexAlpha && !isSkyObj;
|
const bool vtxAlphaOp = data.batches[i].vertexAlpha && !isSkyObj;
|
||||||
object->mDrawOps[i].alphaTest = (object->mIsShadow != 0) || vtxAlphaOp;
|
// BTFX brighten veil (spot cone): route to the blend pass like the
|
||||||
|
// vertex-alpha cards; the draw-state branch in DrawMesh adds
|
||||||
|
// dest += emissive x factor instead of SRCALPHA/INVSRCALPHA.
|
||||||
|
const bool brightenOp = data.batches[i].brightenFactor > 0.0f && !isSkyObj;
|
||||||
|
object->mDrawOps[i].alphaTest = (object->mIsShadow != 0) || vtxAlphaOp || brightenOp;
|
||||||
object->mDrawOps[i].vertexAlphaBlend = vtxAlphaOp;
|
object->mDrawOps[i].vertexAlphaBlend = vtxAlphaOp;
|
||||||
|
object->mDrawOps[i].brightenAlpha = brightenOp ? data.batches[i].brightenFactor : 0.0f;
|
||||||
object->mDrawOps[i].drawAsDecal = false;
|
object->mDrawOps[i].drawAsDecal = false;
|
||||||
object->mDrawOps[i].drawAsSky = isSkyObj; // sky dome -> PASS_SKY (fullbright)
|
object->mDrawOps[i].drawAsSky = isSkyObj; // sky dome -> PASS_SKY (fullbright)
|
||||||
// PUNCH (dpl_Punchize; BT_PUNCH=0 disables): cutout batch -- black texels
|
// PUNCH (dpl_Punchize; BT_PUNCH=0 disables): cutout batch -- black texels
|
||||||
@@ -1279,7 +1284,9 @@ void d3d_OBJECT::DrawMesh(int pass, const D3DXMATRIX *viewTransform, Time target
|
|||||||
// (SRCALPHA/INVSRCALPHA). Runs only in the blend pass (the op filter
|
// (SRCALPHA/INVSRCALPHA). Runs only in the blend pass (the op filter
|
||||||
// above routed it there; ALPHABLEND is on and z-write off pass-wide).
|
// above routed it there; ALPHABLEND is on and z-write off pass-wide).
|
||||||
DWORD sVLight = 0, sVSrc = 0, sVDst = 0, sVCop = 0, sVCa1 = 0, sVCa2 = 0, sVAop = 0, sVAa2 = 0;
|
DWORD sVLight = 0, sVSrc = 0, sVDst = 0, sVCop = 0, sVCa1 = 0, sVCa2 = 0, sVAop = 0, sVAa2 = 0;
|
||||||
const bool vtxAlphaCard = (drawOp->vertexAlphaBlend && pass == PASS_ALPHABLEND);
|
const bool brightenCard = (drawOp->brightenAlpha > 0.0f && pass == PASS_ALPHABLEND);
|
||||||
|
const bool vtxAlphaCard =
|
||||||
|
((drawOp->vertexAlphaBlend && pass == PASS_ALPHABLEND) || brightenCard);
|
||||||
if (vtxAlphaCard)
|
if (vtxAlphaCard)
|
||||||
{
|
{
|
||||||
mDevice->GetRenderState(D3DRS_LIGHTING, &sVLight);
|
mDevice->GetRenderState(D3DRS_LIGHTING, &sVLight);
|
||||||
@@ -1292,13 +1299,35 @@ void d3d_OBJECT::DrawMesh(int pass, const D3DXMATRIX *viewTransform, Time target
|
|||||||
mDevice->GetTextureStageState(0, D3DTSS_ALPHAARG2, &sVAa2);
|
mDevice->GetTextureStageState(0, D3DTSS_ALPHAARG2, &sVAa2);
|
||||||
mDevice->SetRenderState(D3DRS_LIGHTING, FALSE);
|
mDevice->SetRenderState(D3DRS_LIGHTING, FALSE);
|
||||||
mDevice->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
|
mDevice->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
|
||||||
mDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
|
mDevice->SetRenderState(D3DRS_DESTBLEND,
|
||||||
|
brightenCard ? D3DBLEND_ONE : D3DBLEND_INVSRCALPHA);
|
||||||
|
if (brightenCard)
|
||||||
|
{
|
||||||
|
// BTFX brighten veil: dest += vertexRGB x factor. SPOT.BGF's
|
||||||
|
// verts author the beam tint (cyan-white 0.12/0.98/1.0, alpha
|
||||||
|
// 1.0 -- no fade gradient); the factor rides TFACTOR alpha.
|
||||||
|
// [T3 tint compose]: the night material ALSO authors emissive
|
||||||
|
// (0.9,0.4,0.2 warm) -- whether the DIV board tinted the veil
|
||||||
|
// by vertex, emissive, or their product is unconfirmed; vertex
|
||||||
|
// (a white-blue searchlight) matches the authored geometry
|
||||||
|
// gradient model. Flip here if era-look evidence says warm.
|
||||||
|
mDevice->SetRenderState(D3DRS_TEXTUREFACTOR,
|
||||||
|
D3DCOLOR_COLORVALUE(1.0f, 1.0f, 1.0f, drawOp->brightenAlpha));
|
||||||
|
mDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
|
||||||
|
mDevice->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_DIFFUSE);
|
||||||
|
mDevice->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_TFACTOR);
|
||||||
|
mDevice->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG2);
|
||||||
|
mDevice->SetTextureStageState(0, D3DTSS_ALPHAARG2, D3DTA_TFACTOR);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
mDevice->SetTextureStageState(0, D3DTSS_COLOROP,
|
mDevice->SetTextureStageState(0, D3DTSS_COLOROP,
|
||||||
drawOp->texture.texture != NULL ? D3DTOP_MODULATE : D3DTOP_SELECTARG2);
|
drawOp->texture.texture != NULL ? D3DTOP_MODULATE : D3DTOP_SELECTARG2);
|
||||||
mDevice->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
|
mDevice->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
|
||||||
mDevice->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE);
|
mDevice->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE);
|
||||||
mDevice->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG2);
|
mDevice->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG2);
|
||||||
mDevice->SetTextureStageState(0, D3DTSS_ALPHAARG2, D3DTA_DIFFUSE);
|
mDevice->SetTextureStageState(0, D3DTSS_ALPHAARG2, D3DTA_DIFFUSE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool copCut = (drawOp->copRole == 2 && iOp > 0
|
const bool copCut = (drawOp->copRole == 2 && iOp > 0
|
||||||
|
|||||||
+606
-15
@@ -115,6 +115,18 @@ struct GWin
|
|||||||
int wantX, wantY;
|
int wantX, wantY;
|
||||||
int restored; // wantX/wantY came from glass_layout.cfg -> don't re-snap
|
int restored; // wantX/wantY came from glass_layout.cfg -> don't re-snap
|
||||||
int noFrame; // ",noframe" in the cfg -> WS_POPUP, drag by the surface
|
int noFrame; // ",noframe" in the cfg -> WS_POPUP, drag by the surface
|
||||||
|
|
||||||
|
// RGB CHANNEL GROUP (BT_POD_RGB): on splitter-wired pod glass this window
|
||||||
|
// is one VGA PORT feeding up to three mono monitors through R/G/B. These
|
||||||
|
// are the OTHER surfaces sharing the port; each is composited into its own
|
||||||
|
// channel by BlitSurface. groupCount 0 = ordinary single-surface window.
|
||||||
|
const char *groupPort[3];
|
||||||
|
const char *groupAlt[3]; // the Eng<n> twin, or NULL
|
||||||
|
int groupCount;
|
||||||
|
|
||||||
|
char monitorName[40]; // the PHYSICAL monitor this window landed on
|
||||||
|
// (\.\DISPLAYn), stamped at creation; shown
|
||||||
|
// by BT_POD_IDENT so the cab can be mapped.
|
||||||
};
|
};
|
||||||
|
|
||||||
static GWin gWins[8];
|
static GWin gWins[8];
|
||||||
@@ -124,7 +136,8 @@ static int pressedAddress = -1;
|
|||||||
static unsigned char latched[128];
|
static unsigned char latched[128];
|
||||||
static int gRadarRot = 3; // BT_GAUGE_SEC_ROT (default CW, upright)
|
static int gRadarRot = 3; // BT_GAUGE_SEC_ROT (default CW, upright)
|
||||||
static bool sRepositionedToMain = false;
|
static bool sRepositionedToMain = false;
|
||||||
static unsigned long *gStage = NULL; // 640*480 BGRA staging (shared, main-thread)
|
static unsigned long *gStage = NULL; // 640*480 BGRA staging (shared, main-thread)
|
||||||
|
static unsigned long *gStage2 = NULL; // RGB composite scratch (BT_POD_RGB)
|
||||||
|
|
||||||
static HFONT titleFont = NULL;
|
static HFONT titleFont = NULL;
|
||||||
static HFONT buttonFont = NULL;
|
static HFONT buttonFont = NULL;
|
||||||
@@ -244,17 +257,26 @@ static void
|
|||||||
// (0x15 sits on the left column -- Cyd listed 0x10-0x14; move it if wanted.)
|
// (0x15 sits on the left column -- Cyd listed 0x10-0x14; move it if wanted.)
|
||||||
static const int bottomAddrs[4] = { 0x16, 0x17, 0x1F, 0x1E };
|
static const int bottomAddrs[4] = { 0x16, 0x17, 0x1F, 0x1E };
|
||||||
|
|
||||||
|
// The SURFACE SIZE follows the rotation: a quarter turn (1 or 3) transposes
|
||||||
|
// the 640x480 source to 480x640 portrait -- the pod's rotated CRT -- while
|
||||||
|
// 0 (none) and 2 (180) keep it LANDSCAPE. Sizing this independently of the
|
||||||
|
// rotation squashed the picture into the wrong aspect on a landscape
|
||||||
|
// replacement panel (crash cart, 2026-08-06).
|
||||||
|
const int quarterTurn = (gRadarRot == 1 || gRadarRot == 3);
|
||||||
|
const int surfW = quarterTurn ? RadarSurfW : RadarSurfH; // 480 : 640
|
||||||
|
const int surfH = quarterTurn ? RadarSurfH : RadarSurfW; // 640 : 480
|
||||||
|
|
||||||
BTRioBankMetrics metrics;
|
BTRioBankMetrics metrics;
|
||||||
BTRioBankMetricsFor(RadarSurfW, RadarSurfH, &metrics);
|
BTRioBankMetricsFor(surfW, surfH, &metrics);
|
||||||
|
|
||||||
BTRioBank bank;
|
BTRioBank bank;
|
||||||
BTRioBankLayout(BTRioBankRadar, 0, 0, RadarSurfW, RadarSurfH,
|
BTRioBankLayout(BTRioBankRadar, 0, 0, surfW, surfH,
|
||||||
0x10, 0x18, &metrics, bottomAddrs, &bank);
|
0x10, 0x18, &metrics, bottomAddrs, &bank);
|
||||||
|
|
||||||
BTRioBankDump("Secondary / Radar", &bank);
|
BTRioBankDump("Secondary / Radar", &bank);
|
||||||
int dx = -bank.boundsX, dy = -bank.boundsY;
|
int dx = -bank.boundsX, dy = -bank.boundsY;
|
||||||
AdoptBank(w, bank, ClrYellow, dx, dy);
|
AdoptBank(w, bank, ClrYellow, dx, dy);
|
||||||
SetRect(&w.surfaceRect, dx, dy, dx + RadarSurfW, dy + RadarSurfH);
|
SetRect(&w.surfaceRect, dx, dy, dx + surfW, dy + surfH);
|
||||||
w.clientW = bank.boundsW;
|
w.clientW = bank.boundsW;
|
||||||
w.clientH = bank.boundsH;
|
w.clientH = bank.boundsH;
|
||||||
}
|
}
|
||||||
@@ -407,6 +429,179 @@ enum { LayoutOff = 0, LayoutLoad = 1, LayoutSave = 2 };
|
|||||||
|
|
||||||
static const char *layoutFileName = "glass_layout.cfg";
|
static const char *layoutFileName = "glass_layout.cfg";
|
||||||
|
|
||||||
|
// POD SURFACE MODE (BT_POD_SURFACES=1, 2026-08-06) -- the REAL pod bring-up
|
||||||
|
// shape. On the actual cab each MFD is its own 640x480 panel with PHYSICAL
|
||||||
|
// buttons and lamps around it, so the desktop chrome the glass windows draw
|
||||||
|
// (the RIO button bank + the frame) is exactly wrong there: it would paint
|
||||||
|
// fake buttons onto a panel that has real ones behind glass. In pod mode a
|
||||||
|
// window is cropped to its SURFACE, frameless, and the bank is dropped:
|
||||||
|
// one clean picture per physical panel, positioned by glass_layout.cfg.
|
||||||
|
// The Flight Controls window (no surface at all) is not created.
|
||||||
|
// BT_POD_SURFACES=1 -> every display window goes bare
|
||||||
|
// glass_layout.cfg "<title>=x,y,bare" -> just that one (mixed rigs)
|
||||||
|
// BT_POD_CHANMAP -- per-cart channel override, e.g.
|
||||||
|
// BT_POD_CHANMAP=Comm=blue,Heat=red
|
||||||
|
// The 1995 config authors which colour line each surface rides (L4GAUGE.CFG),
|
||||||
|
// but a rebuilt cab can have its RGB splitter wired to different panels than
|
||||||
|
// the original -- on Nick's cart the coolant (Heat) and hot-box (Comm) screens
|
||||||
|
// came out swapped, which is a WIRING difference, not a software bug. Rather
|
||||||
|
// than edit authentic config, override the channel here, per rig.
|
||||||
|
// Returns 0/1/2 (red/green/blue) for a named port, or -1 if unmapped.
|
||||||
|
static int
|
||||||
|
PodChannelOverride(const char *portName)
|
||||||
|
{
|
||||||
|
static int loaded = 0;
|
||||||
|
static char names[8][20];
|
||||||
|
static int chans[8];
|
||||||
|
static int count = 0;
|
||||||
|
if (!loaded)
|
||||||
|
{
|
||||||
|
loaded = 1;
|
||||||
|
const char *e = getenv("BT_POD_CHANMAP");
|
||||||
|
while (e != NULL && *e && count < 8)
|
||||||
|
{
|
||||||
|
while (*e == ' ' || *e == ',') ++e;
|
||||||
|
const char *eq = strchr(e, '=');
|
||||||
|
if (eq == NULL) break;
|
||||||
|
int n = 0;
|
||||||
|
for (const char *p = e; p < eq && n < 19; ++p) names[count][n++] = *p;
|
||||||
|
names[count][n] = 0;
|
||||||
|
const char *v = eq + 1;
|
||||||
|
int ch = -1;
|
||||||
|
if (_strnicmp(v, "red", 3) == 0) ch = L4GraphicsPort::RedChannel;
|
||||||
|
else if (_strnicmp(v, "green", 5) == 0) ch = L4GraphicsPort::GreenChannel;
|
||||||
|
else if (_strnicmp(v, "blue", 4) == 0) ch = L4GraphicsPort::BlueChannel;
|
||||||
|
if (ch >= 0)
|
||||||
|
{
|
||||||
|
chans[count] = ch;
|
||||||
|
DEBUG_STREAM << "[glasswin] channel override: " << names[count]
|
||||||
|
<< " -> " << (ch == L4GraphicsPort::RedChannel ? "red"
|
||||||
|
: ch == L4GraphicsPort::GreenChannel ? "green" : "blue")
|
||||||
|
<< std::endl << std::flush;
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
const char *nx = strchr(v, ',');
|
||||||
|
if (nx == NULL) break;
|
||||||
|
e = nx + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int i = 0; i < count; ++i)
|
||||||
|
if (portName != NULL && _stricmp(names[i], portName) == 0)
|
||||||
|
return chans[i];
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The channel a surface actually rides on THIS rig: the override if given,
|
||||||
|
// else what the pod config authored.
|
||||||
|
static int
|
||||||
|
ResolvedChannelOf(const char *portName, L4GraphicsPort *port)
|
||||||
|
{
|
||||||
|
int ov = PodChannelOverride(portName);
|
||||||
|
if (ov >= 0)
|
||||||
|
return ov;
|
||||||
|
return (port != NULL) ? (port->GetEnableID() & 0x7F) : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// BT_POD_IDENT=1 -- draw each surface's identity ON the glass (title, monitor,
|
||||||
|
// and in RGB mode the channel->surface map). The only reliable way to learn
|
||||||
|
// which physical cab panel a Windows display drives.
|
||||||
|
static int
|
||||||
|
PodIdentMode()
|
||||||
|
{
|
||||||
|
static int m = -1;
|
||||||
|
if (m < 0)
|
||||||
|
{
|
||||||
|
const char *e = getenv("BT_POD_IDENT");
|
||||||
|
m = (e != NULL && e[0] != 0 && e[0] != '0') ? 1 : 0;
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
// BT_POD_CHANTEST=<seconds> -- CHANNEL WALK, the instrument for mapping
|
||||||
|
// splitter-wired glass. On a cab, three mono CRTs share one VGA port through
|
||||||
|
// its R/G/B lines, so "which panel is which" cannot be read off Windows: the
|
||||||
|
// desktop sees ONE display for three monitors. This drives ONE channel at a
|
||||||
|
// time -- all-red, then all-green, then all-blue, N seconds each, with the
|
||||||
|
// channel name drawn huge -- so the answer is simply: watch which panel lights
|
||||||
|
// up, and when. Readable even over a rough livestream.
|
||||||
|
// Returns the dwell in seconds (0 = off).
|
||||||
|
static int
|
||||||
|
PodChanTestSeconds()
|
||||||
|
{
|
||||||
|
static int s = -1;
|
||||||
|
if (s < 0)
|
||||||
|
{
|
||||||
|
const char *e = getenv("BT_POD_CHANTEST");
|
||||||
|
s = (e != NULL && e[0] != 0) ? atoi(e) : 0;
|
||||||
|
if (s < 0) s = 0;
|
||||||
|
if (e != NULL && e[0] != 0 && s == 0) s = 3; // "1"/"on" -> 3s default
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Which channel is lit right now (0=red 1=green 2=blue), from the wall clock.
|
||||||
|
static int
|
||||||
|
PodChanTestPhase()
|
||||||
|
{
|
||||||
|
int dwell = PodChanTestSeconds();
|
||||||
|
if (dwell <= 0)
|
||||||
|
return -1;
|
||||||
|
return (int)((GetTickCount() / (DWORD)(dwell * 1000)) % 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// BT_POD_RGB=1 -- the pod's REAL video wiring (see pod-hardware.md §THE RGB
|
||||||
|
// SPLIT): one VGA port per window, its R/G/B analog lines split to three
|
||||||
|
// monochrome MFD monitors. Implies pod surface mode.
|
||||||
|
static int
|
||||||
|
PodRgbSplitMode()
|
||||||
|
{
|
||||||
|
static int m = -1;
|
||||||
|
if (m < 0)
|
||||||
|
{
|
||||||
|
const char *e = getenv("BT_POD_RGB");
|
||||||
|
m = (e != NULL && e[0] != 0 && e[0] != '0') ? 1 : 0;
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
PodSurfaceMode()
|
||||||
|
{
|
||||||
|
static int m = -1;
|
||||||
|
if (m < 0)
|
||||||
|
{
|
||||||
|
const char *e = getenv("BT_POD_SURFACES");
|
||||||
|
m = (e != NULL && *e != '\0' && *e != '0') ? 1 : 0;
|
||||||
|
if (!m && PodRgbSplitMode())
|
||||||
|
m = 1; // the RGB split is pod glass -- always bare
|
||||||
|
if (m)
|
||||||
|
DEBUG_STREAM << "[glasswin] POD SURFACE MODE: bare surfaces, no "
|
||||||
|
"button banks (physical pod buttons assumed)\n" << std::flush;
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crop a built window to its surface: drop the bank, move the picture to the
|
||||||
|
// client origin, go frameless. Called at the end of each Build* when pod mode
|
||||||
|
// is on (and from LoadLayout for a per-window ",bare").
|
||||||
|
static void
|
||||||
|
MakeBareSurface(GWin &w)
|
||||||
|
{
|
||||||
|
if (w.portPrimary == NULL) // Flight Controls -- nothing to show
|
||||||
|
{
|
||||||
|
w.clientW = w.clientH = 0;
|
||||||
|
w.buttonCount = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const int sw = w.surfaceRect.right - w.surfaceRect.left;
|
||||||
|
const int sh = w.surfaceRect.bottom - w.surfaceRect.top;
|
||||||
|
w.buttonCount = 0; // the pod's buttons are real
|
||||||
|
SetRect(&w.surfaceRect, 0, 0, sw, sh);
|
||||||
|
w.clientW = sw;
|
||||||
|
w.clientH = sh;
|
||||||
|
w.noFrame = 1;
|
||||||
|
}
|
||||||
|
|
||||||
// A framed window is a normal tool window; ",noframe" makes it a bare
|
// A framed window is a normal tool window; ",noframe" makes it a bare
|
||||||
// WS_POPUP -- no caption, no border, nothing but the display and its buttons.
|
// WS_POPUP -- no caption, no border, nothing but the display and its buttons.
|
||||||
static DWORD
|
static DWORD
|
||||||
@@ -537,6 +732,73 @@ void
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Restore saved positions over the just-computed pod-faithful defaults. Called
|
// Restore saved positions over the just-computed pod-faithful defaults. Called
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
// MONITOR BINDING (2026-08-06) -- "monitor:<name-or-index>" instead of x,y.
|
||||||
|
//
|
||||||
|
// Absolute desktop coordinates do not survive a reboot on a pod: Windows
|
||||||
|
// renumbers the virtual desktop whenever adapters enumerate differently (a USB
|
||||||
|
// display adapter is especially prone), so a layout pinned to 800,122 can land
|
||||||
|
// on the wrong glass -- or off-screen -- after a power cycle. Binding to the
|
||||||
|
// MONITOR instead and resolving its rect at startup is stable across boots.
|
||||||
|
// Heat MFD=monitor:\.\DISPLAY4,bare (device name -- what the receipts print)
|
||||||
|
// Heat MFD=monitor:2,bare (index into the enumeration order)
|
||||||
|
// The surface is centred on that monitor; an exact-size panel fills it.
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
struct MonScan { int index; const char *want; int wantIndex; RECT rect; int found; };
|
||||||
|
|
||||||
|
static BOOL CALLBACK
|
||||||
|
MonScanProc(HMONITOR mon, HDC, LPRECT, LPARAM param)
|
||||||
|
{
|
||||||
|
MonScan *sc = (MonScan *)param;
|
||||||
|
MONITORINFOEXA mi;
|
||||||
|
memset(&mi, 0, sizeof(mi));
|
||||||
|
mi.cbSize = sizeof(mi);
|
||||||
|
if (GetMonitorInfoA(mon, (MONITORINFO *)&mi))
|
||||||
|
{
|
||||||
|
int hit = 0;
|
||||||
|
if (sc->want != NULL)
|
||||||
|
{
|
||||||
|
// Match the full device name OR just its tail, so a hand-written
|
||||||
|
// cfg can say `monitor:DISPLAY4` and skip the \\.\ prefix entirely
|
||||||
|
// (backslashes in a config file are a foot-gun -- every shell,
|
||||||
|
// editor and copy-paste path mangles them differently).
|
||||||
|
size_t dl = strlen(mi.szDevice), wl = strlen(sc->want);
|
||||||
|
if (_stricmp(mi.szDevice, sc->want) == 0)
|
||||||
|
hit = 1;
|
||||||
|
else if (wl > 0 && wl <= dl
|
||||||
|
&& _stricmp(mi.szDevice + (dl - wl), sc->want) == 0)
|
||||||
|
hit = 1;
|
||||||
|
}
|
||||||
|
if (!hit && sc->wantIndex >= 0 && sc->index == sc->wantIndex)
|
||||||
|
hit = 1;
|
||||||
|
if (hit && !sc->found)
|
||||||
|
{
|
||||||
|
sc->rect = mi.rcMonitor;
|
||||||
|
sc->found = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
++sc->index;
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve "monitor:<spec>" to a rect. Returns 1 on success.
|
||||||
|
static int
|
||||||
|
ResolveMonitorSpec(const char *spec, RECT *out)
|
||||||
|
{
|
||||||
|
while (*spec == ' ' || *spec == ' ') ++spec;
|
||||||
|
MonScan sc;
|
||||||
|
memset(&sc, 0, sizeof(sc));
|
||||||
|
sc.wantIndex = -1;
|
||||||
|
if (spec[0] >= '0' && spec[0] <= '9')
|
||||||
|
sc.wantIndex = atoi(spec);
|
||||||
|
else
|
||||||
|
sc.want = spec;
|
||||||
|
EnumDisplayMonitors(NULL, NULL, MonScanProc, (LPARAM)&sc);
|
||||||
|
if (sc.found)
|
||||||
|
*out = sc.rect;
|
||||||
|
return sc.found;
|
||||||
|
}
|
||||||
|
|
||||||
// from Create AFTER ComputeLayout, so any window not named in the file keeps its
|
// from Create AFTER ComputeLayout, so any window not named in the file keeps its
|
||||||
// computed spot. Restored windows are flagged so the WM_TIMER re-snap (which
|
// computed spot. Restored windows are flagged so the WM_TIMER re-snap (which
|
||||||
// re-runs ComputeLayout once the main window appears) leaves them alone.
|
// re-runs ComputeLayout once the main window appears) leaves them alone.
|
||||||
@@ -574,30 +836,77 @@ static void
|
|||||||
*end = '\0';
|
*end = '\0';
|
||||||
|
|
||||||
int x = 0, y = 0, w = 0, h = 0;
|
int x = 0, y = 0, w = 0, h = 0;
|
||||||
if (sscanf(eq + 1, "%d,%d,%d,%d", &x, &y, &w, &h) < 2)
|
int haveXY = (sscanf(eq + 1, "%d,%d,%d,%d", &x, &y, &w, &h) >= 2);
|
||||||
|
int viaMonitor = 0;
|
||||||
|
if (!haveXY)
|
||||||
|
{
|
||||||
|
// "monitor:<name|index>" -- boot-stable binding (see the resolver).
|
||||||
|
const char *v = eq + 1;
|
||||||
|
while (*v == ' ' || *v == '\t') ++v;
|
||||||
|
if (_strnicmp(v, "monitor:", 8) == 0 || _strnicmp(v, "mon:", 4) == 0)
|
||||||
|
{
|
||||||
|
char spec[64];
|
||||||
|
const char *p2 = strchr(v, ':') + 1;
|
||||||
|
int n2 = 0;
|
||||||
|
while (*p2 && *p2 != ',' && n2 < (int)sizeof(spec) - 1) spec[n2++] = *p2++;
|
||||||
|
spec[n2] = 0;
|
||||||
|
RECT mr;
|
||||||
|
if (ResolveMonitorSpec(spec, &mr))
|
||||||
|
{
|
||||||
|
x = mr.left; y = mr.top;
|
||||||
|
w = mr.right - mr.left; h = mr.bottom - mr.top;
|
||||||
|
haveXY = 1;
|
||||||
|
viaMonitor = 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
DEBUG_STREAM << "[glasswin] monitor '" << spec
|
||||||
|
<< "' not found -- leaving computed placement\n" << std::flush;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!haveXY)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// Trailing options after the numbers, comma-separated. Unknown ones
|
// Trailing options after the numbers, comma-separated. Unknown ones
|
||||||
// are ignored (so an older build reading a newer file just loses the
|
// are ignored (so an older build reading a newer file just loses the
|
||||||
// option, never the line) -- the bindings.txt grammar rule.
|
// option, never the line) -- the bindings.txt grammar rule.
|
||||||
int noframe = 0;
|
int noframe = 0, bare = 0;
|
||||||
for (const char *opt = strchr(eq + 1, ','); opt != NULL; opt = strchr(opt + 1, ','))
|
for (const char *opt = strchr(eq + 1, ','); opt != NULL; opt = strchr(opt + 1, ','))
|
||||||
{
|
{
|
||||||
const char *t = opt + 1;
|
const char *t = opt + 1;
|
||||||
while (*t == ' ' || *t == '\t') ++t;
|
while (*t == ' ' || *t == '\t') ++t;
|
||||||
if (_strnicmp(t, "noframe", 7) == 0)
|
if (_strnicmp(t, "noframe", 7) == 0)
|
||||||
{
|
|
||||||
noframe = 1;
|
noframe = 1;
|
||||||
break;
|
else if (_strnicmp(t, "bare", 4) == 0)
|
||||||
}
|
bare = 1; // pod panel: surface only (see PodSurfaceMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
GWin *gw = FindGWinByTitle(s);
|
GWin *gw = FindGWinByTitle(s);
|
||||||
if (gw == NULL)
|
if (gw == NULL)
|
||||||
continue;
|
continue;
|
||||||
gw->wantX = x;
|
// "bare" is a pod panel: it always implies frameless. (Without this a
|
||||||
gw->wantY = y;
|
// cfg line saying only ",bare" silently re-framed a window that pod
|
||||||
gw->noFrame = noframe;
|
// surface mode had already cropped -- caught on the bench 2026-08-06.)
|
||||||
|
gw->noFrame = (noframe || bare) ? 1 : 0;
|
||||||
|
if (bare && gw->buttonCount > 0)
|
||||||
|
MakeBareSurface(*gw); // per-window pod panel (mixed rigs)
|
||||||
|
if (viaMonitor)
|
||||||
|
{
|
||||||
|
// x,y,w,h is the MONITOR rect -- centre this surface on it (an
|
||||||
|
// exact-size pod panel fills it edge to edge).
|
||||||
|
int sw = gw->clientW > 0 ? gw->clientW : (gw->surfaceRect.right - gw->surfaceRect.left);
|
||||||
|
int sh = gw->clientH > 0 ? gw->clientH : (gw->surfaceRect.bottom - gw->surfaceRect.top);
|
||||||
|
gw->wantX = x + ((w - sw) / 2);
|
||||||
|
gw->wantY = y + ((h - sh) / 2);
|
||||||
|
DEBUG_STREAM << "[glasswin] '" << gw->title
|
||||||
|
<< "' bound to monitor " << x << "," << y << " " << w << "x" << h
|
||||||
|
<< " -> window at " << gw->wantX << "," << gw->wantY
|
||||||
|
<< std::endl << std::flush;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
gw->wantX = x;
|
||||||
|
gw->wantY = y;
|
||||||
|
}
|
||||||
gw->restored = 1;
|
gw->restored = 1;
|
||||||
++restored;
|
++restored;
|
||||||
}
|
}
|
||||||
@@ -768,8 +1077,86 @@ static void
|
|||||||
mask = 0xFF; // index the low (palette) byte
|
mask = 0xFF; // index the low (palette) byte
|
||||||
palId = SVGA16::SecondaryPalette;
|
palId = SVGA16::SecondaryPalette;
|
||||||
}
|
}
|
||||||
|
// RGB mode: the base surface rides a CHANNEL, so its tint is that pure
|
||||||
|
// primary -- resolved live (override first), never the build-time guess.
|
||||||
|
if (w->groupCount > 0 && tint >= 0)
|
||||||
|
{
|
||||||
|
switch (ResolvedChannelOf(w->portPrimary, port))
|
||||||
|
{
|
||||||
|
case L4GraphicsPort::RedChannel: tint = (int)0x00FF0000; break;
|
||||||
|
case L4GraphicsPort::GreenChannel: tint = (int)0x0000FF00; break;
|
||||||
|
case L4GraphicsPort::BlueChannel: tint = (int)0x000000FF; break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int ow = 0, oh = 0;
|
int ow = 0, oh = 0;
|
||||||
svga->ExpandPlaneToBGRA(mask, palId, tint, w->rotate, gStage, &ow, &oh);
|
svga->ExpandPlaneToBGRA(mask, palId, tint, w->rotate, gStage, &ow, &oh);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// RGB CHANNEL COMPOSITE (BT_POD_RGB, 2026-08-06) -- the AUTHENTIC pod
|
||||||
|
// output. The cab does not give each mono MFD its own video port: one
|
||||||
|
// VGA port's R/G/B analog lines are SPLIT to three monochrome monitors,
|
||||||
|
// and the game puts a different surface in each colour channel of one
|
||||||
|
// palettized framebuffer (L4GAUGE.CFG: Comm=red, Mfd2=green, Heat=blue
|
||||||
|
// on clut2; Mfd1=red, Mfd3=green on clut1 -- and
|
||||||
|
// L4GraphicsPort::BuildSecondaryColor writes exactly ONE component per
|
||||||
|
// port, which IS the split). So on splitter-wired glass this window is
|
||||||
|
// not one MFD: it is one VGA PORT, carrying up to three. Composite the
|
||||||
|
// group's planes into this frame, each in its own channel, and let the
|
||||||
|
// hardware separate them again.
|
||||||
|
//
|
||||||
|
// Channel comes from the port itself (GetEnableID), so the authored
|
||||||
|
// config -- including a page swap that hands the channel to an Eng
|
||||||
|
// plane -- decides, never a hardcoded table here.
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
if (w->groupCount > 0 && ow > 0 && oh > 0)
|
||||||
|
{
|
||||||
|
const int n = ow * oh;
|
||||||
|
for (int gi = 0; gi < w->groupCount; ++gi)
|
||||||
|
{
|
||||||
|
L4GraphicsPort *gp =
|
||||||
|
static_cast<L4GraphicsPort*>(gr->GetGraphicsPort(w->groupPort[gi]));
|
||||||
|
if (gp == NULL)
|
||||||
|
continue;
|
||||||
|
// honour the live Mfd<n>/Eng<n> page swap: take whichever twin
|
||||||
|
// currently owns a channel
|
||||||
|
if (gp->GetEnableID() == L4GraphicsPort::BlankColor
|
||||||
|
&& w->groupAlt[gi] != NULL)
|
||||||
|
{
|
||||||
|
L4GraphicsPort *ga =
|
||||||
|
static_cast<L4GraphicsPort*>(gr->GetGraphicsPort(w->groupAlt[gi]));
|
||||||
|
if (ga != NULL && ga->GetEnableID() != L4GraphicsPort::BlankColor)
|
||||||
|
gp = ga;
|
||||||
|
}
|
||||||
|
const int ch = ResolvedChannelOf(w->groupPort[gi], gp);
|
||||||
|
unsigned long chMask;
|
||||||
|
switch (ch)
|
||||||
|
{
|
||||||
|
case L4GraphicsPort::RedChannel: chMask = 0x00FF0000ul; break;
|
||||||
|
case L4GraphicsPort::GreenChannel: chMask = 0x0000FF00ul; break;
|
||||||
|
case L4GraphicsPort::BlueChannel: chMask = 0x000000FFul; break;
|
||||||
|
default: continue; // blank / direct-colour -> contributes nothing
|
||||||
|
}
|
||||||
|
int gw = 0, gh = 0;
|
||||||
|
// -1 tint would palette-expand; pass white so the plane comes back
|
||||||
|
// as full-intensity mono, then keep only this port's channel.
|
||||||
|
svga->ExpandPlaneToBGRA(gp->GetBitMask(), gp->paletteID,
|
||||||
|
(int)0x00FFFFFF, w->rotate, gStage2, &gw, &gh);
|
||||||
|
if (gw != ow || gh != oh)
|
||||||
|
continue;
|
||||||
|
for (int i = 0; i < n; ++i)
|
||||||
|
gStage[i] |= (gStage2[i] & chMask);
|
||||||
|
}
|
||||||
|
if (log)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "[glass] '" << w->title << "' RGB COMPOSITE of "
|
||||||
|
<< w->groupCount << " plane(s):";
|
||||||
|
for (int gi = 0; gi < w->groupCount; ++gi)
|
||||||
|
DEBUG_STREAM << " " << w->groupPort[gi];
|
||||||
|
DEBUG_STREAM << "\n" << std::flush;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (log)
|
if (log)
|
||||||
{
|
{
|
||||||
int nz = 0, n = ow * oh;
|
int nz = 0, n = ow * oh;
|
||||||
@@ -927,6 +1314,117 @@ static void
|
|||||||
SelectObject(dc, oldBrush);
|
SelectObject(dc, oldBrush);
|
||||||
SelectObject(dc, oldPen);
|
SelectObject(dc, oldPen);
|
||||||
DeleteObject(pen);
|
DeleteObject(pen);
|
||||||
|
|
||||||
|
// (NO full-face flash overlay here either -- same wrongness as the
|
||||||
|
// L4VB16 one, removed 2026-08-03: the buttons are big rects tucked
|
||||||
|
// under the surface by design, and the protruding edge is the lamp.)
|
||||||
|
|
||||||
|
// ---- CHANNEL WALK (BT_POD_CHANTEST) ---------------------------------
|
||||||
|
// Overwrite the whole surface with ONE channel at full intensity, so
|
||||||
|
// exactly one monitor of a splitter-fed trio lights. Cycles R->G->B.
|
||||||
|
{
|
||||||
|
const int phase = PodChanTestPhase();
|
||||||
|
if (phase >= 0)
|
||||||
|
{
|
||||||
|
static const COLORREF chColor[3] =
|
||||||
|
{ RGB(255, 0, 0), RGB(0, 255, 0), RGB(0, 0, 255) };
|
||||||
|
static const char *chName[3] = { "RED", "GREEN", "BLUE" };
|
||||||
|
HBRUSH fill = CreateSolidBrush(chColor[phase]);
|
||||||
|
FillRect(dc, &w->surfaceRect, fill);
|
||||||
|
DeleteObject(fill);
|
||||||
|
|
||||||
|
// Name the channel AND what the pod authors onto it here, so a
|
||||||
|
// lit panel identifies itself completely: "GREEN = Mfd2".
|
||||||
|
const char *who = "-";
|
||||||
|
GaugeRenderer *cgr = BTResolveGaugeRenderer();
|
||||||
|
if (cgr != NULL)
|
||||||
|
{
|
||||||
|
const char *cand[4]; int nc = 0;
|
||||||
|
cand[nc++] = w->portPrimary;
|
||||||
|
for (int gi = 0; gi < w->groupCount && nc < 4; ++gi)
|
||||||
|
cand[nc++] = w->groupPort[gi];
|
||||||
|
for (int i = 0; i < nc; ++i)
|
||||||
|
{
|
||||||
|
L4GraphicsPort *pp =
|
||||||
|
static_cast<L4GraphicsPort*>(cgr->GetGraphicsPort(cand[i]));
|
||||||
|
if (pp != NULL && (pp->GetEnableID() & 0x7F) == phase)
|
||||||
|
{ who = cand[i]; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
char big[96];
|
||||||
|
sprintf(big, "%s %s = %s", w->monitorName, chName[phase], who);
|
||||||
|
RECT br = w->surfaceRect;
|
||||||
|
br.top += (br.bottom - br.top) / 3;
|
||||||
|
SetBkMode(dc, TRANSPARENT);
|
||||||
|
DrawLabelText(dc, big, titleFont, &br, RGB(0, 0, 0),
|
||||||
|
DT_CENTER | DT_SINGLELINE | DT_VCENTER);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- PANEL IDENTIFY (BT_POD_IDENT=1) --------------------------------
|
||||||
|
// Pod bring-up: nobody can tell which PHYSICAL panel a Windows display
|
||||||
|
// is, and desktop coordinate order says nothing about where a panel
|
||||||
|
// sits in the cab -- guessing that mapping is exactly how it came out
|
||||||
|
// wrong. So label each surface ON ITS OWN GLASS with what it is, which
|
||||||
|
// monitor it came from, and (in RGB mode) the channel->surface map.
|
||||||
|
// Walk the pod, read the panels, write the layout from what you SAW.
|
||||||
|
if (PodIdentMode())
|
||||||
|
{
|
||||||
|
char line1[96], line2[160];
|
||||||
|
sprintf(line1, "%s", w->title);
|
||||||
|
if (w->groupCount > 0)
|
||||||
|
{
|
||||||
|
// name the channels so a splitter-fed trio is self-describing
|
||||||
|
char parts[128];
|
||||||
|
parts[0] = 0;
|
||||||
|
GaugeRenderer *igr = BTResolveGaugeRenderer();
|
||||||
|
const char *names[4]; int chans[4]; int nn = 0;
|
||||||
|
names[nn] = w->portPrimary; chans[nn] = -1; ++nn;
|
||||||
|
for (int gi = 0; gi < w->groupCount && nn < 4; ++gi)
|
||||||
|
{ names[nn] = w->groupPort[gi]; chans[nn] = -1; ++nn; }
|
||||||
|
for (int i = 0; i < nn; ++i)
|
||||||
|
{
|
||||||
|
const char *chName = "?";
|
||||||
|
if (igr != NULL)
|
||||||
|
{
|
||||||
|
L4GraphicsPort *pp =
|
||||||
|
static_cast<L4GraphicsPort*>(igr->GetGraphicsPort(names[i]));
|
||||||
|
if (pp != NULL)
|
||||||
|
switch (pp->GetEnableID() & 0x7F)
|
||||||
|
{
|
||||||
|
case L4GraphicsPort::RedChannel: chName = "RED"; break;
|
||||||
|
case L4GraphicsPort::GreenChannel: chName = "GREEN"; break;
|
||||||
|
case L4GraphicsPort::BlueChannel: chName = "BLUE"; break;
|
||||||
|
case L4GraphicsPort::AllChannels: chName = "RGB"; break;
|
||||||
|
default: chName = "blank"; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
char one[48];
|
||||||
|
sprintf(one, "%s%s=%s", (i ? " " : ""), chName, names[i]);
|
||||||
|
if (strlen(parts) + strlen(one) < sizeof(parts) - 1)
|
||||||
|
strcat(parts, one);
|
||||||
|
}
|
||||||
|
sprintf(line2, "%s %s", w->monitorName, parts);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sprintf(line2, "%s surface=%s", w->monitorName,
|
||||||
|
w->portPrimary ? w->portPrimary : "-");
|
||||||
|
}
|
||||||
|
|
||||||
|
RECT lr = w->surfaceRect;
|
||||||
|
lr.bottom = lr.top + 64;
|
||||||
|
HBRUSH back = CreateSolidBrush(RGB(0, 0, 0));
|
||||||
|
FillRect(dc, &lr, back);
|
||||||
|
DeleteObject(back);
|
||||||
|
SetBkMode(dc, TRANSPARENT);
|
||||||
|
RECT t1 = lr; t1.top += 4; t1.bottom = t1.top + 30;
|
||||||
|
DrawLabelText(dc, line1, titleFont, &t1, RGB(255, 255, 0),
|
||||||
|
DT_LEFT | DT_SINGLELINE | DT_VCENTER);
|
||||||
|
RECT t2 = lr; t2.top += 32; t2.bottom = t2.top + 28;
|
||||||
|
DrawLabelText(dc, line2, buttonFont, &t2, RGB(0, 255, 255),
|
||||||
|
DT_LEFT | DT_SINGLELINE | DT_VCENTER);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BitBlt(winDC, 0, 0, client.right - client.left, client.bottom - client.top,
|
BitBlt(winDC, 0, 0, client.right - client.left, client.bottom - client.top,
|
||||||
@@ -1063,11 +1561,23 @@ void
|
|||||||
if (gWinCount != 0)
|
if (gWinCount != 0)
|
||||||
return; // already up
|
return; // already up
|
||||||
|
|
||||||
// BT_GAUGE_SEC_ROT: the portrait-CRT unrotation direction (shared with the
|
// BT_GAUGE_SEC_ROT: how far to turn the secondary/radar surface.
|
||||||
// D3D dev path; default 3 = CW = user-verified upright).
|
// 0 = none, 1 = 90 CCW, 2 = 180, 3 = 90 CW (default -- the pod's
|
||||||
|
// portrait CRT, user-verified upright).
|
||||||
|
// 0 and 2 keep the picture LANDSCAPE, which a modern replacement panel
|
||||||
|
// needs. (Until 2026-08-06 this read only '1' and forced everything else
|
||||||
|
// to 3, so BT_GAUGE_SEC_ROT=2 was silently ignored -- caught on the cart
|
||||||
|
// when a 180 request kept coming back portrait.)
|
||||||
{
|
{
|
||||||
const char *rv = getenv("BT_GAUGE_SEC_ROT");
|
const char *rv = getenv("BT_GAUGE_SEC_ROT");
|
||||||
gRadarRot = (rv != NULL && rv[0] == '1') ? 1 : 3;
|
if (rv != NULL && rv[0] >= '0' && rv[0] <= '3')
|
||||||
|
gRadarRot = rv[0] - '0';
|
||||||
|
else
|
||||||
|
gRadarRot = 3;
|
||||||
|
DEBUG_STREAM << "[glasswin] radar rotation " << gRadarRot
|
||||||
|
<< (gRadarRot == 3 ? " (90 CW)" : gRadarRot == 1 ? " (90 CCW)"
|
||||||
|
: gRadarRot == 2 ? " (180)" : " (none)")
|
||||||
|
<< std::endl << std::flush;
|
||||||
}
|
}
|
||||||
|
|
||||||
memset(latched, 0, sizeof(latched));
|
memset(latched, 0, sizeof(latched));
|
||||||
@@ -1075,6 +1585,8 @@ void
|
|||||||
sRepositionedToMain = false;
|
sRepositionedToMain = false;
|
||||||
if (gStage == NULL)
|
if (gStage == NULL)
|
||||||
gStage = new unsigned long[640 * 480];
|
gStage = new unsigned long[640 * 480];
|
||||||
|
if (gStage2 == NULL)
|
||||||
|
gStage2 = new unsigned long[640 * 480];
|
||||||
|
|
||||||
// Build the seven windows. Order fixes the ComputeLayout roles (0..6).
|
// Build the seven windows. Order fixes the ComputeLayout roles (0..6).
|
||||||
BuildMfd (gWins[0], "Heat MFD", "Heat", NULL, 0x2F); // UL 0x28-0x2F
|
BuildMfd (gWins[0], "Heat MFD", "Heat", NULL, 0x2F); // UL 0x28-0x2F
|
||||||
@@ -1086,6 +1598,52 @@ void
|
|||||||
BuildFlight(gWins[6]); // 0x38-0x47
|
BuildFlight(gWins[6]); // 0x38-0x47
|
||||||
gWinCount = 7;
|
gWinCount = 7;
|
||||||
|
|
||||||
|
// POD SURFACE MODE: crop every display to its picture and drop the Flight
|
||||||
|
// Controls pad (it is pure desktop chrome -- the cab has a real stick).
|
||||||
|
if (PodSurfaceMode())
|
||||||
|
{
|
||||||
|
for (int i = 0; i < gWinCount; ++i)
|
||||||
|
MakeBareSurface(gWins[i]);
|
||||||
|
if (gWins[6].portPrimary == NULL)
|
||||||
|
gWinCount = 6; // drop Flight Controls
|
||||||
|
}
|
||||||
|
|
||||||
|
// RGB SPLIT MODE (BT_POD_RGB): the cab's real wiring -- ONE VGA port per
|
||||||
|
// window, its R/G/B lines split to three mono monitors. Collapse the five
|
||||||
|
// separate MFD windows into the TWO ports the pod actually drives
|
||||||
|
// (L4GAUGE.CFG clut2 = Comm/Mfd2/Heat, clut1 = Mfd1/Mfd3), each window
|
||||||
|
// compositing its group into the colour channels; the radar keeps its own
|
||||||
|
// full-colour port. Channels are read live from each port, so a page swap
|
||||||
|
// to an Eng plane follows automatically.
|
||||||
|
if (PodRgbSplitMode())
|
||||||
|
{
|
||||||
|
GWin port2 = gWins[0]; // reuse the Heat window's shape
|
||||||
|
port2.title = "VGA Port A"; // Comm=red, Mfd2=green, Heat=blue
|
||||||
|
port2.portPrimary = "Heat"; // base plane (blue); group adds the rest
|
||||||
|
port2.portAlt = NULL;
|
||||||
|
port2.groupCount = 2;
|
||||||
|
port2.groupPort[0] = "Comm"; port2.groupAlt[0] = NULL;
|
||||||
|
port2.groupPort[1] = "Mfd2"; port2.groupAlt[1] = "Eng2";
|
||||||
|
port2.monoTint = (int)0x000000FF; // Heat rides BLUE
|
||||||
|
|
||||||
|
GWin port1 = gWins[3]; // the Mfd1 window's shape
|
||||||
|
port1.title = "VGA Port B"; // Mfd1=red, Mfd3=green (blue spare)
|
||||||
|
port1.portPrimary = "Mfd1";
|
||||||
|
port1.portAlt = "Eng1";
|
||||||
|
port1.groupCount = 1;
|
||||||
|
port1.groupPort[0] = "Mfd3"; port1.groupAlt[0] = "Eng3";
|
||||||
|
port1.monoTint = (int)0x00FF0000; // Mfd1 rides RED
|
||||||
|
|
||||||
|
GWin radar = gWins[5]; // unchanged: its own colour port
|
||||||
|
|
||||||
|
gWins[0] = port2;
|
||||||
|
gWins[1] = port1;
|
||||||
|
gWins[2] = radar;
|
||||||
|
gWinCount = 3;
|
||||||
|
DEBUG_STREAM << "[glasswin] RGB SPLIT MODE: 2 VGA ports + radar "
|
||||||
|
"(each port's R/G/B feeds three mono panels)\n" << std::flush;
|
||||||
|
}
|
||||||
|
|
||||||
titleFont = CreateFontW(-11, 0, 0, 0, FW_BOLD, 0, 0, 0,
|
titleFont = CreateFontW(-11, 0, 0, 0, FW_BOLD, 0, 0, 0,
|
||||||
DEFAULT_CHARSET, 0, 0, CLEARTYPE_QUALITY, 0, L"Segoe UI");
|
DEFAULT_CHARSET, 0, 0, CLEARTYPE_QUALITY, 0, L"Segoe UI");
|
||||||
buttonFont = CreateFontW(-10, 0, 0, 0, FW_NORMAL, 0, 0, 0,
|
buttonFont = CreateFontW(-10, 0, 0, 0, FW_NORMAL, 0, 0, 0,
|
||||||
@@ -1141,6 +1699,39 @@ void
|
|||||||
SetLayeredWindowAttributes(w.hwnd, 0, 255, LWA_ALPHA);
|
SetLayeredWindowAttributes(w.hwnd, 0, 255, LWA_ALPHA);
|
||||||
SetTimer(w.hwnd, RepaintTimerId, RepaintMilliseconds, NULL);
|
SetTimer(w.hwnd, RepaintTimerId, RepaintMilliseconds, NULL);
|
||||||
ShowWindow(w.hwnd, SW_SHOWNOACTIVATE);
|
ShowWindow(w.hwnd, SW_SHOWNOACTIVATE);
|
||||||
|
|
||||||
|
// PLACEMENT RECEIPT (pod bring-up, 2026-08-06): which PHYSICAL monitor
|
||||||
|
// each display actually landed on. On a real cab -- or any remote
|
||||||
|
// session -- nobody can see all seven surfaces at once, so the log is
|
||||||
|
// the only way to confirm the map without walking around the pod.
|
||||||
|
{
|
||||||
|
RECT wr = { 0, 0, 0, 0 };
|
||||||
|
GetWindowRect(w.hwnd, &wr);
|
||||||
|
HMONITOR mon = MonitorFromWindow(w.hwnd, MONITOR_DEFAULTTONEAREST);
|
||||||
|
MONITORINFOEXA mi;
|
||||||
|
memset(&mi, 0, sizeof(mi));
|
||||||
|
mi.cbSize = sizeof(mi);
|
||||||
|
const char *devName = "?";
|
||||||
|
int mx = 0, my = 0, mw = 0, mh = 0, primary = 0;
|
||||||
|
if (mon != NULL && GetMonitorInfoA(mon, (MONITORINFO *)&mi))
|
||||||
|
{
|
||||||
|
devName = mi.szDevice;
|
||||||
|
strncpy(w.monitorName, mi.szDevice, sizeof(w.monitorName) - 1);
|
||||||
|
w.monitorName[sizeof(w.monitorName) - 1] = 0;
|
||||||
|
mx = mi.rcMonitor.left; my = mi.rcMonitor.top;
|
||||||
|
mw = mi.rcMonitor.right - mi.rcMonitor.left;
|
||||||
|
mh = mi.rcMonitor.bottom - mi.rcMonitor.top;
|
||||||
|
primary = (mi.dwFlags & MONITORINFOF_PRIMARY) ? 1 : 0;
|
||||||
|
}
|
||||||
|
DEBUG_STREAM << "[glasswin] '" << w.title << "' surface="
|
||||||
|
<< (w.portPrimary ? w.portPrimary : "-")
|
||||||
|
<< " at " << wr.left << "," << wr.top
|
||||||
|
<< " " << (wr.right - wr.left) << "x" << (wr.bottom - wr.top)
|
||||||
|
<< (w.noFrame ? " bare" : " framed")
|
||||||
|
<< " -> monitor " << devName
|
||||||
|
<< " (" << mx << "," << my << " " << mw << "x" << mh
|
||||||
|
<< (primary ? ", PRIMARY)" : ")") << "\n" << std::flush;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DEBUG_STREAM << "[glasswin] per-display cockpit up (" << gWinCount
|
DEBUG_STREAM << "[glasswin] per-display cockpit up (" << gWinCount
|
||||||
|
|||||||
@@ -90,6 +90,23 @@ L4GaugeRenderer::L4GaugeRenderer(bool windowed, int *secondaryIndex, int *aux1In
|
|||||||
char
|
char
|
||||||
*plasma_string = getenv("L4PLASMA");
|
*plasma_string = getenv("L4PLASMA");
|
||||||
|
|
||||||
|
//
|
||||||
|
// L4PLASMA=NONE / OFF / 0 -- no plasma marquee at all (2026-08-06).
|
||||||
|
// A real cab drives the 128x32 marquee over serial; a rebuilt cart may
|
||||||
|
// not have one wired, and the GLASS profile force-defaults L4PLASMA to
|
||||||
|
// SCREEN, which puts a desktop plasma window on the pod's glass where
|
||||||
|
// nothing should be. Leaving the variable UNSET is not an option
|
||||||
|
// there (the profile fills it in), so give "off" an explicit spelling.
|
||||||
|
//
|
||||||
|
if (plasma_string != NULL
|
||||||
|
&& (_stricmp(plasma_string, "NONE") == 0
|
||||||
|
|| _stricmp(plasma_string, "OFF") == 0
|
||||||
|
|| strcmp(plasma_string, "0") == 0))
|
||||||
|
{
|
||||||
|
Tell("Plasma display disabled (L4PLASMA=none)\n");
|
||||||
|
plasma_string = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
if (plasma_string != NULL)
|
if (plasma_string != NULL)
|
||||||
{
|
{
|
||||||
//Win32 Serial support: ADB 06/30/07
|
//Win32 Serial support: ADB 06/30/07
|
||||||
|
|||||||
@@ -466,6 +466,21 @@ void
|
|||||||
<< (pressed ? "HELD" : "released") << "\n" << std::flush;
|
<< (pressed ? "HELD" : "released") << "\n" << std::flush;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PANIC (0x3D) -> pilot-keypad key. The binary's ONLY pilot-eject input
|
||||||
|
// is keyboardGroup[KeyboardPilot].Add(mode 0x200000, mech, msg 0x19)
|
||||||
|
// (FUN_004d266c; exhaustive scan: no other 0x200000 consumer). The
|
||||||
|
// guarded PANIC key has no button-space consumer anywhere in the image,
|
||||||
|
// so on the pod it reported through the pilot keypad matrix -- this is
|
||||||
|
// that wire.
|
||||||
|
if (address == 0x3D && pressed)
|
||||||
|
EmitKeypad(LBE4ControlsManager::KeyboardPilot, 0);
|
||||||
|
|
||||||
|
// (The ENG-PAGE EJECT KEY intent-completion lives in the game-thread RIO
|
||||||
|
// drain -- LBE4ControlsManager::ProcessRIOEvent, ButtonPressedEvent --
|
||||||
|
// where mode_mask is safely in hand. A first cut queried the mode
|
||||||
|
// manager HERE, on the window-click thread, and crashed on the unbound
|
||||||
|
// TU global (field-caught 2026-08-03).)
|
||||||
|
|
||||||
RIOEvent event;
|
RIOEvent event;
|
||||||
event.Type = pressed ? ButtonPressedEvent : ButtonReleasedEvent;
|
event.Type = pressed ? ButtonPressedEvent : ButtonReleasedEvent;
|
||||||
event.Data.Unit = address;
|
event.Data.Unit = address;
|
||||||
@@ -479,6 +494,9 @@ void
|
|||||||
event.Type = KeyEvent;
|
event.Type = KeyEvent;
|
||||||
event.Data.Keyboard.Unit = unit;
|
event.Data.Keyboard.Unit = unit;
|
||||||
event.Data.Keyboard.Key = key;
|
event.Data.Keyboard.Key = key;
|
||||||
|
if (getenv("BT_PAD_LOG"))
|
||||||
|
DEBUG_STREAM << "[padkey] unit " << unit << " key " << key
|
||||||
|
<< " queued\n" << std::flush;
|
||||||
PushEvent(event);
|
PushEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -524,6 +542,33 @@ void
|
|||||||
DEBUG_STREAM << "[btntest] RELEASE 0x" << std::hex << s_btnAddr
|
DEBUG_STREAM << "[btntest] RELEASE 0x" << std::hex << s_btnAddr
|
||||||
<< std::dec << " at poll " << s_poll << "\n" << std::flush;
|
<< std::dec << " at poll " << s_poll << "\n" << std::flush;
|
||||||
}
|
}
|
||||||
|
// BT_BTNTEST2="addr,pressPoll,releasePoll": an optional SECOND
|
||||||
|
// scripted cycle (e.g. crouch then rise) on the same poll clock.
|
||||||
|
static int s_bt2Addr = -2, s_bt2On = 0, s_bt2Off = 0, s_bt2State = 0;
|
||||||
|
if (s_bt2Addr == -2)
|
||||||
|
{
|
||||||
|
s_bt2Addr = -1;
|
||||||
|
const char *e2 = getenv("BT_BTNTEST2");
|
||||||
|
if (e2 != NULL)
|
||||||
|
sscanf(e2, "%i,%i,%i", &s_bt2Addr, &s_bt2On, &s_bt2Off);
|
||||||
|
}
|
||||||
|
if (s_bt2Addr >= 0)
|
||||||
|
{
|
||||||
|
if (s_bt2State == 0 && s_poll >= s_bt2On)
|
||||||
|
{
|
||||||
|
s_bt2State = 1;
|
||||||
|
EmitButton(s_bt2Addr, 1);
|
||||||
|
DEBUG_STREAM << "[btntest] PRESS2 0x" << std::hex << s_bt2Addr
|
||||||
|
<< std::dec << " at poll " << s_poll << "\n" << std::flush;
|
||||||
|
}
|
||||||
|
else if (s_bt2State == 1 && s_poll >= s_bt2Off)
|
||||||
|
{
|
||||||
|
s_bt2State = 2;
|
||||||
|
EmitButton(s_bt2Addr, 0);
|
||||||
|
DEBUG_STREAM << "[btntest] RELEASE2 0x" << std::hex << s_bt2Addr
|
||||||
|
<< std::dec << " at poll " << s_poll << "\n" << std::flush;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -739,6 +739,34 @@ void SVGA16::ExpandPlaneToBGRA(int mask, int paletteID, int monoTint, int rotate
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 180 degrees (rotate 2): dimensions UNCHANGED -- the case a landscape
|
||||||
|
// replacement panel needs when the 1995 glass was a portrait CRT mounted
|
||||||
|
// the other way up. Added 2026-08-06 from the crash cart, whose radar is a
|
||||||
|
// landscape LCD: 90 CW left the picture portrait and overhanging its
|
||||||
|
// monitor, and a further 90 CW is this.
|
||||||
|
if (rotateQuadrant == 2)
|
||||||
|
{
|
||||||
|
for (int oy = 0; oy < h; oy++)
|
||||||
|
{
|
||||||
|
unsigned long *d = dst + oy * w;
|
||||||
|
for (int ox = 0; ox < w; ox++)
|
||||||
|
{
|
||||||
|
Word s = base[(h - 1 - oy) * w + (w - 1 - ox)];
|
||||||
|
if (monoTint < 0)
|
||||||
|
{
|
||||||
|
PaletteTriplet *pe = &(pal->paletteData.Color[s & mask]);
|
||||||
|
d[ox] = ((unsigned long)pe->Red << 16) |
|
||||||
|
((unsigned long)pe->Green << 8) | (unsigned long)pe->Blue;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
d[ox] = (s & mask) ? (unsigned long)monoTint : 0UL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (outW) *outW = w;
|
||||||
|
if (outH) *outH = h;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 90-degree rotation: output is transposed (ow = h, oh = w). rotate 3 = CW,
|
// 90-degree rotation: output is transposed (ow = h, oh = w). rotate 3 = CW,
|
||||||
// rotate 1 = CCW (the DrawDevSurface convention; BT_GAUGE_SEC_ROT picks it).
|
// rotate 1 = CCW (the DrawDevSurface convention; BT_GAUGE_SEC_ROT picks it).
|
||||||
int ow = h, oh = w;
|
int ow = h, oh = w;
|
||||||
@@ -1180,6 +1208,15 @@ void BTDrawCockpitPanels(LPDIRECT3DDEVICE9 device)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// (NO full-face "flash overlay" here -- twice field-broken 2026-08-03.
|
||||||
|
// The side buttons are BIG rects deliberately tucked UNDER the surfaces;
|
||||||
|
// step 2 already animates their lamp shades, and the protruding edge IS
|
||||||
|
// the port's lamp light (the pod's backlit keys sat beside the CRT --
|
||||||
|
// there is no in-display face to light). An on-top redraw either covers
|
||||||
|
// the MFD imagery with flashing faces, or -- drawn without resetting the
|
||||||
|
// texture state DrawDevSurface leaves bound -- smears the gauge atlas as
|
||||||
|
// striped garbage (operator captures: green stripes, then red faces).)
|
||||||
|
|
||||||
// 4) Flight-block labels (on top; the blue faces aren't covered by surfaces).
|
// 4) Flight-block labels (on top; the blue faces aren't covered by surfaces).
|
||||||
for (int i = 0; i < L.buttonCount; i++)
|
for (int i = 0; i < L.buttonCount; i++)
|
||||||
{
|
{
|
||||||
@@ -1293,7 +1330,7 @@ void BTCockpitMouseUp(void)
|
|||||||
// that turn fires only ONCE PER FULL GAUGE CYCLE (foreground -> background -> copy).
|
// that turn fires only ONCE PER FULL GAUGE CYCLE (foreground -> background -> copy).
|
||||||
// The cycle only advances to the next foreground turn after the THROTTLED background
|
// The cycle only advances to the next foreground turn after the THROTTLED background
|
||||||
// gauge sweep drains the whole active-instrument list -- and that background task
|
// gauge sweep drains the whole active-instrument list -- and that background task
|
||||||
// starves under load (issue #45: "every cockpit instrument freezes at its last paint
|
// starves under load (field reports: "every cockpit instrument freezes at its last paint
|
||||||
// while the underlying values AND fps stay perfectly healthy"). So on a busy MP
|
// while the underlying values AND fps stay perfectly healthy"). So on a busy MP
|
||||||
// mission the lamp sweep ran ~1x/second: the lit buttons froze / flashes stalled while
|
// mission the lamp sweep ran ~1x/second: the lit buttons froze / flashes stalled while
|
||||||
// the 3D view (a separate per-frame foreground render) stayed smooth -- exactly the
|
// the 3D view (a separate per-frame foreground render) stayed smooth -- exactly the
|
||||||
|
|||||||
@@ -4361,6 +4361,12 @@ void
|
|||||||
fogFar = searchLightFogFar;
|
fogFar = searchLightFogFar;
|
||||||
if(fogUpdating)
|
if(fogUpdating)
|
||||||
{
|
{
|
||||||
|
// BT (searchlight, 2026-08-05): the stubbed dpl_SetViewFog below also
|
||||||
|
// carried the PLANES; the per-frame FOGSTART/FOGEND application reads
|
||||||
|
// currentFogNear/Far, so the swap must land there too (the color line
|
||||||
|
// alone left the fog DISTANCES stuck at load values).
|
||||||
|
currentFogNear = fogNear;
|
||||||
|
currentFogFar = fogFar;
|
||||||
mDevice->SetRenderState(D3DRS_FOGCOLOR, D3DCOLOR_XRGB((int)(255 * fogRed), (int)(255 * fogGreen), (int)(255 * fogBlue)));
|
mDevice->SetRenderState(D3DRS_FOGCOLOR, D3DCOLOR_XRGB((int)(255 * fogRed), (int)(255 * fogGreen), (int)(255 * fogBlue)));
|
||||||
// dpl_SetViewFog(
|
// dpl_SetViewFog(
|
||||||
// dplMainView,
|
// dplMainView,
|
||||||
@@ -4381,6 +4387,8 @@ void
|
|||||||
fogFar = noSearchLightFogFar;
|
fogFar = noSearchLightFogFar;
|
||||||
if(fogUpdating)
|
if(fogUpdating)
|
||||||
{
|
{
|
||||||
|
currentFogNear = fogNear; // see the On case
|
||||||
|
currentFogFar = fogFar;
|
||||||
mDevice->SetRenderState(D3DRS_FOGCOLOR, D3DCOLOR_XRGB((int)(255 * fogRed), (int)(255 * fogGreen), (int)(255 * fogBlue)));
|
mDevice->SetRenderState(D3DRS_FOGCOLOR, D3DCOLOR_XRGB((int)(255 * fogRed), (int)(255 * fogGreen), (int)(255 * fogBlue)));
|
||||||
// dpl_SetViewFog(
|
// dpl_SetViewFog(
|
||||||
// dplMainView,
|
// dplMainView,
|
||||||
|
|||||||
@@ -350,6 +350,12 @@ struct MatInfo {
|
|||||||
// EMISSIVE (tag 0x26): pure-emissive materials (diffuse black) render as an
|
// EMISSIVE (tag 0x26): pure-emissive materials (diffuse black) render as an
|
||||||
// unlit glow -- tex x emissive (btpolar:pintBIceEmit_mtl, the polar ice).
|
// unlit glow -- tex x emissive (btpolar:pintBIceEmit_mtl, the polar ice).
|
||||||
bool hasEmissive = false;
|
bool hasEmissive = false;
|
||||||
|
// BTFX "brighten" class (SPOT.BGF searchlight beam cone): the material
|
||||||
|
// smuggles its ADDITIVE factor in DIFFUSE.r (brighten.25 authors diffuse
|
||||||
|
// {0.25,0,0}) and its glow colour in EMISSIVE ({0.9,0.4,0.2} on the night
|
||||||
|
// page). 0 = not a brighten material; >0 = draw as an additive veil,
|
||||||
|
// dest += emissive x factor (the 1995 DIV brighten semantics).
|
||||||
|
float brightenFactor = 0.0f;
|
||||||
float emissive[3] = {0,0,0};
|
float emissive[3] = {0,0,0};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -495,12 +501,14 @@ struct MaterialResolver {
|
|||||||
std::string name, texName, rampName;
|
std::string name, texName, rampName;
|
||||||
MatInfo info;
|
MatInfo info;
|
||||||
bool haveColor = false;
|
bool haveColor = false;
|
||||||
|
float rawDiffuseR = -1.0f; // brighten-class factor carrier
|
||||||
for (const Chunk& ch : c.children) {
|
for (const Chunk& ch : c.children) {
|
||||||
if (ch.id == TAG_NAME) name = chunkStr(ch);
|
if (ch.id == TAG_NAME) name = chunkStr(ch);
|
||||||
else if (ch.id == TAG_MATERIAL_TEXTURE && ch.len > 1) texName = chunkStr(ch, 1);
|
else if (ch.id == TAG_MATERIAL_TEXTURE && ch.len > 1) texName = chunkStr(ch, 1);
|
||||||
else if (ch.id == TAG_RAMP_REF) rampName = chunkStr(ch);
|
else if (ch.id == TAG_RAMP_REF) rampName = chunkStr(ch);
|
||||||
else if (ch.id == TAG_DIFFUSE && ch.len >= 12) {
|
else if (ch.id == TAG_DIFFUSE && ch.len >= 12) {
|
||||||
info.color = packColor(rdF32(ch.data), rdF32(ch.data + 4), rdF32(ch.data + 8));
|
info.color = packColor(rdF32(ch.data), rdF32(ch.data + 4), rdF32(ch.data + 8));
|
||||||
|
rawDiffuseR = rdF32(ch.data);
|
||||||
haveColor = true;
|
haveColor = true;
|
||||||
info.hasDiffuse = true;
|
info.hasDiffuse = true;
|
||||||
} else if (ch.id == TAG_AMBIENT && ch.len >= 12 && !haveColor
|
} else if (ch.id == TAG_AMBIENT && ch.len >= 12 && !haveColor
|
||||||
@@ -520,6 +528,12 @@ struct MaterialResolver {
|
|||||||
info.hasEmissive = (info.emissive[0] + info.emissive[1] + info.emissive[2]) > 0.001f;
|
info.hasEmissive = (info.emissive[0] + info.emissive[1] + info.emissive[2]) > 0.001f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// BTFX brighten class: name-gated so ordinary materials keep
|
||||||
|
// their diffuse semantics untouched (the factor rides only in
|
||||||
|
// "brighten*" records -- brighten.25/.5/.75 in btfx.bmf).
|
||||||
|
if (name.compare(0, 8, "brighten") == 0 && rawDiffuseR > 0.0f)
|
||||||
|
info.brightenFactor = rawDiffuseR;
|
||||||
|
|
||||||
// Resolve the ramp reference to its low/high colours: this
|
// Resolve the ramp reference to its low/high colours: this
|
||||||
// file's own definitions first, then the cross-library
|
// file's own definitions first, then the cross-library
|
||||||
// registry (mech skins reference 'softer' defined elsewhere).
|
// registry (mech skins reference 'softer' defined elsewhere).
|
||||||
@@ -640,10 +654,12 @@ struct Builder {
|
|||||||
bool currentTSphere = false; // material is tsphere_mtl (translocation warp): ramp it despite normals
|
bool currentTSphere = false; // material is tsphere_mtl (translocation warp): ramp it despite normals
|
||||||
bool meshIsTSphere = false; // this OBJECT is the translocation warp -> smooth-tessellate the cone
|
bool meshIsTSphere = false; // this OBJECT is the translocation warp -> smooth-tessellate the cone
|
||||||
bool meshIsCop = false; // this OBJECT is a *_cop cockpit canopy shell (task #55)
|
bool meshIsCop = false; // this OBJECT is a *_cop cockpit canopy shell (task #55)
|
||||||
|
std::string meshStem; // lowercased load stem (BT_MAT_LOG diag)
|
||||||
std::map<const void*, int> copRoleMap; // punch-kit roles per PMESH chunk: 1=mask 2=hull 3=skip (see TAG_PATCH)
|
std::map<const void*, int> copRoleMap; // punch-kit roles per PMESH chunk: 1=mask 2=hull 3=skip (see TAG_PATCH)
|
||||||
int currentCopRole = 0; // role of the pmesh being built (consumed by buildPmesh)
|
int currentCopRole = 0; // role of the pmesh being built (consumed by buildPmesh)
|
||||||
bool currentHasEmissive = false;
|
bool currentHasEmissive = false;
|
||||||
float currentEmissive[3] = {0,0,0};
|
float currentEmissive[3] = {0,0,0};
|
||||||
|
float currentBrighten = 0.0f; // btfx brighten-class additive factor
|
||||||
|
|
||||||
// per-vertex scratch (normals accumulated from forward triangles only)
|
// per-vertex scratch (normals accumulated from forward triangles only)
|
||||||
std::vector<float> px, py, pz, nx, ny, nz, uu, vv;
|
std::vector<float> px, py, pz, nx, ny, nz, uu, vv;
|
||||||
@@ -778,7 +794,19 @@ struct Builder {
|
|||||||
// value is not recoverable from the code (texture-less ramp = no texel
|
// value is not recoverable from the code (texture-less ramp = no texel
|
||||||
// luminance to index); default (0.13,0.12,0.15) [T3] matches the dark
|
// luminance to index); default (0.13,0.12,0.15) [T3] matches the dark
|
||||||
// near-black frame in pod gameplay footage. BT_COP_FRAME="r g b" overrides.
|
// near-black frame in pod gameplay footage. BT_COP_FRAME="r g b" overrides.
|
||||||
if (useRamp && currentTex.empty() && meshIsCop) {
|
// The same interior-structure treatment applies to the blakskn SKIN
|
||||||
|
// family on ANY mesh, not just *_cop shells: the inside skeletons
|
||||||
|
// author pilot-facing surfaces of the OWN-BODY meshes (thor thx_msl
|
||||||
|
// missile pod, owens owx_* legs -- issue #91) with the identical
|
||||||
|
// "<pfx>skin:blakskn_dz_*" material the canopy frame uses, and the
|
||||||
|
// board shaded them all the same way. Keying on the filename alone
|
||||||
|
// left those surfaces vcol=pure-black -- the reported "black
|
||||||
|
// rectangle" was the thor pod's blakskn mount plate at RGB(0,0,0)
|
||||||
|
// instead of the frame constant. mechfx:blakskn_mtl (the tshd
|
||||||
|
// shadow quads) must NOT match -- key on the skin-lib "_dz_" form.
|
||||||
|
const bool blaksknSkin =
|
||||||
|
currentMatName.find("skin:blakskn_dz_") != std::string::npos;
|
||||||
|
if (useRamp && currentTex.empty() && (meshIsCop || blaksknSkin)) {
|
||||||
auto CB = [](float ff){ int v=(int)(ff*255.0f+0.5f); return (uint32_t)(v<0?0:v>255?255:v); };
|
auto CB = [](float ff){ int v=(int)(ff*255.0f+0.5f); return (uint32_t)(v<0?0:v>255?255:v); };
|
||||||
vcol = 0xFF000000u | (CB(copFrameRGB(0))<<16) | (CB(copFrameRGB(1))<<8) | CB(copFrameRGB(2));
|
vcol = 0xFF000000u | (CB(copFrameRGB(0))<<16) | (CB(copFrameRGB(1))<<8) | CB(copFrameRGB(2));
|
||||||
}
|
}
|
||||||
@@ -913,6 +941,7 @@ struct Builder {
|
|||||||
batch.shadowMat = currentShadowMat;
|
batch.shadowMat = currentShadowMat;
|
||||||
batch.hasEmissive = currentHasEmissive;
|
batch.hasEmissive = currentHasEmissive;
|
||||||
for (int i = 0; i < 3; ++i) batch.emissive[i] = currentEmissive[i];
|
for (int i = 0; i < 3; ++i) batch.emissive[i] = currentEmissive[i];
|
||||||
|
batch.brightenFactor = currentBrighten; // btfx additive veil (spot cone)
|
||||||
// SUBMISSION-ORDER DEPTH BIAS (additive objects): the content layers
|
// SUBMISSION-ORDER DEPTH BIAS (additive objects): the content layers
|
||||||
// EXACTLY-COPLANAR shells (AR02 LOD2: solid concrete + a coplanar
|
// EXACTLY-COPLANAR shells (AR02 LOD2: solid concrete + a coplanar
|
||||||
// PUNCH-cutout overlay + an inner shell, plane separations
|
// PUNCH-cutout overlay + an inner shell, plane separations
|
||||||
@@ -939,6 +968,28 @@ struct Builder {
|
|||||||
else
|
else
|
||||||
batch.lodBias = 0.0f;
|
batch.lodBias = 0.0f;
|
||||||
mesh->batches.push_back(batch);
|
mesh->batches.push_back(batch);
|
||||||
|
// #91 diag: BT_MAT_LOG=<substr> dumps each batch of a matching mesh
|
||||||
|
// stem -- the material routing decision (ramp vs lit vs emissive,
|
||||||
|
// resolved colour, texture) for the inside-view black-mesh hunt.
|
||||||
|
{
|
||||||
|
static const char* s_matLog = getenv("BT_MAT_LOG");
|
||||||
|
if (s_matLog && *s_matLog
|
||||||
|
&& meshStem.find(s_matLog) != std::string::npos)
|
||||||
|
fprintf(stderr, "[matlog] %s mat=%s color=%08X hasDiff=%d "
|
||||||
|
"tex='%s' ch=%d hasRamp=%d useRamp=%d norms=%d "
|
||||||
|
"emis=%d vcol=%08X tris=%u\n",
|
||||||
|
meshStem.c_str(), currentMatName.c_str(), currentColor,
|
||||||
|
(int)currentHasDiffuse, currentTex.c_str(),
|
||||||
|
currentTexChannel, (int)currentHasRamp, (int)useRamp,
|
||||||
|
(int)hasNormals(vtag), (int)pureEmissive, vcol,
|
||||||
|
idxCount / 3);
|
||||||
|
if (s_matLog && *s_matLog
|
||||||
|
&& meshStem.find(s_matLog) != std::string::npos)
|
||||||
|
fprintf(stderr, "[matlog] ramp lo=(%.3f,%.3f,%.3f) "
|
||||||
|
"hi=(%.3f,%.3f,%.3f)\n",
|
||||||
|
currentRampLo[0], currentRampLo[1], currentRampLo[2],
|
||||||
|
currentRampHi[0], currentRampHi[1], currentRampHi[2]);
|
||||||
|
}
|
||||||
if (getenv("BT_COP_DUMP") && currentHasRamp && currentTex.empty()) {
|
if (getenv("BT_COP_DUMP") && currentHasRamp && currentTex.empty()) {
|
||||||
float lo[3]={1e9f,1e9f,1e9f}, hi[3]={-1e9f,-1e9f,-1e9f}, cen[3]={0,0,0}; int nv=0;
|
float lo[3]={1e9f,1e9f,1e9f}, hi[3]={-1e9f,-1e9f,-1e9f}, cen[3]={0,0,0}; int nv=0;
|
||||||
for (uint32_t i = idxStart; i < idxStart + idxCount; ++i) {
|
for (uint32_t i = idxStart; i < idxStart + idxCount; ++i) {
|
||||||
@@ -1216,8 +1267,9 @@ struct Builder {
|
|||||||
for (int i = 0; i < 3; ++i) { currentRampLo[i] = info.rampLo[i]; currentRampHi[i] = info.rampHi[i]; }
|
for (int i = 0; i < 3; ++i) { currentRampLo[i] = info.rampLo[i]; currentRampHi[i] = info.rampHi[i]; }
|
||||||
currentHasEmissive = info.hasEmissive;
|
currentHasEmissive = info.hasEmissive;
|
||||||
for (int i = 0; i < 3; ++i) currentEmissive[i] = info.emissive[i];
|
for (int i = 0; i < 3; ++i) currentEmissive[i] = info.emissive[i];
|
||||||
|
currentBrighten = info.brightenFactor;
|
||||||
}
|
}
|
||||||
else { currentColor = colorForMaterial(full); currentTex.clear(); currentHasDiffuse = false; currentTexChannel = 0; currentTexScroll = false; currentTexScrollU = currentTexScrollV = 0.0f; }
|
else { currentColor = colorForMaterial(full); currentTex.clear(); currentHasDiffuse = false; currentTexChannel = 0; currentTexScroll = false; currentTexScrollU = currentTexScrollV = 0.0f; currentBrighten = 0.0f; }
|
||||||
// TRANSLOCATION WARP: override the material's coarse "sky" ramp with the
|
// TRANSLOCATION WARP: override the material's coarse "sky" ramp with the
|
||||||
// NARROW LAVENDER ramp that matched the original (capture.png). The swirl
|
// NARROW LAVENDER ramp that matched the original (capture.png). The swirl
|
||||||
// is the bintA cloud coloured lo->hi by luminance; a wide sky ramp
|
// is the bintA cloud coloured lo->hi by luminance; a wide sky ramp
|
||||||
@@ -1386,6 +1438,7 @@ bool LoadBgfFile(const std::string& name, BgfData& out) {
|
|||||||
// single-siding + dark-frame ramp reconstruction in emitTri/buildPmesh applies to
|
// single-siding + dark-frame ramp reconstruction in emitTri/buildPmesh applies to
|
||||||
// these meshes only.
|
// these meshes only.
|
||||||
b.meshIsCop = stemLower(name).find("_cop") != std::string::npos;
|
b.meshIsCop = stemLower(name).find("_cop") != std::string::npos;
|
||||||
|
b.meshStem = stemLower(name);
|
||||||
b.res = &res;
|
b.res = &res;
|
||||||
for (const Chunk& c : roots) b.collect(c);
|
for (const Chunk& c : roots) b.collect(c);
|
||||||
b.finish();
|
b.finish();
|
||||||
|
|||||||
@@ -44,6 +44,12 @@ struct BgfDrawBatch {
|
|||||||
// alpha). The verts keep their authored RGBA gradient and the draw routes
|
// alpha). The verts keep their authored RGBA gradient and the draw routes
|
||||||
// to the alpha-blend pass, unlit, colour = texture x vertex gradient.
|
// to the alpha-blend pass, unlit, colour = texture x vertex gradient.
|
||||||
bool vertexAlpha = false;
|
bool vertexAlpha = false;
|
||||||
|
// BTFX "brighten" ADDITIVE VEIL (the searchlight SPOT.BGF beam cone): the
|
||||||
|
// material class authors its factor in DIFFUSE.r and its glow colour in
|
||||||
|
// EMISSIVE; the batch draws in the blend pass as dest += emissive x factor
|
||||||
|
// (1995 DIV brighten semantics; the record also authors fog IMMUNE +
|
||||||
|
// DRAWLAST, both satisfied by the blend pass). 0 = not a brighten batch.
|
||||||
|
float brightenFactor = 0.0f;
|
||||||
// TEXTURE ALPHA CUTOUT (issue #3, the wreck scorch splat): an RGBA4444 BSL
|
// TEXTURE ALPHA CUTOUT (issue #3, the wreck scorch splat): an RGBA4444 BSL
|
||||||
// slice (texChannel >= 8) carries an AUTHORED alpha channel -- in the shipped
|
// slice (texChannel >= 8) carries an AUTHORED alpha channel -- in the shipped
|
||||||
// content it is a BINARY 0/240 cutout mask (bexp9 = the scorch splat
|
// content it is a BINARY 0/240 cutout mask (bexp9 = the scorch splat
|
||||||
|
|||||||
@@ -109,6 +109,11 @@ struct L4DRAWOP
|
|||||||
// UNLIT, colour = texture x the authored per-vertex gradient, alpha = the
|
// UNLIT, colour = texture x the authored per-vertex gradient, alpha = the
|
||||||
// per-vertex fade (SRCALPHA/INVSRCALPHA).
|
// per-vertex fade (SRCALPHA/INVSRCALPHA).
|
||||||
bool vertexAlphaBlend;
|
bool vertexAlphaBlend;
|
||||||
|
// BTFX BRIGHTEN VEIL (the searchlight SPOT.BGF cone): additive brighten of
|
||||||
|
// the framebuffer, dest += material.Emissive x this factor (the class
|
||||||
|
// authors the factor in DIFFUSE.r, the glow colour in EMISSIVE). Drawn in
|
||||||
|
// the alpha-blend pass, unlit, fog-immune by authoring. 0 = off.
|
||||||
|
float brightenAlpha;
|
||||||
// COCKPIT PUNCH STENCIL-CUT (task #55, i860-firmware-decoded): 0=normal,
|
// COCKPIT PUNCH STENCIL-CUT (task #55, i860-firmware-decoded): 0=normal,
|
||||||
// 1=aperture MASK (drawn stencil-only, paired with the following hull op),
|
// 1=aperture MASK (drawn stencil-only, paired with the following hull op),
|
||||||
// 2=HULL (drawn stencil-rejected under the mask = window cutouts).
|
// 2=HULL (drawn stencil-rejected under the mask = window cutouts).
|
||||||
|
|||||||
+42
-1
@@ -407,6 +407,32 @@ static const char *gBTCwdFixNote = NULL;
|
|||||||
//
|
//
|
||||||
int gBTFitDisplay = 0;
|
int gBTFitDisplay = 0;
|
||||||
|
|
||||||
|
//
|
||||||
|
// L4PLASMA=NONE/OFF/0 means "no marquee at all" (L4GREND.cpp reads the same
|
||||||
|
// spellings). Only used to keep the boot banner honest.
|
||||||
|
//
|
||||||
|
static bool BTPlasmaDisabled(void)
|
||||||
|
{
|
||||||
|
const char *p = getenv("L4PLASMA");
|
||||||
|
return p != NULL && (_stricmp(p, "NONE") == 0 ||
|
||||||
|
_stricmp(p, "OFF") == 0 ||
|
||||||
|
strcmp(p, "0") == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Which control device the GLASS profile actually resolved to. The banner
|
||||||
|
// used to hardcode "PadRIO", which is a lie on a cab wired to the real board
|
||||||
|
// -- and precisely the log line someone reads to check that (2026-08-06).
|
||||||
|
//
|
||||||
|
static const char *BTControlsSummary(void)
|
||||||
|
{
|
||||||
|
const char *c = getenv("L4CONTROLS");
|
||||||
|
if (c == NULL) return "PadRIO";
|
||||||
|
if (_strnicmp(c, "RIO", 3) == 0) return "hardware RIO";
|
||||||
|
if (_strnicmp(c, "PAD", 3) == 0) return "PadRIO";
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
static void BTEnsureContentDirectory(void)
|
static void BTEnsureContentDirectory(void)
|
||||||
{
|
{
|
||||||
if (GetFileAttributesA("BTL4.RES") != INVALID_FILE_ATTRIBUTES)
|
if (GetFileAttributesA("BTL4.RES") != INVALID_FILE_ATTRIBUTES)
|
||||||
@@ -848,6 +874,18 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
|||||||
{
|
{
|
||||||
gBTFitDisplay = 1;
|
gBTFitDisplay = 1;
|
||||||
}
|
}
|
||||||
|
//
|
||||||
|
// BT_FIT=1 -- the env spelling of -fit, so a POD can freeze it in
|
||||||
|
// content\environ.ini alongside the rest of its rig config instead of
|
||||||
|
// depending on every launcher passing the flag (2026-08-06, crash cart:
|
||||||
|
// the main view has to fill its 800x600 panel on every boot, however the
|
||||||
|
// game gets started -- shortcut, autostart or scheduled task).
|
||||||
|
//
|
||||||
|
{
|
||||||
|
const char *fitEnv = getenv("BT_FIT");
|
||||||
|
if (fitEnv != NULL && fitEnv[0] != '\0' && fitEnv[0] != '0')
|
||||||
|
gBTFitDisplay = 1;
|
||||||
|
}
|
||||||
|
|
||||||
// BT_JOYCONFIG=1: the generic-joystick capture wizard (flight sticks /
|
// BT_JOYCONFIG=1: the generic-joystick capture wizard (flight sticks /
|
||||||
// HOTAS / pedals -- L4JOY.h). Console prompts detect which device/axis
|
// HOTAS / pedals -- L4JOY.h). Console prompts detect which device/axis
|
||||||
@@ -1145,8 +1183,11 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
|||||||
std::cout << "[boot] platform profile: "
|
std::cout << "[boot] platform profile: "
|
||||||
<< (fe_menu_mode ? "MENU (front end -- no profile applied)"
|
<< (fe_menu_mode ? "MENU (front end -- no profile applied)"
|
||||||
: gBTPlatformPod ? "POD (RIO cockpit input; multi-surface gauges/MFDs via pod hardware or explicit L4GAUGE)"
|
: gBTPlatformPod ? "POD (RIO cockpit input; multi-surface gauges/MFDs via pod hardware or explicit L4GAUGE)"
|
||||||
: gBTPlatformGlass ? "GLASS (PadRIO + plasma window)"
|
: gBTPlatformGlass ? "GLASS"
|
||||||
: "DEV (single window + keyboard)");
|
: "DEV (single window + keyboard)");
|
||||||
|
if (gBTPlatformGlass && !fe_menu_mode)
|
||||||
|
std::cout << " (" << BTControlsSummary()
|
||||||
|
<< (BTPlasmaDisabled() ? "; plasma off [L4PLASMA])" : " + plasma window)");
|
||||||
if (!fe_menu_mode)
|
if (!fe_menu_mode)
|
||||||
std::cout << " [secondary displays: " << kGlassLayoutName[glassLayout] << "]";
|
std::cout << " [secondary displays: " << kGlassLayoutName[glassLayout] << "]";
|
||||||
std::cout << std::endl << std::flush;
|
std::cout << std::endl << std::flush;
|
||||||
|
|||||||
+149
-5
@@ -13,6 +13,7 @@
|
|||||||
#pragma pack(push, 8)
|
#pragma pack(push, 8)
|
||||||
#include "steam/steam_api.h"
|
#include "steam/steam_api.h"
|
||||||
#include "steam/isteammatchmaking.h"
|
#include "steam/isteammatchmaking.h"
|
||||||
|
#include <btversion.h> // BUILD GATE (#108 confound): exact-build lobbies
|
||||||
#pragma pack(pop)
|
#pragma pack(pop)
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -48,6 +49,50 @@ static void
|
|||||||
}
|
}
|
||||||
#include <stdarg.h>
|
#include <stdarg.h>
|
||||||
|
|
||||||
|
//
|
||||||
|
// A rejection the PLAYER must see goes through LobbyNotice: the same text
|
||||||
|
// lands in the day log (newlines flattened so the line stays greppable)
|
||||||
|
// AND in a blocking message box. These paths are synchronous FE flows --
|
||||||
|
// a bare return here QUITS the exe, and #68 taught us a player bounced
|
||||||
|
// with only a log line reports "the game just closed".
|
||||||
|
//
|
||||||
|
static void
|
||||||
|
LobbyNotice(const char *format, ...)
|
||||||
|
{
|
||||||
|
char text[512];
|
||||||
|
va_list arguments;
|
||||||
|
va_start(arguments, format);
|
||||||
|
_vsnprintf(text, sizeof(text) - 1, format, arguments);
|
||||||
|
text[sizeof(text) - 1] = 0;
|
||||||
|
va_end(arguments);
|
||||||
|
{
|
||||||
|
char flat[512];
|
||||||
|
int n = 0;
|
||||||
|
for (const char *s = text; *s && n < 510; ++s)
|
||||||
|
{
|
||||||
|
if (*s == '\n')
|
||||||
|
{
|
||||||
|
if (n > 0 && flat[n - 1] == ' ')
|
||||||
|
continue;
|
||||||
|
flat[n++] = ' ';
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
flat[n++] = *s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flat[n] = 0;
|
||||||
|
LobbyLog("NOTICE: %s", flat);
|
||||||
|
}
|
||||||
|
WCHAR wide[512];
|
||||||
|
int n = 0;
|
||||||
|
for (const char *s = text; *s && n < 511; ++s)
|
||||||
|
wide[n++] = (WCHAR)*s;
|
||||||
|
wide[n] = 0;
|
||||||
|
MessageBoxW(NULL, wide, L"BATTLETECH -- STEAM LOBBY",
|
||||||
|
MB_OK | MB_ICONWARNING | MB_SETFOREGROUND);
|
||||||
|
}
|
||||||
|
|
||||||
//###########################################################################
|
//###########################################################################
|
||||||
// Synchronous Steam call-result helper (manual polling -- no callback
|
// Synchronous Steam call-result helper (manual polling -- no callback
|
||||||
// template machinery in this C-style TU).
|
// template machinery in this C-style TU).
|
||||||
@@ -95,6 +140,11 @@ static void
|
|||||||
matchmaking->SetLobbyMemberData(currentLobby, "vh", vehicle);
|
matchmaking->SetLobbyMemberData(currentLobby, "vh", vehicle);
|
||||||
matchmaking->SetLobbyMemberData(currentLobby, "cl", color);
|
matchmaking->SetLobbyMemberData(currentLobby, "cl", color);
|
||||||
matchmaking->SetLobbyMemberData(currentLobby, "xp", experience);
|
matchmaking->SetLobbyMemberData(currentLobby, "xp", experience);
|
||||||
|
// BUILD GATE, member half: publish our exact build so the HOST can
|
||||||
|
// reject mismatches at GO time. This is what catches OLD exes -- they
|
||||||
|
// predate the joiner-side lobby filter, but they can't fake a "bv" key
|
||||||
|
// they never set.
|
||||||
|
matchmaking->SetLobbyMemberData(currentLobby, "bv", BT_VERSION_STRING);
|
||||||
}
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
@@ -186,10 +236,17 @@ static void
|
|||||||
int n = 0;
|
int n = 0;
|
||||||
for (const char *s = member.name; *s && n < 60; ++s) name[n++] = (WCHAR)*s;
|
for (const char *s = member.name; *s && n < 60; ++s) name[n++] = (WCHAR)*s;
|
||||||
name[n] = 0;
|
name[n] = 0;
|
||||||
wsprintfW(line, L" %d. %-16s %hs, %hs%s", i + 1, name,
|
// BUILD GATE: flag mismatched members in the roster so the host
|
||||||
|
// sees WHO won't launch before pressing GO (the log alone left
|
||||||
|
// the operator guessing on field nights).
|
||||||
|
const char *bv = matchmaking->GetLobbyMemberData(currentLobby,
|
||||||
|
matchmaking->GetLobbyMemberByIndex(currentLobby, i), "bv");
|
||||||
|
int build_ok = (bv != NULL && strcmp(bv, BT_VERSION_STRING) == 0);
|
||||||
|
wsprintfW(line, L" %d. %-16s %hs, %hs%s%s", i + 1, name,
|
||||||
member.vehicle[0] ? member.vehicle : "mech",
|
member.vehicle[0] ? member.vehicle : "mech",
|
||||||
member.color[0] ? member.color : "-",
|
member.color[0] ? member.color : "-",
|
||||||
member.isSelf ? L" (you)" : L"");
|
member.isSelf ? L" (you)" : L"",
|
||||||
|
build_ok ? L"" : L" [WRONG BUILD -- WILL NOT LAUNCH]");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -346,6 +403,12 @@ int
|
|||||||
}
|
}
|
||||||
currentLobby = created.m_ulSteamIDLobby;
|
currentLobby = created.m_ulSteamIDLobby;
|
||||||
SteamMatchmaking()->SetLobbyData(currentLobby, "btl4", "1");
|
SteamMatchmaking()->SetLobbyData(currentLobby, "btl4", "1");
|
||||||
|
// BUILD GATE: stamp the host's exact build on the lobby. Joiners filter
|
||||||
|
// on equality (below) and verify after entry -- a mixed-build lobby
|
||||||
|
// silently corrupts raw-struct replication (the night-9 ghost confound:
|
||||||
|
// one stale-zip player desyncs one connection's stream). Same build or
|
||||||
|
// no entry.
|
||||||
|
SteamMatchmaking()->SetLobbyData(currentLobby, "btl4ver", BT_VERSION_STRING);
|
||||||
PublishSelf(pilot_name, vehicle, color, experience);
|
PublishSelf(pilot_name, vehicle, color, experience);
|
||||||
LobbyLog("host: lobby up (%llu)", (unsigned long long)created.m_ulSteamIDLobby);
|
LobbyLog("host: lobby up (%llu)", (unsigned long long)created.m_ulSteamIDLobby);
|
||||||
|
|
||||||
@@ -371,6 +434,25 @@ int
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (!member.isSelf)
|
||||||
|
{
|
||||||
|
// BUILD GATE, host half: a member on a different zip (or an
|
||||||
|
// exe old enough to have no "bv" key at all) gets NO token.
|
||||||
|
// Omitted from btl4map, their client -- old builds included,
|
||||||
|
// the code predates the gate -- hits its own "the host's map
|
||||||
|
// is missing us" path and fails the join cleanly instead of
|
||||||
|
// desyncing the session (#108's stale-zip confound).
|
||||||
|
const char *bv = matchmaking->GetLobbyMemberData(
|
||||||
|
currentLobby, CSteamID(member.steamID), "bv");
|
||||||
|
if (bv == NULL || strcmp(bv, BT_VERSION_STRING) != 0)
|
||||||
|
{
|
||||||
|
LobbyLog("host: REJECT %s at GO -- their build [%s], "
|
||||||
|
"ours %s (no token minted; their join will fail)",
|
||||||
|
member.name, (bv && *bv) ? bv : "pre-gate/unknown",
|
||||||
|
BT_VERSION_STRING);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
if ((pass == 0) == (member.isSelf != 0))
|
if ((pass == 0) == (member.isSelf != 0))
|
||||||
{
|
{
|
||||||
roster_out->members[roster_out->memberCount++] = member;
|
roster_out->members[roster_out->memberCount++] = member;
|
||||||
@@ -420,13 +502,20 @@ int
|
|||||||
{
|
{
|
||||||
if (BTSteamNet_Install() != 0 || !BTSteamNet_Active())
|
if (BTSteamNet_Install() != 0 || !BTSteamNet_Active())
|
||||||
{
|
{
|
||||||
LobbyLog("join: Steam transport unavailable");
|
LobbyNotice("STEAM UNAVAILABLE\n\n"
|
||||||
|
"Could not reach Steam.\n"
|
||||||
|
"Is Steam running and logged in?");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
ISteamMatchmaking *matchmaking = SteamMatchmaking();
|
ISteamMatchmaking *matchmaking = SteamMatchmaking();
|
||||||
matchmaking->AddRequestLobbyListStringFilter(
|
matchmaking->AddRequestLobbyListStringFilter(
|
||||||
"btl4", "1", k_ELobbyComparisonEqual);
|
"btl4", "1", k_ELobbyComparisonEqual);
|
||||||
|
// BUILD GATE: only lobbies stamped with OUR exact build are visible. A
|
||||||
|
// stale-zip player finds nothing -- and the log names the build so the
|
||||||
|
// "no lobby found" report is self-diagnosing (#68's silent-exit lesson).
|
||||||
|
matchmaking->AddRequestLobbyListStringFilter(
|
||||||
|
"btl4ver", BT_VERSION_STRING, k_ELobbyComparisonEqual);
|
||||||
SteamAPICall_t call = matchmaking->RequestLobbyList();
|
SteamAPICall_t call = matchmaking->RequestLobbyList();
|
||||||
LobbyMatchList_t match_list;
|
LobbyMatchList_t match_list;
|
||||||
memset(&match_list, 0, sizeof(match_list));
|
memset(&match_list, 0, sizeof(match_list));
|
||||||
@@ -434,7 +523,36 @@ int
|
|||||||
LobbyMatchList_t::k_iCallback, 15000) != 0 ||
|
LobbyMatchList_t::k_iCallback, 15000) != 0 ||
|
||||||
match_list.m_nLobbiesMatching == 0)
|
match_list.m_nLobbiesMatching == 0)
|
||||||
{
|
{
|
||||||
LobbyLog("join: no btl4 lobby found");
|
//
|
||||||
|
// Nothing on OUR build. Probe once WITHOUT the version filter so
|
||||||
|
// the rejection is specific: "the host is on a different zip"
|
||||||
|
// beats "no lobby" when one is in fact up. Lobby data rides back
|
||||||
|
// with the list result, readable without joining. (Filters only
|
||||||
|
// apply to the next request, so the probe re-adds the game key.)
|
||||||
|
//
|
||||||
|
matchmaking->AddRequestLobbyListStringFilter(
|
||||||
|
"btl4", "1", k_ELobbyComparisonEqual);
|
||||||
|
call = matchmaking->RequestLobbyList();
|
||||||
|
memset(&match_list, 0, sizeof(match_list));
|
||||||
|
if (WaitApiCall(call, &match_list, sizeof(match_list),
|
||||||
|
LobbyMatchList_t::k_iCallback, 8000) == 0 &&
|
||||||
|
match_list.m_nLobbiesMatching > 0)
|
||||||
|
{
|
||||||
|
const char *host_ver = matchmaking->GetLobbyData(
|
||||||
|
matchmaking->GetLobbyByIndex(0), "btl4ver");
|
||||||
|
LobbyNotice("BUILD MISMATCH\n\n"
|
||||||
|
"A lobby is up, but the host runs build %s\n"
|
||||||
|
"and this machine runs build %s.\n\n"
|
||||||
|
"Everyone must run the same zip to play together.",
|
||||||
|
(host_ver && *host_ver) ? host_ver : "(an older build)",
|
||||||
|
BT_VERSION_STRING);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LobbyNotice("NO LOBBY FOUND\n\n"
|
||||||
|
"No BattleTech lobby is up right now.\n"
|
||||||
|
"(This machine runs build %s.)", BT_VERSION_STRING);
|
||||||
|
}
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -450,6 +568,24 @@ int
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
currentLobby = lobby;
|
currentLobby = lobby;
|
||||||
|
// BUILD GATE belt-and-suspenders: the list filter covers discovery, but
|
||||||
|
// verify the entered lobby too (covers invite/direct joins and any
|
||||||
|
// filter drift). Mismatch = leave loudly, never play version-skewed.
|
||||||
|
{
|
||||||
|
const char *host_ver = matchmaking->GetLobbyData(currentLobby, "btl4ver");
|
||||||
|
if (host_ver == NULL || strcmp(host_ver, BT_VERSION_STRING) != 0)
|
||||||
|
{
|
||||||
|
matchmaking->LeaveLobby(currentLobby);
|
||||||
|
currentLobby = CSteamID();
|
||||||
|
LobbyNotice("BUILD MISMATCH\n\n"
|
||||||
|
"The host runs build %s\n"
|
||||||
|
"and this machine runs build %s.\n\n"
|
||||||
|
"Everyone must run the same zip to play together.",
|
||||||
|
(host_ver && *host_ver) ? host_ver : "(an older build)",
|
||||||
|
BT_VERSION_STRING);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
PublishSelf(pilot_name, vehicle, color, experience);
|
PublishSelf(pilot_name, vehicle, color, experience);
|
||||||
LobbyLog("join: in lobby, waiting for GO");
|
LobbyLog("join: in lobby, waiting for GO");
|
||||||
|
|
||||||
@@ -487,7 +623,15 @@ int
|
|||||||
LobbyLog("join: my token [%s], map [%s]", my_token_out, steam_map_out);
|
LobbyLog("join: my token [%s], map [%s]", my_token_out, steam_map_out);
|
||||||
if (my_token_out[0] == 0)
|
if (my_token_out[0] == 0)
|
||||||
{
|
{
|
||||||
result = -1; // the host's map is missing us
|
// The host launched without us -- no seat in the mission map.
|
||||||
|
// (Same-build clients can only hit this via a full lobby or a
|
||||||
|
// publish race now; version skew is gated before entry.)
|
||||||
|
LobbyNotice("LEFT BEHIND\n\n"
|
||||||
|
"The host launched the mission without this machine\n"
|
||||||
|
"(no seat in the mission map).\n\n"
|
||||||
|
"Rejoin on the next launch. If this repeats, compare\n"
|
||||||
|
"builds: this machine runs %s.", BT_VERSION_STRING);
|
||||||
|
result = -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SteamMatchmaking()->LeaveLobby(currentLobby);
|
SteamMatchmaking()->LeaveLobby(currentLobby);
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ enum BTActionID
|
|||||||
BTActMfd1Cycle, // Gitea #9: cycle the lower-left MFD preset page
|
BTActMfd1Cycle, // Gitea #9: cycle the lower-left MFD preset page
|
||||||
BTActMfd2Cycle, // Gitea #9: cycle the upper-center MFD preset page
|
BTActMfd2Cycle, // Gitea #9: cycle the upper-center MFD preset page
|
||||||
BTActMfd3Cycle, // Gitea #9: cycle the lower-right MFD preset page
|
BTActMfd3Cycle, // Gitea #9: cycle the lower-right MFD preset page
|
||||||
|
BTActEject, // PANIC/EJECT punch-out -> Mech msg 0x19 (@0049f854)
|
||||||
BTActCount
|
BTActCount
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -204,6 +205,7 @@ static const BTName sActionNames[] =
|
|||||||
{"Reconnect", BTActReconnect},
|
{"Reconnect", BTActReconnect},
|
||||||
{"Mfd1Cycle", BTActMfd1Cycle}, {"Mfd2Cycle", BTActMfd2Cycle},
|
{"Mfd1Cycle", BTActMfd1Cycle}, {"Mfd2Cycle", BTActMfd2Cycle},
|
||||||
{"Mfd3Cycle", BTActMfd3Cycle},
|
{"Mfd3Cycle", BTActMfd3Cycle},
|
||||||
|
{"Eject", BTActEject},
|
||||||
{0, 0}
|
{0, 0}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -288,6 +290,8 @@ static const char *sDefaultProfile =
|
|||||||
"key L action Mfd3Cycle\n"
|
"key L action Mfd3Cycle\n"
|
||||||
"key V action ViewToggle\n"
|
"key V action ViewToggle\n"
|
||||||
"key B action LookBehind\n"
|
"key B action LookBehind\n"
|
||||||
|
"key Back action Eject\n"
|
||||||
|
"pad LeftThumb action Eject\n"
|
||||||
"key F5 action Generator1\n"
|
"key F5 action Generator1\n"
|
||||||
"key F6 action Generator2\n"
|
"key F6 action Generator2\n"
|
||||||
"key F7 action Generator3\n"
|
"key F7 action Generator3\n"
|
||||||
@@ -991,6 +995,7 @@ void
|
|||||||
case BTActMfd1Cycle: next.mfdCycle[0] = 1; break;
|
case BTActMfd1Cycle: next.mfdCycle[0] = 1; break;
|
||||||
case BTActMfd2Cycle: next.mfdCycle[1] = 1; break;
|
case BTActMfd2Cycle: next.mfdCycle[1] = 1; break;
|
||||||
case BTActMfd3Cycle: next.mfdCycle[2] = 1; break;
|
case BTActMfd3Cycle: next.mfdCycle[2] = 1; break;
|
||||||
|
case BTActEject: next.eject = 1; break;
|
||||||
case BTActGenerator1: next.genSel = 4; break;
|
case BTActGenerator1: next.genSel = 4; break;
|
||||||
case BTActGenerator2: next.genSel = 5; break;
|
case BTActGenerator2: next.genSel = 5; break;
|
||||||
case BTActGenerator3: next.genSel = 6; break;
|
case BTActGenerator3: next.genSel = 6; break;
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ struct BTInputState
|
|||||||
int valve;
|
int valve;
|
||||||
int flush; // Gitea #7: coolant flush HELD (InjectCoolant)
|
int flush; // Gitea #7: coolant flush HELD (InjectCoolant)
|
||||||
int configHold;
|
int configHold;
|
||||||
|
int eject; // PANIC/EJECT punch-out (level; mech4 edge-dispatches Mech 0x19)
|
||||||
int genSel; // 0 = none, 4..7 = Generator A..D, 8 = reconnect
|
int genSel; // 0 = none, 4..7 = Generator A..D, 8 = reconnect
|
||||||
// Gitea #9: per-MFD preset-page cycle (desktop senders for SetPresetMode;
|
// Gitea #9: per-MFD preset-page cycle (desktop senders for SetPresetMode;
|
||||||
// index 0/1/2 = Mfd1 lower-left / Mfd2 upper-center / Mfd3 lower-right)
|
// index 0/1/2 = Mfd1 lower-left / Mfd2 upper-center / Mfd3 lower-right)
|
||||||
|
|||||||
@@ -276,6 +276,14 @@ void
|
|||||||
mem_stream->ReadBytes(&item_condition, sizeof(item_condition)); // (*stream+0x1c)
|
mem_stream->ReadBytes(&item_condition, sizeof(item_condition)); // (*stream+0x1c)
|
||||||
mem_stream->ReadBytes(&lamp_code, sizeof(lamp_code));
|
mem_stream->ReadBytes(&lamp_code, sizeof(lamp_code));
|
||||||
|
|
||||||
|
if (BTLampLog())
|
||||||
|
DEBUG_STREAM << "[galm-item] sub='"
|
||||||
|
<< (the_subsystem ? (const char *)the_subsystem->GetName() : "?")
|
||||||
|
<< "' cond=" << (int)the_condition
|
||||||
|
<< " itemCond=" << item_condition
|
||||||
|
<< " lampCode=0x" << std::hex << lamp_code << std::dec
|
||||||
|
<< "\n" << std::flush;
|
||||||
|
|
||||||
if ((int)the_condition != item_condition)
|
if ((int)the_condition != item_condition)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -2020,11 +2020,23 @@ BallisticWeaponCluster::BallisticWeaponCluster(
|
|||||||
destroyedLamp = new TwoState(ChildRate(), eng_mode, renderer_in, owner_ID,
|
destroyedLamp = new TwoState(ChildRate(), eng_mode, renderer_in, owner_ID,
|
||||||
engPort, 0xc2, 0x85, "edestryd.pcc", 0, 0xff, (int *)&failedState, "TwoState"); // the destroyed X: bright when FAILED
|
engPort, 0xc2, 0x85, "edestryd.pcc", 0, 0xff, (int *)&failedState, "TwoState"); // the destroyed X: bright when FAILED
|
||||||
|
|
||||||
// ejectWipe (BitMapInverseWipeScalar @004c61c8, eject-timer wipe reading
|
// ejectWipe (BitMapInverseWipeScalar @004c61c8): the round-EJECT progress
|
||||||
// subsys+0x3f8). BRING-UP: the BitMapInverseWipeScalar class is not yet
|
// sweep -- bteejtm.pcc as the weapon's PercentOfEject (@0x3F8) runs 0..1.
|
||||||
// declared/reconstructed in btl4gaug (only the non-Scalar BitMapInverseWipe);
|
// Binary call (part_014.c:2312): FUN_004c61c8(., rate, eng_mode,
|
||||||
// tracked NULL until it lands.
|
// renderer, ownerID, engPort, 0xF, 0, "bteejtm.pcc", 0, 0xFF,
|
||||||
ejectWipe = NULL; // @0x44
|
// weapon+0x3f8, name). RE-WIRED 2026-08-03 after the draw op was
|
||||||
|
// properly settled: vtbl+0x58 = DrawBitMapOpaque, NOT DrawBitMap -- the
|
||||||
|
// mis-guess was the striped-MFD corruption (gotcha #27; the wipe's
|
||||||
|
// Execute in btl4gaug now carries the byte-exact two-span transcription).
|
||||||
|
{
|
||||||
|
extern Scalar *BTWeaponPercentOfEjectPtr(void *weapon);
|
||||||
|
Scalar *ejectPct = BTWeaponPercentOfEjectPtr(subsystem_in);
|
||||||
|
ejectWipe = (ejectPct != NULL)
|
||||||
|
? new BitMapInverseWipeScalar(ChildRate(), eng_mode, renderer_in,
|
||||||
|
owner_ID, engPort, 0xF, 0, "bteejtm.pcc", 0, 0xFF,
|
||||||
|
ejectPct, "BitMapInverseWipeScalar")
|
||||||
|
: NULL; // @0x44
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BallisticWeaponCluster::~BallisticWeaponCluster()
|
BallisticWeaponCluster::~BallisticWeaponCluster()
|
||||||
|
|||||||
@@ -2152,6 +2152,114 @@ void BitMapInverseWipe::Execute()
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//###########################################################################
|
||||||
|
// BitMapInverseWipeScalar @004c61c8 ctor / @004c5fb8 Execute
|
||||||
|
//###########################################################################
|
||||||
|
//
|
||||||
|
// The weapon-eng round-EJECT progress sweep (bteejtm.pcc <- PercentOfEject).
|
||||||
|
// Base ctor @004c5e84 interns the image and captures colours + the bitmap
|
||||||
|
// geometry; the @004c61c8 tail adds the Scalar value-watcher (FUN_00474855
|
||||||
|
// into this+0xA8) -- our GaugeConnectionDirectOf<Scalar> is that watcher.
|
||||||
|
//
|
||||||
|
BitMapInverseWipeScalar::BitMapInverseWipeScalar(
|
||||||
|
GaugeRate rate, ModeMask mode_mask, L4GaugeRenderer *renderer_in,
|
||||||
|
int owner_ID, int graphics_port_number, int x, int y, const char *image,
|
||||||
|
int color_a, int color_b, Scalar *value_pointer,
|
||||||
|
const char *identification_string
|
||||||
|
):
|
||||||
|
GraphicGauge(rate, mode_mask, renderer_in, owner_ID, graphics_port_number,
|
||||||
|
identification_string)
|
||||||
|
{
|
||||||
|
localView.SetOrigin(x, y);
|
||||||
|
imageName = new char[strlen(image) + 1];
|
||||||
|
strcpy(imageName, image);
|
||||||
|
colorA = color_a;
|
||||||
|
colorB = color_b;
|
||||||
|
value = 0.0f;
|
||||||
|
previousLevel = -1;
|
||||||
|
L4Warehouse *warehouse = (L4Warehouse *)renderer_in->warehousePointer;
|
||||||
|
BitMap *bmp = warehouse->bitMapBin.Get(imageName);
|
||||||
|
if (bmp == NULL)
|
||||||
|
{
|
||||||
|
width = 0;
|
||||||
|
height = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
width = bmp->Data.Size.x;
|
||||||
|
height = bmp->Data.Size.y;
|
||||||
|
}
|
||||||
|
AddConnection(new GaugeConnectionDirectOf<Scalar>(0, &value, value_pointer));
|
||||||
|
}
|
||||||
|
|
||||||
|
BitMapInverseWipeScalar::~BitMapInverseWipeScalar()
|
||||||
|
{
|
||||||
|
L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer;
|
||||||
|
warehouse->bitMapBin.Release(imageName);
|
||||||
|
delete[] imageName;
|
||||||
|
imageName = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logical BitMapInverseWipeScalar::TestInstance() const { return GraphicGauge::TestInstance(); }
|
||||||
|
|
||||||
|
void BitMapInverseWipeScalar::BecameActive() // @004c5fa4
|
||||||
|
{
|
||||||
|
previousLevel = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// @004c5fb8 -- Execute. level = round(value x width) clamped to [0, width]
|
||||||
|
// (the x87 form Ghidra drops -- fild width / fmul value, gotcha #19), and on
|
||||||
|
// a change draw the two source-column spans OPAQUE with INVERTED colour
|
||||||
|
// pairs -- the literal "inverse wipe".
|
||||||
|
//
|
||||||
|
// SETTLED 2026-08-03 (the MFD-corruption arc): the binary's draw call is
|
||||||
|
// vtbl+0x58 = GraphicsView::DrawBitMapOpaque(background, rotation, bitmap,
|
||||||
|
// sx1, sy1, sx2, sy2) [T0 GRAPH2D.h slot 22 -- the NEXT virtual after
|
||||||
|
// DrawBitMap at +0x54; verified by counting the declaration order:
|
||||||
|
// +0x18 SetColor, +0x24 MoveToAbsolute both land exactly]. The earlier
|
||||||
|
// reconstruction guessed DrawBitMap and fed it a colour as "rotation" --
|
||||||
|
// the striped-garbage smear the operator captured. Byte-exact spans:
|
||||||
|
// level != 0: MoveTo(0,0); SetColor(colorA);
|
||||||
|
// DrawBitMapOpaque(colorB, 0, bmp, 0, 0, level, height);
|
||||||
|
// ++level; // inclusive-bounds step
|
||||||
|
// level < w: MoveTo(level,0); SetColor(colorB);
|
||||||
|
// DrawBitMapOpaque(colorA, 0, bmp, level, 0, width, height);
|
||||||
|
//
|
||||||
|
void BitMapInverseWipeScalar::Execute()
|
||||||
|
{
|
||||||
|
Scalar scaled = value * (Scalar)width;
|
||||||
|
int level = (int)(scaled + (scaled < 0.0f ? -0.5f : 0.5f)); // FUN_004dcd94 round
|
||||||
|
if (level < 0) level = 0;
|
||||||
|
if (level > width) level = width;
|
||||||
|
|
||||||
|
if (level != previousLevel)
|
||||||
|
{
|
||||||
|
previousLevel = level;
|
||||||
|
L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer;
|
||||||
|
BitMap *bmp = warehouse->bitMapBin.Get(imageName);
|
||||||
|
if (bmp != NULL)
|
||||||
|
{
|
||||||
|
int split = level;
|
||||||
|
if (split != 0)
|
||||||
|
{
|
||||||
|
localView.MoveToAbsolute(0, 0);
|
||||||
|
localView.SetColor(colorA);
|
||||||
|
localView.DrawBitMapOpaque(colorB, 0, bmp, 0, 0, split, height);
|
||||||
|
++split; // the binary's iVar2++
|
||||||
|
}
|
||||||
|
if (split < width)
|
||||||
|
{
|
||||||
|
localView.MoveToAbsolute(split, 0);
|
||||||
|
localView.SetColor(colorB);
|
||||||
|
localView.DrawBitMapOpaque(colorA, 0, bmp, split, 0, width, height);
|
||||||
|
}
|
||||||
|
warehouse->bitMapBin.Release(imageName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//###########################################################################
|
//###########################################################################
|
||||||
//###########################################################################
|
//###########################################################################
|
||||||
// HeadingPointer @004c554c Make / @004c562c ctor
|
// HeadingPointer @004c554c Make / @004c562c ctor
|
||||||
|
|||||||
@@ -541,6 +541,39 @@
|
|||||||
Scalar value; // @0xB4 this[0x2D] (connection)
|
Scalar value; // @0xB4 this[0x2D] (connection)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//#######################################################################
|
||||||
|
// BitMapInverseWipeScalar @004c61c8 (vtable 0x518a14; base ctor @004c5e84,
|
||||||
|
// Execute @004c5fb8, BecameActive @004c5fa4). A bitmap COLUMN SWEEP whose
|
||||||
|
// level tracks a live Scalar 0..1 (the ctor's trailing Scalar* rides a
|
||||||
|
// value-watcher into this+0xA8): columns [0..level] draw fg colorA over
|
||||||
|
// bg colorB, the remainder draws the INVERSE -- the weapon-engineering
|
||||||
|
// panel's round-EJECT progress graphic (bteejtm.pcc, watching the
|
||||||
|
// weapon's PercentOfEject @0x3F8). Reconstructed 2026-08-03 (#118: the
|
||||||
|
// operator saw the eject slot flash with NO graphic -- this was the
|
||||||
|
// tracked-NULL bring-up stub in BallisticWeaponCluster).
|
||||||
|
//#######################################################################
|
||||||
|
class BitMapInverseWipeScalar :
|
||||||
|
public GraphicGauge
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
BitMapInverseWipeScalar( // @004c61c8
|
||||||
|
GaugeRate, ModeMask, L4GaugeRenderer *, int owner_ID,
|
||||||
|
int graphics_port_number, int x, int y, const char *image,
|
||||||
|
int color_a, int color_b, Scalar *value_pointer, const char *);
|
||||||
|
~BitMapInverseWipeScalar();
|
||||||
|
Logical TestInstance() const;
|
||||||
|
void BecameActive(); // @004c5fa4 (previousLevel = -1)
|
||||||
|
void Execute(); // @004c5fb8 (two-span inverse sweep)
|
||||||
|
protected:
|
||||||
|
char *imageName; // @0x90
|
||||||
|
int colorA; // @0x94
|
||||||
|
int colorB; // @0x98
|
||||||
|
int width; // @0x9C bitmap width (columns)
|
||||||
|
int height; // @0xA0 bitmap height
|
||||||
|
int previousLevel; // @0xA4
|
||||||
|
Scalar value; // @0xA8 (connection: the 0..1 progress)
|
||||||
|
};
|
||||||
|
|
||||||
//#######################################################################
|
//#######################################################################
|
||||||
// SegmentArc gauges -- needle / dial over a 270-degree arc. Derived from
|
// SegmentArc gauges -- needle / dial over a 270-degree arc. Derived from
|
||||||
// the MUNGA L4 arc primitives (FUN_004745e0 / FUN_00473f44).
|
// the MUNGA L4 arc primitives (FUN_004745e0 / FUN_00473f44).
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
// Function -> method map:
|
// Function -> method map:
|
||||||
// L4MechControlsMapper ----------------------------- vtable @0051e440
|
// L4MechControlsMapper ----------------------------- vtable @0051e440
|
||||||
// @004d17ac ctor @004d1814 dtor
|
// @004d17ac ctor @004d1814 dtor
|
||||||
// @004d196c InterpretControls (target-range ramp + review-mode watch)
|
// @004d196c InterpretControls (target-range ramp + panic-arm watch)
|
||||||
// @004d1b64 ZoomTargetRangeIn @004d1b9c ZoomTargetRangeOut
|
// @004d1b64 ZoomTargetRangeIn @004d1b9c ZoomTargetRangeOut
|
||||||
// @004d1acc NotifyOfControlModeChange (+0x48; forwards to base no-op @004b048c)
|
// @004d1acc NotifyOfControlModeChange (+0x48; forwards to base no-op @004b048c)
|
||||||
// @004d1ae4 NotifyOfDisplayModeChange (+0x4C; the secondary-view mask swap
|
// @004d1ae4 NotifyOfDisplayModeChange (+0x4C; the secondary-view mask swap
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
// DAT_0051dcd0[8] = {0x37,0x36,0x35,0x34,0x33,0x32,0x31,0x30} hotbox buttons
|
// DAT_0051dcd0[8] = {0x37,0x36,0x35,0x34,0x33,0x32,0x31,0x30} hotbox buttons
|
||||||
//
|
//
|
||||||
// ModeManager (app+0x50): +0x4 currentMode mask, +0x8 savedMode mask.
|
// ModeManager (app+0x50): +0x4 currentMode mask, +0x8 savedMode mask.
|
||||||
// Mech (mapper owner @this+0xd0): +0x404 targetRange, +0x414 missionReviewMode.
|
// Mech (mapper owner @this+0xd0): +0x404 targetRange, +0x414 ejectPermitted.
|
||||||
//
|
//
|
||||||
|
|
||||||
#include <bt.hpp>
|
#include <bt.hpp>
|
||||||
@@ -329,7 +329,7 @@ L4MechControlsMapper::MessageHandlerSet&
|
|||||||
//
|
//
|
||||||
// Chains to MechControlsMapper (FUN_004b02f0); stamps vtable &0051e440 and
|
// Chains to MechControlsMapper (FUN_004b02f0); stamps vtable &0051e440 and
|
||||||
// initialises the target-range zoom (exponent 2.0 == 250*2^2 == 1000m) and the
|
// initialises the target-range zoom (exponent 2.0 == 250*2^2 == 1000m) and the
|
||||||
// review-mode watcher.
|
// panic-arm watcher (ejectPermitted edge).
|
||||||
//
|
//
|
||||||
L4MechControlsMapper::L4MechControlsMapper(
|
L4MechControlsMapper::L4MechControlsMapper(
|
||||||
Mech *owner,
|
Mech *owner,
|
||||||
@@ -349,7 +349,7 @@ L4MechControlsMapper::MessageHandlerSet&
|
|||||||
)
|
)
|
||||||
{
|
{
|
||||||
(void)class_ID;
|
(void)class_ID;
|
||||||
previousMissionReviewMode = 0; // this[0x6a] @0x1a8
|
previousEjectPermitted = 0; // this[0x6a] @0x1a8
|
||||||
targetRangeExponentDemand = 2.0f; // this[0x68] @0x1a0
|
targetRangeExponentDemand = 2.0f; // this[0x68] @0x1a0
|
||||||
targetRangeExponent = 2.0f; // this[0x69] @0x1a4
|
targetRangeExponent = 2.0f; // this[0x69] @0x1a4
|
||||||
Check_Fpu();
|
Check_Fpu();
|
||||||
@@ -374,8 +374,14 @@ L4MechControlsMapper::MessageHandlerSet&
|
|||||||
// The L4-layer performance, run every frame ahead of the in-Mech mapper:
|
// The L4-layer performance, run every frame ahead of the in-Mech mapper:
|
||||||
// 1. snap the throttle to a full-throttle detent,
|
// 1. snap the throttle to a full-throttle detent,
|
||||||
// 2. (re)build the pilot roster,
|
// 2. (re)build the pilot roster,
|
||||||
// 3. watch the Mech's mission-review flag and toggle the corresponding mode
|
// 3. watch the Mech's EJECT-PERMISSION flag (ejectPermitted @0x414 --
|
||||||
// bit (0x200000) on the application mode manager,
|
// refreshed each frame by the master performance, FUN_004a9b5c+0x10 ->
|
||||||
|
// @0049fa1c) and toggle the PANIC-ARMED mode bit (0x200000) on the
|
||||||
|
// application mode manager. That mode bit is what lights the pod's
|
||||||
|
// physical PANIC button (MakeLinkedLamp, btl4mppr RIO ctor) and any
|
||||||
|
// gauge elements carrying the mode in their ModeMask. [Corrected
|
||||||
|
// 2026-08-03: this cell was mislabeled "mission-review mode" -- the
|
||||||
|
// real review mode is the GLOBAL DAT_004fd550 (btl4pb), not mech+0x414.]
|
||||||
// 4. slew the smoothed target-range exponent toward the panel demand and
|
// 4. slew the smoothed target-range exponent toward the panel demand and
|
||||||
// push the resulting range (250 * 2^exponent) onto the Mech,
|
// push the resulting range (250 * 2^exponent) onto the Mech,
|
||||||
// 5. delegate to MechControlsMapper::InterpretControls for the actual
|
// 5. delegate to MechControlsMapper::InterpretControls for the actual
|
||||||
@@ -405,19 +411,19 @@ L4MechControlsMapper::MessageHandlerSet&
|
|||||||
BuildPilotArray(); // FUN_004b0600
|
BuildPilotArray(); // FUN_004b0600
|
||||||
|
|
||||||
//
|
//
|
||||||
// (3) Mission-review mode change watcher.
|
// (3) Panic-arm watcher: edge-detect ejectPermitted -> mode 0x200000.
|
||||||
//
|
//
|
||||||
Mech *mech = GetMech();
|
Mech *mech = GetMech();
|
||||||
int review_mode = mech->GetMissionReviewMode(); // (mech)+0x414
|
int eject_armed = mech->GetEjectPermitted(); // (mech)+0x414
|
||||||
if (review_mode != previousMissionReviewMode) // @0x1a8
|
if (eject_armed != previousEjectPermitted) // @0x1a8
|
||||||
{
|
{
|
||||||
previousMissionReviewMode = review_mode;
|
previousEjectPermitted = eject_armed;
|
||||||
|
|
||||||
BTL4ModeManager *mode_manager =
|
BTL4ModeManager *mode_manager =
|
||||||
(BTL4ModeManager*)application->GetModeManager(); // DAT_004efc94+0x50
|
(BTL4ModeManager*)application->GetModeManager(); // DAT_004efc94+0x50
|
||||||
// ModeManager API: Add/RemoveModeMask save the old mask into
|
// ModeManager API: Add/RemoveModeMask save the old mask into
|
||||||
// previousModeMask (the recovered "savedMode = currentMode" step).
|
// previousModeMask (the recovered "savedMode = currentMode" step).
|
||||||
if (review_mode == 0)
|
if (eject_armed == 0)
|
||||||
{
|
{
|
||||||
mode_manager->RemoveModeMask(0x200000);
|
mode_manager->RemoveModeMask(0x200000);
|
||||||
}
|
}
|
||||||
@@ -425,6 +431,10 @@ L4MechControlsMapper::MessageHandlerSet&
|
|||||||
{
|
{
|
||||||
mode_manager->AddModeMask(0x200000);
|
mode_manager->AddModeMask(0x200000);
|
||||||
}
|
}
|
||||||
|
if (getenv("BT_EJECT_LOG"))
|
||||||
|
DEBUG_STREAM << "[eject] panic-arm mode "
|
||||||
|
<< (eject_armed ? "ON" : "off") << " ("
|
||||||
|
<< mech->GetEntityID() << ")\n" << std::flush;
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -74,8 +74,17 @@
|
|||||||
//
|
//
|
||||||
public:
|
public:
|
||||||
enum {
|
enum {
|
||||||
KeypressMessageID = MechControlsMapper::NextMessageID,
|
// PINNED to the binary literals (FUN_004d266c passes 0x19/0x1a
|
||||||
StringMatchMessageID,
|
// straight into the group Adds; corrected 2026-08-03). The chain
|
||||||
|
// arithmetic (MechControlsMapper::NextMessageID) lands on 0x17 --
|
||||||
|
// TWO mapper ids between 0x16 ToggleVoiceAssist and 0x19 are
|
||||||
|
// unreconstructed -- and the mis-numbered Keypress send was
|
||||||
|
// SILENTLY swallowed by the Mech (no 0x17 handler), which is why
|
||||||
|
// the armed pilot-keypad press never ejected. Mech::EjectPilot is
|
||||||
|
// id 0x19 on the MECH's receiver: the panic-mode keypad binding
|
||||||
|
// sends THIS id to the OWNER, and the collision is the design.
|
||||||
|
KeypressMessageID = 0x19,
|
||||||
|
StringMatchMessageID, // 0x1a (unregistered; stringManager route)
|
||||||
NextMessageID
|
NextMessageID
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -203,7 +212,9 @@
|
|||||||
targetRangeExponentDemand, // @0x1a0 panel "zoom" demand (init 2.0, range 0..5)
|
targetRangeExponentDemand, // @0x1a0 panel "zoom" demand (init 2.0, range 0..5)
|
||||||
targetRangeExponent; // @0x1a4 smoothed value -> mech target range (2^x)
|
targetRangeExponent; // @0x1a4 smoothed value -> mech target range (2^x)
|
||||||
int
|
int
|
||||||
previousMissionReviewMode; // @0x1a8 last-seen mech mission-review flag
|
previousEjectPermitted; // @0x1a8 last-seen mech ejectPermitted (@0x414) --
|
||||||
|
// the panic-arm edge detector (was mislabeled
|
||||||
|
// "mission-review flag"; corrected 2026-08-03)
|
||||||
|
|
||||||
//
|
//
|
||||||
// Per-platform input staging slots (filled by the derived ctor's
|
// Per-platform input staging slots (filled by the derived ctor's
|
||||||
@@ -305,8 +316,12 @@
|
|||||||
// was previously self-consistent at the wrong value (0x2a), so it
|
// was previously self-consistent at the wrong value (0x2a), so it
|
||||||
// worked by accident -- now it matches the binary.
|
// worked by accident -- now it matches the binary.
|
||||||
// (Faithful-fidelity note: the binary RIO table also carries its OWN
|
// (Faithful-fidelity note: the binary RIO table also carries its OWN
|
||||||
// Keypress id 0x19 -> @004d2514; our ctor still registers the shared
|
// Keypress id: FIXED 2026-08-03 -- KeypressMessageID is now pinned to
|
||||||
// L4 Keypress (0x17, @004d1bf0) -- see the Gitea backlog issue.)
|
// the binary's 0x19 (the old chain arithmetic landed on 0x17, and the
|
||||||
|
// panic-mode pilot-keypad send was silently swallowed by the Mech --
|
||||||
|
// no 0x17 handler -- so the armed eject press never fired). Remaining
|
||||||
|
// refinement: the ctor registers the shared L4 Keypress BODY
|
||||||
|
// (@004d1bf0); the RIO-specific body is @004d2514.)
|
||||||
//
|
//
|
||||||
enum {
|
enum {
|
||||||
Aux1QuadMessageID = MechControlsMapper::Aux1QuadMessageID, // 3
|
Aux1QuadMessageID = MechControlsMapper::Aux1QuadMessageID, // 3
|
||||||
|
|||||||
@@ -369,8 +369,13 @@ BTL4PlaybackApplication::SharedData
|
|||||||
|
|
||||||
//
|
//
|
||||||
// Controls only matter in interactive ("scrub") review mode.
|
// Controls only matter in interactive ("scrub") review mode.
|
||||||
|
// The binary reads the GLOBAL review-mode cell DAT_004fd550 here --
|
||||||
|
// NOT mech+0x414 (that cell is ejectPermitted; the old
|
||||||
|
// GetMissionReviewMode() call was a mislabel, corrected 2026-08-03).
|
||||||
|
// The port never enters scrub mode, so the global stays 0.
|
||||||
//
|
//
|
||||||
if (GetMissionReviewMode() == 2) // DAT_004fd550
|
extern int gMissionReviewMode; // DAT_004fd550 (btstubs)
|
||||||
|
if (gMissionReviewMode == 2)
|
||||||
{
|
{
|
||||||
Check(controlsManager);
|
Check(controlsManager);
|
||||||
controlsManager->Execute(); // vtbl+0x18
|
controlsManager->Execute(); // vtbl+0x18
|
||||||
|
|||||||
+615
-16
@@ -170,6 +170,104 @@ void
|
|||||||
// localToWorld in its ctor -- no explicit AddDynamicRenderable here
|
// localToWorld in its ctor -- no explicit AddDynamicRenderable here
|
||||||
// (unlike the 1996 VideoComponent path).
|
// (unlike the 1996 VideoComponent path).
|
||||||
(void)mech_root;
|
(void)mech_root;
|
||||||
|
|
||||||
|
//
|
||||||
|
// SEARCHLIGHT pass (2026-08-05) -- the roster walk's `case 0xbd8`
|
||||||
|
// (@004cef28, raw pseudocode part_014). The binary, per searchlight
|
||||||
|
// subsystem: resolve the "LightOn" attribute (error print
|
||||||
|
// BTL4VID.CPP:0x941 when absent); COCKPIT build (view 1) -> stash up
|
||||||
|
// to TWO attrs for the fog-swap watcher (@00456778); EXTERNAL build
|
||||||
|
// -> load "spot.bgf" (warn if missing), hang it as a child of the
|
||||||
|
// mount joint and bind visibility to the attribute (@0045612c). Our
|
||||||
|
// tree builder does not walk the roster, so the probe bridge
|
||||||
|
// (searchlight.cpp) supplies {attr, mount segment} and the two
|
||||||
|
// watcher Execute bodies live in TickSearchlight.
|
||||||
|
//
|
||||||
|
{
|
||||||
|
extern int BTMechSearchlightProbe(void *, int **, int *);
|
||||||
|
int *attrs[2];
|
||||||
|
int segs[2];
|
||||||
|
int count = BTMechSearchlightProbe((void *)entity, attrs, segs);
|
||||||
|
std::map<Entity*, MechRenderTree>::iterator ti =
|
||||||
|
mMechRenderTrees.find(entity);
|
||||||
|
if (count > 0 && ti != mMechRenderTrees.end())
|
||||||
|
{
|
||||||
|
MechRenderTree &tree = ti->second;
|
||||||
|
tree.searchLightCount = 0;
|
||||||
|
tree.searchIsCockpit = (view_type == 1);
|
||||||
|
for (int sl = 0; sl < count; ++sl)
|
||||||
|
{
|
||||||
|
MechRenderTree::SearchLight &light =
|
||||||
|
tree.searchLight[tree.searchLightCount];
|
||||||
|
light.cone = NULL;
|
||||||
|
light.coneObj = NULL;
|
||||||
|
light.lightOn = attrs[sl];
|
||||||
|
light.shown = 0;
|
||||||
|
light.mountSeg = segs[sl];
|
||||||
|
// the binary's inverted seed (@00456778: [9]/[10] =
|
||||||
|
// (*attr == 0)) -- guarantees the FIRST tick "changes"
|
||||||
|
// and normalizes the fog to the real lamp state, which
|
||||||
|
// is how a night mission starts on nosearchlightfog.
|
||||||
|
tree.searchFogCache[tree.searchLightCount] =
|
||||||
|
(*attrs[sl] == 0);
|
||||||
|
// BT_SPOT_SELF=1 (bench-only): build the cone on the
|
||||||
|
// OWN-cockpit tree too, so the V-chase camera can
|
||||||
|
// eyeball it solo. Authentic builds never cone the
|
||||||
|
// cockpit tree (the 1995 view-1 branch stashes attrs
|
||||||
|
// only) -- keep this env off outside look benches.
|
||||||
|
if (!tree.searchIsCockpit || getenv("BT_SPOT_SELF"))
|
||||||
|
{
|
||||||
|
// NB: the renderer-level LoadObject wrapper (@00498448) is
|
||||||
|
// still a btstubs no-op; load through the d3d route the
|
||||||
|
// tree builder itself uses (extension REQUIRED there).
|
||||||
|
d3d_OBJECT *spot = d3d_OBJECT::LoadObject(
|
||||||
|
GetDevice(), (char *)"spot.bgf"); // @0051d6c0
|
||||||
|
if (spot == NULL)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "[spot] Couldn't locate spot.bgf "
|
||||||
|
"for mech\n" << std::flush; // @0051d6c9
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
std::map<int, HierarchicalDrawComponent*>::iterator si =
|
||||||
|
tree.segRenderable.find(segs[sl]);
|
||||||
|
HierarchicalDrawComponent *parent =
|
||||||
|
(si != tree.segRenderable.end()) ? si->second : NULL;
|
||||||
|
if (parent == NULL)
|
||||||
|
{
|
||||||
|
// slot-space mismatch would land here -- keep the
|
||||||
|
// receipt loud (see the bench).
|
||||||
|
DEBUG_STREAM << "[spot] mount segment " << segs[sl]
|
||||||
|
<< " has no joint renderable -- cone skipped; slots:";
|
||||||
|
for (std::map<int, HierarchicalDrawComponent*>::iterator di =
|
||||||
|
tree.segRenderable.begin();
|
||||||
|
di != tree.segRenderable.end(); ++di)
|
||||||
|
DEBUG_STREAM << " " << di->first;
|
||||||
|
DEBUG_STREAM << "\n" << std::flush;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LinearMatrix identity(True);
|
||||||
|
dpl_ISECT_MODE gSpotIsectMode; // stub type (binary: mode 1)
|
||||||
|
light.cone = new DPLStaticChildRenderable(
|
||||||
|
entity, false /* main zone */, spot,
|
||||||
|
gSpotIsectMode, 0 /* mask 0: never pickable */,
|
||||||
|
identity, parent);
|
||||||
|
light.cone->SetDrawObj(NULL); // hidden until LightOn
|
||||||
|
light.coneObj = spot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
++tree.searchLightCount;
|
||||||
|
}
|
||||||
|
DEBUG_STREAM << "[spot] searchlight pass: " << count
|
||||||
|
<< " light(s), view=" << (int)view_type
|
||||||
|
<< " mountSeg=" << segs[0]
|
||||||
|
<< (tree.searchIsCockpit ? " (cockpit: fog watcher)"
|
||||||
|
: " (external: spot cone)")
|
||||||
|
<< "\n" << std::flush;
|
||||||
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,7 +499,8 @@ HierarchicalDrawComponent*
|
|||||||
// bring-up diagnostics (counts geometry actually loaded vs. requested)
|
// bring-up diagnostics (counts geometry actually loaded vs. requested)
|
||||||
int dbg_obj_requested = 0, dbg_obj_loaded = 0, dbg_eye = 0;
|
int dbg_obj_requested = 0, dbg_obj_loaded = 0, dbg_eye = 0;
|
||||||
DEBUG_STREAM << "[BTrender] MakeMechRenderables: " << segment_count
|
DEBUG_STREAM << "[BTrender] MakeMechRenderables: " << segment_count
|
||||||
<< " segments, view=" << (int)type << "\n" << std::flush;
|
<< " segments, view=" << (int)type
|
||||||
|
<< " entity=" << entity->GetEntityID() << "\n" << std::flush;
|
||||||
|
|
||||||
JointSubsystem *joint_subsystem = jointed_mover->GetJointSubsystem(); // [0x31c]
|
JointSubsystem *joint_subsystem = jointed_mover->GetJointSubsystem(); // [0x31c]
|
||||||
|
|
||||||
@@ -459,6 +558,11 @@ HierarchicalDrawComponent*
|
|||||||
//
|
//
|
||||||
if (segment->IsSiteSegment() /* [0x10] */ != 0)
|
if (segment->IsSiteSegment() /* [0x10] */ != 0)
|
||||||
{
|
{
|
||||||
|
if (getenv("BT_TREE_LOG"))
|
||||||
|
DEBUG_STREAM << "[tree] SITE seg=" << segment->GetIndex()
|
||||||
|
<< " name=" << (segment->GetName() ? (const char *)segment->GetName() : "?")
|
||||||
|
<< " parent=" << (segment->GetParent() ? segment->GetParentIndex() : -1)
|
||||||
|
<< "\n" << std::flush;
|
||||||
// The authentic COCKPIT EYEPOINT. Built for the true inside view
|
// The authentic COCKPIT EYEPOINT. Built for the true inside view
|
||||||
// AND for the player's chase build (buildDebugChaseCamera), so the
|
// AND for the player's chase build (buildDebugChaseCamera), so the
|
||||||
// V-key toggle can switch to it -- the pod's only view was this
|
// V-key toggle can switch to it -- the pod's only view was this
|
||||||
@@ -491,6 +595,22 @@ HierarchicalDrawComponent*
|
|||||||
mCamera = mEyeCockpit;
|
mCamera = mEyeCockpit;
|
||||||
dbg_eye = 1;
|
dbg_eye = 1;
|
||||||
}
|
}
|
||||||
|
//
|
||||||
|
// SEARCHLIGHT (2026-08-05): sites are ATTACHMENT joints, and the
|
||||||
|
// 1995 render graph gave every one a DCS node -- that node is what
|
||||||
|
// the subsystem-visual walk parents to (the spot cone hangs on the
|
||||||
|
// searchlight site, @004cef28 case 0xbd8). Build the geometry-less
|
||||||
|
// child so site slots are posed + parentable; nothing draws for the
|
||||||
|
// site itself.
|
||||||
|
//
|
||||||
|
{
|
||||||
|
dpl_ISECT_MODE site_isect;
|
||||||
|
HierarchicalDrawComponent *site_child = new DPLStaticChildRenderable(
|
||||||
|
entity, inDeathZone, NULL /* no geometry */,
|
||||||
|
site_isect, 0, offset_matrix, parent_DCS);
|
||||||
|
dcs_array[segment->GetIndex()] = site_child;
|
||||||
|
render_tree.segRenderable[segment->GetIndex()] = site_child;
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -563,6 +683,14 @@ HierarchicalDrawComponent*
|
|||||||
// the JointSubsystem's joint table.
|
// the JointSubsystem's joint table.
|
||||||
//
|
//
|
||||||
int segment_slot = segment->GetIndex() /* [0xcc] */;
|
int segment_slot = segment->GetIndex() /* [0xcc] */;
|
||||||
|
// DIAG (BT_TREE_LOG): the built joint topology -- segment slot, name,
|
||||||
|
// joint index/type, parent -- the crouch root-drop chain hunt.
|
||||||
|
if (getenv("BT_TREE_LOG"))
|
||||||
|
DEBUG_STREAM << "[tree] seg=" << segment_slot
|
||||||
|
<< " name=" << (segment->GetName() ? (const char *)segment->GetName() : "?")
|
||||||
|
<< " joint=" << segment->GetJointIndex()
|
||||||
|
<< " parent=" << (segment->GetParent() ? segment->GetParentIndex() : -1)
|
||||||
|
<< "\n" << std::flush;
|
||||||
Joint *this_joint = NULL;
|
Joint *this_joint = NULL;
|
||||||
Joint::JointType joint_type;
|
Joint::JointType joint_type;
|
||||||
if (segment->GetJointIndex() /* [0xc0] */ == -1)
|
if (segment->GetJointIndex() /* [0xc0] */ == -1)
|
||||||
@@ -884,7 +1012,8 @@ void
|
|||||||
if (swapped != 0 || getenv("BT_DEATH_LOG"))
|
if (swapped != 0 || getenv("BT_DEATH_LOG"))
|
||||||
DEBUG_STREAM << "[BTrender] RemakeEntity: " << swapped
|
DEBUG_STREAM << "[BTrender] RemakeEntity: " << swapped
|
||||||
<< " mesh(es) swapped (" << mapped << " body segs mapped of "
|
<< " mesh(es) swapped (" << mapped << " body segs mapped of "
|
||||||
<< checked << " checked)\n" << std::flush;
|
<< checked << " checked) entity="
|
||||||
|
<< entity->GetEntityID() << "\n" << std::flush;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1171,13 +1300,15 @@ void
|
|||||||
if (hulk != NULL && hulk->GetVertCount() == 0)
|
if (hulk != NULL && hulk->GetVertCount() == 0)
|
||||||
{
|
{
|
||||||
DEBUG_STREAM << "[BTrender] wreck: '" << hulk_name
|
DEBUG_STREAM << "[BTrender] wreck: '" << hulk_name
|
||||||
<< "' is an EMPTY placeholder -> gendbr.bgf fallback\n" << std::flush;
|
<< "' is an EMPTY placeholder -> gendbr.bgf fallback (entity="
|
||||||
|
<< victim->GetEntityID() << ")\n" << std::flush;
|
||||||
hulk = NULL;
|
hulk = NULL;
|
||||||
}
|
}
|
||||||
if (hulk == NULL)
|
if (hulk == NULL)
|
||||||
{
|
{
|
||||||
DEBUG_STREAM << "[BTrender] wreck: '" << hulk_name
|
DEBUG_STREAM << "[BTrender] wreck: '" << hulk_name
|
||||||
<< "' missing -> gendbr.bgf fallback\n" << std::flush;
|
<< "' missing -> gendbr.bgf fallback (entity="
|
||||||
|
<< victim->GetEntityID() << ")\n" << std::flush;
|
||||||
hulk = d3d_OBJECT::LoadObject(GetDevice(), "gendbr.bgf");
|
hulk = d3d_OBJECT::LoadObject(GetDevice(), "gendbr.bgf");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1451,9 +1582,36 @@ void
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (log)
|
if (log)
|
||||||
|
{
|
||||||
DEBUG_STREAM << "[armor] entity " << (void *)entity << " skel="
|
DEBUG_STREAM << "[armor] entity " << (void *)entity << " skel="
|
||||||
<< tree.skeletonType << ": " << tree.dmgBinds.size()
|
<< tree.skeletonType << ": " << tree.dmgBinds.size()
|
||||||
<< " material binding(s)\n" << std::flush;
|
<< " material binding(s)\n" << std::flush;
|
||||||
|
// ORPHAN inventory: drawn ops NO zone claimed. These panels never
|
||||||
|
// darken -- if the big hull materials land here (a .DZM name the mesh
|
||||||
|
// doesn't carry, or per-zone material instancing collapsed at load),
|
||||||
|
// the mech reads "no darkening" live even while every bound op logs
|
||||||
|
// perfectly (the #87 visibility gap).
|
||||||
|
for (std::map<int, MechRenderTree::SegPick>::iterator si = tree.segPick.begin();
|
||||||
|
si != tree.segPick.end(); ++si)
|
||||||
|
{
|
||||||
|
d3d_OBJECT *obj = si->second.obj;
|
||||||
|
if (obj == NULL)
|
||||||
|
continue;
|
||||||
|
for (int op = 0; op < obj->GetDrawOpCount(); ++op)
|
||||||
|
{
|
||||||
|
L4DRAWOP *draw_op = obj->GetDrawOp(op);
|
||||||
|
if (draw_op->dzMatName[0] == '\0')
|
||||||
|
continue;
|
||||||
|
int claimed = 0;
|
||||||
|
for (size_t b = 0; b < tree.dmgBinds.size() && !claimed; ++b)
|
||||||
|
if (tree.dmgBinds[b].obj == obj && tree.dmgBinds[b].op == op)
|
||||||
|
claimed = 1;
|
||||||
|
if (!claimed)
|
||||||
|
DEBUG_STREAM << "[armor] ORPHAN op seg=" << si->first
|
||||||
|
<< " '" << draw_op->dzMatName << "'\n" << std::flush;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1514,6 +1672,63 @@ void
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
//#############################################################################
|
||||||
|
// TickSearchlight -- the two 1995 searchlight watchers, transcribed
|
||||||
|
//#############################################################################
|
||||||
|
//
|
||||||
|
// External cone (@0045612c, Execute @004561d8): poll the subsystem's LightOn
|
||||||
|
// attribute; on change, set the spot instance visible iff the value matches
|
||||||
|
// (match value 1) and flush. Our equivalent of SetInstanceOn/Flush is the
|
||||||
|
// SetDrawObj object swap (the wreck-reveal mechanism).
|
||||||
|
//
|
||||||
|
// Cockpit fog (@00456778, Execute @00456814): poll BOTH stashed attrs; when
|
||||||
|
// either changed, either-on -> SetFogStyle(searchLightOnFogStyle) else
|
||||||
|
// SetFogStyle(searchLightOffFogStyle). The ctor's INVERTED caches make the
|
||||||
|
// first poll always fire, normalizing a night start onto nosearchlightfog --
|
||||||
|
// the authentic dark (fog= is the LIT set; see BTDPL.INI per-page authoring).
|
||||||
|
//
|
||||||
|
void
|
||||||
|
BTL4VideoRenderer::TickSearchlight(Entity *mech)
|
||||||
|
{
|
||||||
|
std::map<Entity*, MechRenderTree>::iterator ti = mMechRenderTrees.find(mech);
|
||||||
|
if (ti == mMechRenderTrees.end() || ti->second.searchLightCount <= 0)
|
||||||
|
return;
|
||||||
|
MechRenderTree &tree = ti->second;
|
||||||
|
|
||||||
|
int any_on = 0, changed = 0;
|
||||||
|
for (int sl = 0; sl < tree.searchLightCount; ++sl)
|
||||||
|
{
|
||||||
|
MechRenderTree::SearchLight &light = tree.searchLight[sl];
|
||||||
|
if (light.lightOn == NULL)
|
||||||
|
continue;
|
||||||
|
const int cur = (*light.lightOn != 0);
|
||||||
|
|
||||||
|
if (light.cone != NULL && cur != light.shown) // @004561d8
|
||||||
|
{
|
||||||
|
light.cone->SetDrawObj(cur ? light.coneObj : NULL);
|
||||||
|
light.shown = cur;
|
||||||
|
if (getenv("BT_FIRE_LOG") || getenv("BT_FOG_LOG"))
|
||||||
|
DEBUG_STREAM << "[spot] cone " << (cur ? "SHOWN" : "HIDDEN")
|
||||||
|
<< " (seg " << light.mountSeg << ")\n" << std::flush;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cur != tree.searchFogCache[sl]) // @00456814
|
||||||
|
changed = 1;
|
||||||
|
tree.searchFogCache[sl] = cur;
|
||||||
|
any_on |= cur;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tree.searchIsCockpit && changed)
|
||||||
|
{
|
||||||
|
SetFogStyle(any_on ? searchLightOnFogStyle : searchLightOffFogStyle);
|
||||||
|
if (getenv("BT_FIRE_LOG") || getenv("BT_FOG_LOG"))
|
||||||
|
DEBUG_STREAM << "[spot] cockpit fog -> "
|
||||||
|
<< (any_on ? "searchLightOn" : "searchLightOff") << "\n" << std::flush;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Sim-side bridge (per-mech, every frame, from Mech::PerformAndWatch).
|
// Sim-side bridge (per-mech, every frame, from Mech::PerformAndWatch).
|
||||||
//
|
//
|
||||||
@@ -1526,6 +1741,7 @@ void BTArmourDamageTick(Entity *mech)
|
|||||||
if (renderer == NULL)
|
if (renderer == NULL)
|
||||||
return;
|
return;
|
||||||
renderer->TickArmourDamage(mech);
|
renderer->TickArmourDamage(mech);
|
||||||
|
renderer->TickSearchlight(mech); // the searchlight watchers ride the same beat
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1545,10 +1761,106 @@ int BTWreckSinkTick(Entity *victim, float dt)
|
|||||||
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// #73 -- the aimed PER-PART pick (see the header note). Ray-vs-sphere over
|
// #124 -- the pick's TRIANGLE cache. The 1995 pick was a dpl scene
|
||||||
// the per-segment draw objects recorded at tree build; world centers come
|
// intersection against the DRAWN GEOMETRY (the division card cast from the
|
||||||
// through the draw-cached mLocalToWorld (updated every drawn frame -- the
|
// view); the port's sphere approximation measurably mis-picked (the zone-walk
|
||||||
// target being aimed at is on screen, so at most one frame stale).
|
// matrix: aim dead-on dtorso -> picked rgun/ruleg -- gun/limb spheres thread
|
||||||
|
// the ray before the torso from most angles, and its own comments admitted "a
|
||||||
|
// foot can be unhittable behind its own knee"). Restore the authentic
|
||||||
|
// semantic: nearest RAY-TRIANGLE hit across the candidate segments' posed
|
||||||
|
// meshes. Positions are read ONCE per d3d_OBJECT from its own BGF buffers
|
||||||
|
// (managed pool, lockable) and cached CPU-side; the per-frame cost is a
|
||||||
|
// sphere pre-filter + Moller-Trumbore over the few threaded segments.
|
||||||
|
//
|
||||||
|
struct BTPickMesh
|
||||||
|
{
|
||||||
|
std::vector<float> pos; // xyz per vertex
|
||||||
|
std::vector<unsigned int> idx; // triangle list
|
||||||
|
int ok;
|
||||||
|
};
|
||||||
|
static std::map<d3d_OBJECT *, BTPickMesh> gBTPickMeshes;
|
||||||
|
|
||||||
|
static BTPickMesh *
|
||||||
|
BTGetPickMesh(d3d_OBJECT *obj)
|
||||||
|
{
|
||||||
|
std::map<d3d_OBJECT *, BTPickMesh>::iterator mi = gBTPickMeshes.find(obj);
|
||||||
|
if (mi != gBTPickMeshes.end())
|
||||||
|
return mi->second.ok ? &mi->second : 0;
|
||||||
|
|
||||||
|
BTPickMesh &pm = gBTPickMeshes[obj];
|
||||||
|
pm.ok = 0;
|
||||||
|
if (obj->mBgfVB == 0 || obj->mBgfIB == 0 || obj->mBgfStride < 12)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
D3DINDEXBUFFER_DESC ibd;
|
||||||
|
if (FAILED(obj->mBgfIB->GetDesc(&ibd)))
|
||||||
|
return 0;
|
||||||
|
int idx32 = (ibd.Format == D3DFMT_INDEX32);
|
||||||
|
unsigned int nIdx = ibd.Size / (idx32 ? 4 : 2);
|
||||||
|
|
||||||
|
void *vp = 0, *ip = 0;
|
||||||
|
if (FAILED(obj->mBgfVB->Lock(0, 0, &vp, D3DLOCK_READONLY)))
|
||||||
|
return 0;
|
||||||
|
if (FAILED(obj->mBgfIB->Lock(0, 0, &ip, D3DLOCK_READONLY)))
|
||||||
|
{
|
||||||
|
obj->mBgfVB->Unlock();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
pm.pos.resize((size_t)obj->mBgfNumVerts * 3);
|
||||||
|
const unsigned char *vb = (const unsigned char *)vp;
|
||||||
|
for (UINT v = 0; v < obj->mBgfNumVerts; ++v)
|
||||||
|
{
|
||||||
|
const float *p = (const float *)(vb + (size_t)v * obj->mBgfStride);
|
||||||
|
pm.pos[v*3+0] = p[0]; // position-first vertex layout (the BGF
|
||||||
|
pm.pos[v*3+1] = p[1]; // loader's own decl; the cull sphere was
|
||||||
|
pm.pos[v*3+2] = p[2]; // computed from these same floats at load)
|
||||||
|
}
|
||||||
|
pm.idx.resize(nIdx);
|
||||||
|
if (idx32)
|
||||||
|
{
|
||||||
|
const unsigned int *s = (const unsigned int *)ip;
|
||||||
|
for (unsigned int k = 0; k < nIdx; ++k) pm.idx[k] = s[k];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const unsigned short *s = (const unsigned short *)ip;
|
||||||
|
for (unsigned int k = 0; k < nIdx; ++k) pm.idx[k] = s[k];
|
||||||
|
}
|
||||||
|
obj->mBgfIB->Unlock();
|
||||||
|
obj->mBgfVB->Unlock();
|
||||||
|
pm.ok = (pm.idx.size() >= 3 && pm.pos.size() >= 9);
|
||||||
|
return pm.ok ? &pm : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Moller-Trumbore, both-sided (the pod's dpl geometry has no consistent
|
||||||
|
// winding guarantee across ported BGF pieces). Returns t >= 0 or -1.
|
||||||
|
static float
|
||||||
|
BTRayTri(const float o[3], const float d[3],
|
||||||
|
const float *a, const float *b, const float *c)
|
||||||
|
{
|
||||||
|
float e1[3] = { b[0]-a[0], b[1]-a[1], b[2]-a[2] };
|
||||||
|
float e2[3] = { c[0]-a[0], c[1]-a[1], c[2]-a[2] };
|
||||||
|
float pv[3] = { d[1]*e2[2]-d[2]*e2[1], d[2]*e2[0]-d[0]*e2[2], d[0]*e2[1]-d[1]*e2[0] };
|
||||||
|
float det = e1[0]*pv[0] + e1[1]*pv[1] + e1[2]*pv[2];
|
||||||
|
if (det > -1e-8f && det < 1e-8f) return -1.0f;
|
||||||
|
float inv = 1.0f / det;
|
||||||
|
float tv[3] = { o[0]-a[0], o[1]-a[1], o[2]-a[2] };
|
||||||
|
float u = (tv[0]*pv[0] + tv[1]*pv[1] + tv[2]*pv[2]) * inv;
|
||||||
|
if (u < 0.0f || u > 1.0f) return -1.0f;
|
||||||
|
float qv[3] = { tv[1]*e1[2]-tv[2]*e1[1], tv[2]*e1[0]-tv[0]*e1[2], tv[0]*e1[1]-tv[1]*e1[0] };
|
||||||
|
float v = (d[0]*qv[0] + d[1]*qv[1] + d[2]*qv[2]) * inv;
|
||||||
|
if (v < 0.0f || u + v > 1.0f) return -1.0f;
|
||||||
|
float t = (e2[0]*qv[0] + e2[1]*qv[1] + e2[2]*qv[2]) * inv;
|
||||||
|
return (t >= 0.0f) ? t : -1.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// #73 -- the aimed PER-PART pick (see the header note). #124: now a true
|
||||||
|
// DRAWN-GEOMETRY intersection -- sphere pre-filter, then nearest ray-triangle
|
||||||
|
// hit across the threaded segments' posed meshes (the 1995 division-card
|
||||||
|
// semantic). The old smallest-sphere selection survives only as the fallback
|
||||||
|
// when no triangle anywhere is struck (grazing edge shots). World transforms
|
||||||
|
// come through the draw-cached mLocalToWorld (at most one frame stale).
|
||||||
//
|
//
|
||||||
int
|
int
|
||||||
BTL4VideoRenderer::MechSegmentPick(
|
BTL4VideoRenderer::MechSegmentPick(
|
||||||
@@ -1561,7 +1873,8 @@ int
|
|||||||
{
|
{
|
||||||
std::map<Entity*, MechRenderTree>::iterator it = mMechRenderTrees.find(mech);
|
std::map<Entity*, MechRenderTree>::iterator it = mMechRenderTrees.find(mech);
|
||||||
if (it == mMechRenderTrees.end() || it->second.wrecked)
|
if (it == mMechRenderTrees.end() || it->second.wrecked)
|
||||||
return 0;
|
return -1; // #131: NO TREE (unbuilt replicant / wreck) -- the caller may
|
||||||
|
// box-test; distinct from 0 = a true drawn-geometry MISS.
|
||||||
|
|
||||||
// Selection is SPECIFICITY-FIRST: among the spheres the ray pierces, the
|
// Selection is SPECIFICITY-FIRST: among the spheres the ray pierces, the
|
||||||
// SMALLEST radius wins (normalized-distance tie-break). Neither nearest-
|
// SMALLEST radius wins (normalized-distance tie-break). Neither nearest-
|
||||||
@@ -1573,12 +1886,35 @@ int
|
|||||||
// envelope; smallest-pierced picks the most specific part on the aim line,
|
// envelope; smallest-pierced picks the most specific part on the aim line,
|
||||||
// and the torso wins only when no limb is threaded -- which is the per-part
|
// and the torso wins only when no limb is threaded -- which is the per-part
|
||||||
// semantic the 1995 mesh intersection produced.
|
// semantic the 1995 mesh intersection produced.
|
||||||
float bestR = 1e30f; // primary key: sphere radius (ascending)
|
float bestR = 1e30f; // sphere-FALLBACK key: radius (ascending)
|
||||||
float bestScore = 1.0f; // tie-break: normalized perpendicular d2/r2
|
float bestScore = 1.0f; // sphere tie-break: normalized d2/r2
|
||||||
float bestT = max_range;
|
float bestT = max_range;
|
||||||
int bestZone = -1;
|
int bestZone = -1;
|
||||||
int hitAny = 0;
|
int hitAny = 0;
|
||||||
|
|
||||||
|
// triangle-accurate primary: nearest surface hit across all segments
|
||||||
|
float triBestT = max_range;
|
||||||
|
int triBestZone = -1;
|
||||||
|
int triBestSeg = -1;
|
||||||
|
int triHit = 0;
|
||||||
|
d3d_OBJECT *triBestObj = 0; // #124-PATCH: winning object + tri
|
||||||
|
size_t triBestK = 0; // (triangle ordinal in its IB)
|
||||||
|
int dbgTriObjs = 0, dbgInvFail = 0, dbgNoTri = 0; // #131 telemetry
|
||||||
|
|
||||||
|
// #124 CORRECTED (2026-08-04, field pushback vindicated): the pick's zone
|
||||||
|
// is the struck PATCH's authored dz_* tag, with the segment's SKL dzone as
|
||||||
|
// the fallback for untagged patches. The earlier "struck segment's dzone,
|
||||||
|
// ALWAYS" model collapsed the hull to dtorso -- but the art zone-tags the
|
||||||
|
// hull PER PANEL (MAD_TOR carries dz_utorso/ltorso/rtorso/dtorso + all four
|
||||||
|
// rear panels + searchlight across its patches), the dpl hit result kept
|
||||||
|
// GEOGROUP granularity (dplHitInstance/DCS/GeoGroup/Geometry), and the
|
||||||
|
// binary's segment->zone map (@49db20) has NO runtime caller (sole caller =
|
||||||
|
// CreateStreamedDamageZone, load-time) -- there was nothing to do the
|
||||||
|
// segment-level collapse with. Era players' "I stripped the left torso in
|
||||||
|
// the pod" stands. The triangle -> patch attribution below reuses the .DZM
|
||||||
|
// zone->material bindings (#87's armour darkening -- the same authored
|
||||||
|
// mapping that already paints those panels per-zone).
|
||||||
|
|
||||||
std::map<int, MechRenderTree::SegPick>::iterator sp;
|
std::map<int, MechRenderTree::SegPick>::iterator sp;
|
||||||
for (sp = it->second.segPick.begin(); sp != it->second.segPick.end(); ++sp)
|
for (sp = it->second.segPick.begin(); sp != it->second.segPick.end(); ++sp)
|
||||||
{
|
{
|
||||||
@@ -1609,6 +1945,77 @@ int
|
|||||||
if (t < 0.0f || t >= max_range)
|
if (t < 0.0f || t >= max_range)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
// ---- TRIANGLE TEST (#124): the sphere only nominates ----
|
||||||
|
BTPickMesh *pm = BTGetPickMesh(obj);
|
||||||
|
if (pm == 0 && getenv("BT_PICK_LOG"))
|
||||||
|
{
|
||||||
|
static int s_nt = 0;
|
||||||
|
if (s_nt++ < 40)
|
||||||
|
DEBUG_STREAM << "[picktri] seg=" << sp->first << " zone="
|
||||||
|
<< sp->second.zone << " NO TRIS (vb=" << (void *)obj->mBgfVB
|
||||||
|
<< " ib=" << (void *)obj->mBgfIB
|
||||||
|
<< " stride=" << obj->mBgfStride << ")\n" << std::flush;
|
||||||
|
}
|
||||||
|
if (pm == 0)
|
||||||
|
++dbgNoTri;
|
||||||
|
if (pm != 0)
|
||||||
|
{
|
||||||
|
++dbgTriObjs;
|
||||||
|
// ray into object-local space (affine inverse; segment poses are
|
||||||
|
// rigid, so local t == world t after direction normalization is
|
||||||
|
// preserved by construction below)
|
||||||
|
D3DXMATRIX w2l;
|
||||||
|
if (D3DXMatrixInverse(&w2l, 0, &l2w) == 0)
|
||||||
|
++dbgInvFail;
|
||||||
|
if (D3DXMatrixInverse(&w2l, 0, &l2w) != 0)
|
||||||
|
{
|
||||||
|
D3DXVECTOR3 lo, ld;
|
||||||
|
D3DXVECTOR3 wo(ray_start[0], ray_start[1], ray_start[2]);
|
||||||
|
D3DXVECTOR3 wd(ray_dir[0], ray_dir[1], ray_dir[2]);
|
||||||
|
D3DXVec3TransformCoord(&lo, &wo, &w2l);
|
||||||
|
D3DXVec3TransformNormal(&ld, &wd, &w2l);
|
||||||
|
float o3[3] = { lo.x, lo.y, lo.z };
|
||||||
|
float d3[3] = { ld.x, ld.y, ld.z };
|
||||||
|
const float *P = &pm->pos[0];
|
||||||
|
size_t nv = pm->pos.size() / 3;
|
||||||
|
for (size_t k = 0; k + 2 < pm->idx.size(); k += 3)
|
||||||
|
{
|
||||||
|
unsigned int i0 = pm->idx[k], i1 = pm->idx[k+1], i2 = pm->idx[k+2];
|
||||||
|
if (i0 >= nv || i1 >= nv || i2 >= nv)
|
||||||
|
continue;
|
||||||
|
float tt = BTRayTri(o3, d3, P + i0*3, P + i1*3, P + i2*3);
|
||||||
|
if (tt >= 0.0f && tt < triBestT)
|
||||||
|
{
|
||||||
|
// world-space t of the local hit (handles any scale)
|
||||||
|
D3DXVECTOR3 lh(o3[0]+d3[0]*tt, o3[1]+d3[1]*tt, o3[2]+d3[2]*tt);
|
||||||
|
D3DXVECTOR3 wh;
|
||||||
|
D3DXVec3TransformCoord(&wh, &lh, &l2w);
|
||||||
|
float wt = (wh.x - ray_start[0]) * ray_dir[0]
|
||||||
|
+ (wh.y - ray_start[1]) * ray_dir[1]
|
||||||
|
+ (wh.z - ray_start[2]) * ray_dir[2];
|
||||||
|
if (wt >= 0.0f && wt < triBestT)
|
||||||
|
{
|
||||||
|
triBestT = wt;
|
||||||
|
triBestZone = sp->second.zone; // segment dzone (fallback)
|
||||||
|
triBestSeg = sp->first;
|
||||||
|
triBestObj = obj;
|
||||||
|
triBestK = k; // first index of the tri
|
||||||
|
triHit = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- sphere FALLBACK bookkeeping -- #131 MISS-MEANS-MISS: only an
|
||||||
|
// object whose drawn mesh is UNREADABLE (pm == 0) may answer by
|
||||||
|
// sphere. A readable mesh the ray misses is a MISS. The old
|
||||||
|
// any-object sphere halo produced lock-without-mech: level rays over
|
||||||
|
// the squat blackhawk read tri=0 / sphereFB=168 on the sweep bench,
|
||||||
|
// and the operator watched the reticle pass over its head with the
|
||||||
|
// ring lit. The 1995 card cast against the DRAWN geometry -- no halo.
|
||||||
|
if (pm != 0)
|
||||||
|
continue;
|
||||||
float score = d2 / r2; // 0 = dead-center thread
|
float score = d2 / r2; // 0 = dead-center thread
|
||||||
if (r > bestR
|
if (r > bestR
|
||||||
|| (r == bestR && score >= bestScore))
|
|| (r == bestR && score >= bestScore))
|
||||||
@@ -1621,6 +2028,58 @@ int
|
|||||||
hitAny = 1;
|
hitAny = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #131 telemetry: how did this pick answer? (TRI = drawn-geometry hit,
|
||||||
|
// the authentic semantic; SPHERE = the fallback answered with no triangle
|
||||||
|
// struck -- the false-lock candidate.) Throttled under BT_PICK_LOG.
|
||||||
|
// objs/inv/noTri localize a dead triangle pass: objs = sphere-nominated
|
||||||
|
// objects entering the tri test, inv = SILENT skips from a failed
|
||||||
|
// world-matrix inverse (degenerate l2w), noTri = pick-mesh read failures.
|
||||||
|
if (getenv("BT_PICK_LOG"))
|
||||||
|
{
|
||||||
|
static int s_src[3] = {0,0,0}; // [0]=tri [1]=sphere [2]=miss
|
||||||
|
static int s_objs = 0, s_inv = 0, s_noTri = 0;
|
||||||
|
static int s_rep = 0;
|
||||||
|
s_objs += dbgTriObjs; s_inv += dbgInvFail; s_noTri += dbgNoTri;
|
||||||
|
++s_src[triHit ? 0 : (hitAny ? 1 : 2)];
|
||||||
|
if ((++s_rep % 240) == 0)
|
||||||
|
DEBUG_STREAM << "[picksrc] tri=" << s_src[0]
|
||||||
|
<< " sphereFB=" << s_src[1] << " miss=" << s_src[2]
|
||||||
|
<< " objs=" << s_objs << " invFail=" << s_inv
|
||||||
|
<< " noTri=" << s_noTri
|
||||||
|
<< "\n" << std::flush;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (triHit)
|
||||||
|
{
|
||||||
|
bestT = triBestT;
|
||||||
|
bestZone = triBestZone; // segment dzone unless a patch tags it
|
||||||
|
hitAny = 1;
|
||||||
|
// #124-PATCH: attribute the struck triangle to its DRAW OP (each op is
|
||||||
|
// an index RANGE in the same IB the pick mesh copied) and take the
|
||||||
|
// op's .DZM-bound zone -- the authored dz_* panel. dmgBinds is the
|
||||||
|
// #87 armour mapping: {obj, op, zone} for every material a zone paints.
|
||||||
|
if (triBestObj != 0)
|
||||||
|
{
|
||||||
|
const std::vector<MechRenderTree::DmgBind> &binds = it->second.dmgBinds;
|
||||||
|
for (size_t b = 0; b < binds.size(); ++b)
|
||||||
|
{
|
||||||
|
if (binds[b].obj != triBestObj)
|
||||||
|
continue;
|
||||||
|
L4DRAWOP *op = triBestObj->GetDrawOp(binds[b].op);
|
||||||
|
if (op == 0 || op->bgfPrimCount <= 0)
|
||||||
|
continue;
|
||||||
|
size_t first = (size_t)op->bgfStartIndex;
|
||||||
|
size_t count = (size_t)op->bgfPrimCount * 3;
|
||||||
|
if (triBestK >= first && triBestK < first + count)
|
||||||
|
{
|
||||||
|
bestZone = binds[b].zone; // the struck PANEL's zone
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(void)triBestSeg;
|
||||||
|
}
|
||||||
|
|
||||||
// #92 probe: which spheres did the ray actually THREAD, and which won?
|
// #92 probe: which spheres did the ray actually THREAD, and which won?
|
||||||
// "smallest radius wins" means a big sphere can never beat a small one that
|
// "smallest radius wins" means a big sphere can never beat a small one that
|
||||||
// the ray also grazes -- so a foot can be unhittable behind its own knee.
|
// the ray also grazes -- so a foot can be unhittable behind its own knee.
|
||||||
@@ -1681,7 +2140,9 @@ int
|
|||||||
static int s_pl = 0;
|
static int s_pl = 0;
|
||||||
if ((s_pl++ % 60) == 0)
|
if ((s_pl++ % 60) == 0)
|
||||||
DEBUG_STREAM << "[pickwin] zone=" << bestZone
|
DEBUG_STREAM << "[pickwin] zone=" << bestZone
|
||||||
<< " score=" << bestScore << " t=" << bestT << "\n" << std::flush;
|
<< " score=" << bestScore << " t=" << bestT
|
||||||
|
<< " tri=" << triHit << " triSeg=" << triBestSeg
|
||||||
|
<< " triZone=" << triBestZone << "\n" << std::flush;
|
||||||
}
|
}
|
||||||
|
|
||||||
hit_out[0] = ray_start[0] + bestT * ray_dir[0];
|
hit_out[0] = ray_start[0] + bestT * ray_dir[0];
|
||||||
@@ -1692,11 +2153,102 @@ int
|
|||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Game-side bridge (mech4.cpp's per-frame target pick; same access pattern as
|
// #124 zone walker: the zone's visual aim anchor (see the hpp note).
|
||||||
// the wreck swap below).
|
|
||||||
//
|
//
|
||||||
int BTMechSegmentPick(void *mech, const float ray_start[3], const float ray_dir[3],
|
int
|
||||||
float max_range, float hit_out[3], int *zone_out)
|
BTL4VideoRenderer::ZoneAimPoint(Entity *mech, int zone, float out3[3])
|
||||||
|
{
|
||||||
|
std::map<Entity*, MechRenderTree>::iterator it = mMechRenderTrees.find(mech);
|
||||||
|
if (it == mMechRenderTrees.end() || it->second.wrecked)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
// #124-PATCH: a zone with .DZM-bound panel patches aims at the PATCH
|
||||||
|
// CENTROID (the authored dz_* panel -- upper/left/right/rear torso are
|
||||||
|
// distinct patch clusters on ONE torso object, so the old cull-center
|
||||||
|
// answer aimed every hull zone at the same chest point). Centroid =
|
||||||
|
// average of the bound ops' triangle vertices, world-transformed.
|
||||||
|
{
|
||||||
|
double acc[3] = { 0, 0, 0 };
|
||||||
|
long nAcc = 0;
|
||||||
|
const std::vector<MechRenderTree::DmgBind> &binds = it->second.dmgBinds;
|
||||||
|
for (size_t b = 0; b < binds.size(); ++b)
|
||||||
|
{
|
||||||
|
if (binds[b].zone != zone || binds[b].obj == NULL)
|
||||||
|
continue;
|
||||||
|
d3d_OBJECT *obj = binds[b].obj;
|
||||||
|
L4DRAWOP *op = obj->GetDrawOp(binds[b].op);
|
||||||
|
if (op == 0 || op->bgfPrimCount <= 0)
|
||||||
|
continue;
|
||||||
|
BTPickMesh *pm = BTGetPickMesh(obj);
|
||||||
|
if (pm == 0)
|
||||||
|
continue;
|
||||||
|
D3DXMATRIX l2w = obj->GetLocalToWorld();
|
||||||
|
size_t first = (size_t)op->bgfStartIndex;
|
||||||
|
size_t last = first + (size_t)op->bgfPrimCount * 3;
|
||||||
|
if (last > pm->idx.size())
|
||||||
|
continue;
|
||||||
|
for (size_t k = first; k < last; ++k)
|
||||||
|
{
|
||||||
|
unsigned int vi = pm->idx[k];
|
||||||
|
if ((size_t)vi * 3 + 2 >= pm->pos.size())
|
||||||
|
continue;
|
||||||
|
D3DXVECTOR3 lp(pm->pos[vi*3], pm->pos[vi*3+1], pm->pos[vi*3+2]);
|
||||||
|
D3DXVECTOR3 wp;
|
||||||
|
D3DXVec3TransformCoord(&wp, &lp, &l2w);
|
||||||
|
acc[0] += wp.x; acc[1] += wp.y; acc[2] += wp.z;
|
||||||
|
++nAcc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nAcc > 0)
|
||||||
|
{
|
||||||
|
out3[0] = (float)(acc[0] / nAcc);
|
||||||
|
out3[1] = (float)(acc[1] / nAcc);
|
||||||
|
out3[2] = (float)(acc[2] / nAcc);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
d3d_OBJECT *best = 0;
|
||||||
|
std::map<int, MechRenderTree::SegPick>::iterator sp;
|
||||||
|
for (sp = it->second.segPick.begin(); sp != it->second.segPick.end(); ++sp)
|
||||||
|
{
|
||||||
|
if (sp->second.zone != zone || sp->second.obj == NULL)
|
||||||
|
continue;
|
||||||
|
if (best == 0 || sp->second.obj->mCullRadius > best->mCullRadius)
|
||||||
|
best = sp->second.obj;
|
||||||
|
}
|
||||||
|
if (best == 0 || best->mCullRadius <= 0.0f)
|
||||||
|
return 0;
|
||||||
|
D3DXMATRIX l2w = best->GetLocalToWorld();
|
||||||
|
D3DXVECTOR3 cw;
|
||||||
|
D3DXVec3TransformCoord(&cw, &best->mCullCenter, &l2w);
|
||||||
|
out3[0] = cw.x; out3[1] = cw.y; out3[2] = cw.z;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The carrier-SEGMENT variant: anchor at segPick[seg]'s cull center (the
|
||||||
|
// piece's visual middle). For hull-family zones no object claims the zone,
|
||||||
|
// and the zone table's carrier is the hull joint whose ORIGIN sits in the
|
||||||
|
// crotch gap -- rays aimed there sail between the legs to the terrain
|
||||||
|
// sentinel. The cull center is the chest.
|
||||||
|
int
|
||||||
|
BTL4VideoRenderer::SegAimPoint(Entity *mech, int seg, float out3[3])
|
||||||
|
{
|
||||||
|
std::map<Entity*, MechRenderTree>::iterator it = mMechRenderTrees.find(mech);
|
||||||
|
if (it == mMechRenderTrees.end() || it->second.wrecked)
|
||||||
|
return 0;
|
||||||
|
std::map<int, MechRenderTree::SegPick>::iterator sp = it->second.segPick.find(seg);
|
||||||
|
if (sp == it->second.segPick.end() || sp->second.obj == NULL
|
||||||
|
|| sp->second.obj->mCullRadius <= 0.0f)
|
||||||
|
return 0;
|
||||||
|
D3DXMATRIX l2w = sp->second.obj->GetLocalToWorld();
|
||||||
|
D3DXVECTOR3 cw;
|
||||||
|
D3DXVec3TransformCoord(&cw, &sp->second.obj->mCullCenter, &l2w);
|
||||||
|
out3[0] = cw.x; out3[1] = cw.y; out3[2] = cw.z;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int BTMechSegAimPoint(void *mech, int seg, float out3[3])
|
||||||
{
|
{
|
||||||
if (mech == NULL || application == NULL)
|
if (mech == NULL || application == NULL)
|
||||||
return 0;
|
return 0;
|
||||||
@@ -1704,6 +2256,35 @@ int BTMechSegmentPick(void *mech, const float ray_start[3], const float ray_dir[
|
|||||||
(BTL4VideoRenderer *)application->GetVideoRenderer();
|
(BTL4VideoRenderer *)application->GetVideoRenderer();
|
||||||
if (renderer == NULL)
|
if (renderer == NULL)
|
||||||
return 0;
|
return 0;
|
||||||
|
return renderer->SegAimPoint((Entity *)mech, seg, out3);
|
||||||
|
}
|
||||||
|
|
||||||
|
int BTMechZoneAimPoint(void *mech, int zone, float out3[3])
|
||||||
|
{
|
||||||
|
if (mech == NULL || application == NULL)
|
||||||
|
return 0;
|
||||||
|
BTL4VideoRenderer *renderer =
|
||||||
|
(BTL4VideoRenderer *)application->GetVideoRenderer();
|
||||||
|
if (renderer == NULL)
|
||||||
|
return 0;
|
||||||
|
return renderer->ZoneAimPoint((Entity *)mech, zone, out3);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Game-side bridge (mech4.cpp's per-frame target pick; same access pattern as
|
||||||
|
// the wreck swap below).
|
||||||
|
//
|
||||||
|
int BTMechSegmentPick(void *mech, const float ray_start[3], const float ray_dir[3],
|
||||||
|
float max_range, float hit_out[3], int *zone_out)
|
||||||
|
{
|
||||||
|
// #131 contract: 1 = drawn-geometry hit, 0 = TRUE MISS (mesh tested),
|
||||||
|
// -1 = no renderer/tree (caller may box-test as pre-tree grace).
|
||||||
|
if (mech == NULL || application == NULL)
|
||||||
|
return -1;
|
||||||
|
BTL4VideoRenderer *renderer =
|
||||||
|
(BTL4VideoRenderer *)application->GetVideoRenderer();
|
||||||
|
if (renderer == NULL)
|
||||||
|
return -1;
|
||||||
return renderer->MechSegmentPick((Entity *)mech, ray_start, ray_dir,
|
return renderer->MechSegmentPick((Entity *)mech, ray_start, ray_dir,
|
||||||
max_range, hit_out, zone_out);
|
max_range, hit_out, zone_out);
|
||||||
}
|
}
|
||||||
@@ -2916,6 +3497,16 @@ int
|
|||||||
if (inside && nm != NULL && strstr((const char *)*nm, "_cop") != NULL
|
if (inside && nm != NULL && strstr((const char *)*nm, "_cop") != NULL
|
||||||
&& getenv("BT_HIDE_COCKPIT"))
|
&& getenv("BT_HIDE_COCKPIT"))
|
||||||
nm = NULL;
|
nm = NULL;
|
||||||
|
// #91 attribution diag: BT_HIDE_INSIDE_SEG=<substr> hides any OTHER
|
||||||
|
// inside-view (type-A) mesh whose name contains the substring -- the
|
||||||
|
// thor authors THREE type-A meshes (cop + thx_tor + thx_msl) and one
|
||||||
|
// of the extras is the reported black rectangle.
|
||||||
|
{
|
||||||
|
const char *hide = getenv("BT_HIDE_INSIDE_SEG");
|
||||||
|
if (inside && nm != NULL && hide && *hide
|
||||||
|
&& strstr((const char *)*nm, hide) != NULL)
|
||||||
|
nm = NULL;
|
||||||
|
}
|
||||||
d3d_OBJECT *obj = NULL;
|
d3d_OBJECT *obj = NULL;
|
||||||
if (nm != NULL)
|
if (nm != NULL)
|
||||||
{
|
{
|
||||||
@@ -2947,6 +3538,14 @@ int
|
|||||||
else
|
else
|
||||||
render_tree.segPick.erase(slot);
|
render_tree.segPick.erase(slot);
|
||||||
if (obj) ++shown; else ++hidden;
|
if (obj) ++shown; else ++hidden;
|
||||||
|
// Inside view: NAME what renders. The KB says the inside view is the
|
||||||
|
// _cop alone; any OTHER type-A mesh here is a black-material stowaway
|
||||||
|
// (the #91 rectangle investigation) -- keep the roster visible.
|
||||||
|
if (inside && obj != NULL)
|
||||||
|
DEBUG_STREAM << "[view] shown seg " << slot << ": "
|
||||||
|
<< (const char *)segment->GetName()
|
||||||
|
<< " mesh " << (nm ? (const char *)*nm : "?")
|
||||||
|
<< "\n" << std::flush;
|
||||||
}
|
}
|
||||||
// This reloaded every segment mesh, so every armour-damage binding is stale.
|
// This reloaded every segment mesh, so every armour-damage binding is stale.
|
||||||
BindArmourDamage(viewpoint, render_tree);
|
BindArmourDamage(viewpoint, render_tree);
|
||||||
|
|||||||
@@ -721,6 +721,24 @@ extern void BTDrawReticle(struct IDirect3DDevice9 *device);
|
|||||||
struct DmgBind { d3d_OBJECT *obj; int op; int zone; };
|
struct DmgBind { d3d_OBJECT *obj; int op; int zone; };
|
||||||
std::vector<DmgBind> dmgBinds;
|
std::vector<DmgBind> dmgBinds;
|
||||||
std::map<int, int> segGState; // slot -> last applied graphic state
|
std::map<int, int> segGState; // slot -> last applied graphic state
|
||||||
|
// SEARCHLIGHT (2026-08-05, MakeMechRenderables case 0xbd8 @004cef28):
|
||||||
|
// external view = a spot.bgf cone child on the mount joint, shown/
|
||||||
|
// hidden from the subsystem's "LightOn" attribute (the @0045612c
|
||||||
|
// watcher, transcribed into TickSearchlight); cockpit view = the fog
|
||||||
|
// swap (@00456778/@00456814 -> SetFogStyle searchLightOn/Off).
|
||||||
|
struct SearchLight
|
||||||
|
{
|
||||||
|
DPLStaticChildRenderable *cone; // external only; NULL on cockpit builds
|
||||||
|
d3d_OBJECT *coneObj; // spot.bgf (held while hidden)
|
||||||
|
int *lightOn; // the subsystem's LightOn attribute
|
||||||
|
int shown; // cone visibility last applied
|
||||||
|
int mountSeg; // resource segmentIndex (subsys +0x1DC)
|
||||||
|
};
|
||||||
|
SearchLight searchLight[2]; // the binary keeps 2 slots
|
||||||
|
int searchLightCount;
|
||||||
|
int searchIsCockpit; // built with view_type 1
|
||||||
|
int searchFogCache[2]; // seeded INVERTED (@00456778
|
||||||
|
// [9]/[10]) -> first tick syncs
|
||||||
char paintSerno; // the %serno% this mech was BUILT with
|
char paintSerno; // the %serno% this mech was BUILT with
|
||||||
// (0 = none) -- ApplyViewSkeleton re-parses
|
// (0 = none) -- ApplyViewSkeleton re-parses
|
||||||
// segment BGFs, so the paint substitutions
|
// segment BGFs, so the paint substitutions
|
||||||
@@ -740,6 +758,15 @@ extern void BTDrawReticle(struct IDirect3DDevice9 *device);
|
|||||||
int
|
int
|
||||||
TickWreck(Entity *victim, float dt);
|
TickWreck(Entity *victim, float dt);
|
||||||
|
|
||||||
|
//
|
||||||
|
// SEARCHLIGHT per-frame drive (2026-08-05): the transcription of the two
|
||||||
|
// 1995 watcher renderables -- cone visibility from "LightOn" (@0045612c
|
||||||
|
// Execute @004561d8) and the cockpit fog swap (@00456778 Execute
|
||||||
|
// @00456814). Rides the same sim bridge as TickArmourDamage.
|
||||||
|
//
|
||||||
|
void
|
||||||
|
TickSearchlight(Entity *mech);
|
||||||
|
|
||||||
//
|
//
|
||||||
// ARMOUR DARKENING (issue #87). BindArmourDamage resolves the mech's
|
// ARMOUR DARKENING (issue #87). BindArmourDamage resolves the mech's
|
||||||
// .DZM zone->material lists (DamageZone::GetMaterialList, already parsed
|
// .DZM zone->material lists (DamageZone::GetMaterialList, already parsed
|
||||||
@@ -797,6 +824,15 @@ extern void BTDrawReticle(struct IDirect3DDevice9 *device);
|
|||||||
const float ray_dir[3], float max_range,
|
const float ray_dir[3], float max_range,
|
||||||
float hit_out[3], int *zone_out);
|
float hit_out[3], int *zone_out);
|
||||||
|
|
||||||
|
// #124 zone walker: a zone's VISUAL aim point -- its largest pick
|
||||||
|
// object's cull-center in world (what a player aims at; the segment
|
||||||
|
// ORIGIN sits at the joint and makes lower segments strike the part
|
||||||
|
// above). 0 when the zone has no pick geometry.
|
||||||
|
int
|
||||||
|
ZoneAimPoint(Entity *mech, int zone, float out3[3]);
|
||||||
|
int
|
||||||
|
SegAimPoint(Entity *mech, int seg, float out3[3]);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
//
|
//
|
||||||
// Renderer-manager overrides
|
// Renderer-manager overrides
|
||||||
|
|||||||
+373
-125
@@ -203,6 +203,35 @@ static_assert(offsetof(BTPlayer::MakeMessage, roleName) == 0x90,
|
|||||||
static_assert(sizeof(BTPlayer::MakeMessage) == 0xD0,
|
static_assert(sizeof(BTPlayer::MakeMessage) == 0xD0,
|
||||||
"BTPlayer::MakeMessage wire size must be 0xD0");
|
"BTPlayer::MakeMessage wire size must be 0xD0");
|
||||||
|
|
||||||
|
// Wire-format lock (2026-08-05): the combat score reports and the extended
|
||||||
|
// VehicleDead cross nodes raw (replicant-player Dispatch reroute), and the
|
||||||
|
// sender/receiver field map was transcribed from the binary's builds
|
||||||
|
// (@0x4a04da/@0x4a05d9/@0x4a06c0 and @0x4a07d4) -- lock every offset.
|
||||||
|
static_assert(offsetof(BTPlayer::ScoreMessage, scoreAward) == 0x1c,
|
||||||
|
"ScoreMessage::scoreAward must be at wire offset 0x1c");
|
||||||
|
static_assert(offsetof(BTPlayer::ScoreMessage, scoreType) == 0x20,
|
||||||
|
"ScoreMessage::scoreType must be at wire offset 0x20");
|
||||||
|
static_assert(offsetof(BTPlayer::ScoreMessage, damageAmount) == 0x24,
|
||||||
|
"ScoreMessage::damageAmount must be at wire offset 0x24");
|
||||||
|
static_assert(offsetof(BTPlayer::ScoreMessage, vitalHit) == 0x28,
|
||||||
|
"ScoreMessage::vitalHit must be at wire offset 0x28");
|
||||||
|
static_assert(offsetof(BTPlayer::ScoreMessage, zoneIndex) == 0x2c,
|
||||||
|
"ScoreMessage::zoneIndex must be at wire offset 0x2c");
|
||||||
|
static_assert(offsetof(BTPlayer::ScoreMessage, subsysID) == 0x30,
|
||||||
|
"ScoreMessage::subsysID must be at wire offset 0x30");
|
||||||
|
static_assert(offsetof(BTPlayer::ScoreMessage, senderMechID) == 0x34,
|
||||||
|
"ScoreMessage::senderMechID must be at wire offset 0x34");
|
||||||
|
static_assert(sizeof(BTPlayer::ScoreMessage) == 0x3c,
|
||||||
|
"ScoreMessage wire size must be 0x3c (binary build @0x4a052b)");
|
||||||
|
static_assert(offsetof(BTPlayer::VehicleDeadMessage, reserved28) == 0x28,
|
||||||
|
"VehicleDeadMessage::reserved28 must be at wire offset 0x28");
|
||||||
|
static_assert(offsetof(BTPlayer::VehicleDeadMessage, killedByPlayerID) == 0x2c,
|
||||||
|
"VehicleDeadMessage::killedByPlayerID must be at wire offset 0x2c");
|
||||||
|
static_assert(offsetof(BTPlayer::VehicleDeadMessage, killZone) == 0x34,
|
||||||
|
"VehicleDeadMessage::killZone must be at wire offset 0x34");
|
||||||
|
static_assert(sizeof(BTPlayer::VehicleDeadMessage) == 0x38,
|
||||||
|
"VehicleDeadMessage wire size must be 0x38 (binary build @0x4a07f6)");
|
||||||
|
|
||||||
//
|
//
|
||||||
// LAYOUT LOCK (Gitea #48/#57). Our compiled BTPlayer is NOT the binary's -- the
|
// LAYOUT LOCK (Gitea #48/#57). Our compiled BTPlayer is NOT the binary's -- the
|
||||||
// Entity base differs -- so the 1995 offsets must never be used as raw byte
|
// Entity base differs -- so the 1995 offsets must never be used as raw byte
|
||||||
@@ -641,6 +670,32 @@ void
|
|||||||
}
|
}
|
||||||
// else: out of lives -> the +10s mission-review post (id 0x18). Deferred.
|
// else: out of lives -> the +10s mission-review post (id 0x18). Deferred.
|
||||||
|
|
||||||
|
//
|
||||||
|
// THE DEATH SCORE COST (@004c07cd-0x4c0828, raw disasm 2026-08-02 -- this
|
||||||
|
// tail was missing from the #52 reconstruction). Gated on advancedDamageOn
|
||||||
|
// (+0x264) alone in the binary: self-award -SpecialCaseDeathPenalty
|
||||||
|
// (role+0x20, the 4.10-only role field restored to SCNROLE.h) as a type-1
|
||||||
|
// score message handed DIRECTLY to the engine base handler (@0042da20 ==
|
||||||
|
// PLAYER.cpp:138 `currentScore += scoreAward` + the vehicle
|
||||||
|
// RespondToScoreMessage no-op hook) -- NOT dispatched, so it bypasses the
|
||||||
|
// BT ScoreInflicted Verify. Shipped content authors the key nowhere, so
|
||||||
|
// the cost is 0 in the field today; a mission that authors it gets it.
|
||||||
|
// BINARY NUANCE [T4]: the 1995 engine cell (+0x1c8) is read by no BT-side
|
||||||
|
// scoreboard (they read +0x278), so the pod's penalty may never have been
|
||||||
|
// visible; our port has ONE currentScore, so it shows.
|
||||||
|
//
|
||||||
|
if (advancedDamageOn && scenarioRole != 0) // this+0x264 (binary derefs role unguarded)
|
||||||
|
{
|
||||||
|
BTPlayer::ScoreMessage death_cost(
|
||||||
|
BTPlayer::ScoreInflictedMessageID, // 0x16
|
||||||
|
sizeof(BTPlayer::ScoreMessage),
|
||||||
|
BTPlayer::ScoreMessage::DamageReceivedScore, // type 1
|
||||||
|
-scenarioRole->GetSpecialCaseDeathPenalty(), // scoreAward @+0x1c, NEGATED
|
||||||
|
0.0f,
|
||||||
|
EntityID::Null); // senderMechID (binary copies @0x522524)
|
||||||
|
Player::ScoreMessageHandler(&death_cost); // the DIRECT base call
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// RELEASE THE DEATH LATCH (#81, the GHOST MECH fix -- 2026-07-30).
|
// RELEASE THE DEATH LATCH (#81, the GHOST MECH fix -- 2026-07-30).
|
||||||
//
|
//
|
||||||
@@ -825,30 +880,45 @@ void
|
|||||||
{
|
{
|
||||||
//
|
//
|
||||||
// Points lost for damage we took (unless we hit ourselves).
|
// Points lost for damage we took (unless we hit ourselves).
|
||||||
|
// sender null guard is PORT SAFETY: the binary derefs the registry
|
||||||
|
// lookup unguarded (@0x4c0453) -- an inflictor that died between
|
||||||
|
// shot and impact would have crashed the 1995 pod.
|
||||||
//
|
//
|
||||||
if (sender_mech != our_mech && scenarioRole != 0) // scoring wave: guard the NULL bring-up role
|
if (sender_mech != 0 && our_mech != 0 // port guards (dead-window straggler)
|
||||||
|
&& sender_mech != our_mech && scenarioRole != 0) // scoring wave: guard the NULL bring-up role
|
||||||
{
|
{
|
||||||
Scalar received =
|
Scalar received =
|
||||||
scenarioRole->CalcDamageReceivedScore(message->damageAmount); // FUN_00429b94
|
scenarioRole->CalcDamageReceivedScore(message->damageAmount); // FUN_00429b94
|
||||||
award = (MECH_TONNAGE(our_mech) / MECH_TONNAGE(sender_mech)) * received;
|
award = (MECH_TONNAGE(our_mech) / MECH_TONNAGE(sender_mech)) * received;
|
||||||
}
|
}
|
||||||
|
// BINARY NUANCE [T1 raw disasm @0x4c0433]: on SELF-inflicted damage
|
||||||
|
// (sender == own vehicle) the 1995 arm skips the math and the common
|
||||||
|
// tail folds its UNINITIALIZED award local into currentScore --
|
||||||
|
// stack garbage, every fall/ram self-hit. The port keeps award=0.0,
|
||||||
|
// a deliberate safe deviation.
|
||||||
|
|
||||||
//
|
//
|
||||||
// Tell the console (if any, and not suppressed) that our VTV
|
// Tell the console (if any, and not suppressed) that our VTV
|
||||||
// was damaged.
|
// was damaged. points_transfered CORRECTED 2026-08-05: the binary
|
||||||
|
// rounds THE COMPUTED AWARD here (raw disasm @0x4c04a7: fld
|
||||||
|
// [ebp-0xc]; call 0x4dcd94 -- an ST0-arg __ftol the decompiler
|
||||||
|
// rendered argless, which an earlier pass misread as Now()).
|
||||||
//
|
//
|
||||||
Host *console_host =
|
Host *console_host =
|
||||||
application->GetHostManager()->GetConsoleHost(); // FUN_00429078
|
application->GetHostManager()->GetConsoleHost(); // FUN_00429078
|
||||||
if (console_host && suppressConsole == 0) // this+0x258
|
BTPlayer *damager_player =
|
||||||
|
(sender_mech != 0) ? MECH_OWNING_PLAYER(sender_mech) : 0; // port guard (dummy shooter)
|
||||||
|
if (console_host && suppressConsole == 0 // this+0x258
|
||||||
|
&& damager_player != 0)
|
||||||
{
|
{
|
||||||
ConsolePlayerVTVDamagedMessage damaged_message(
|
ConsolePlayerVTVDamagedMessage damaged_message(
|
||||||
ownerID, // this+0x18c
|
ownerID, // this+0x18c
|
||||||
MECH_OWNING_PLAYER(sender_mech)->ownerID, // *(sender+0x190)+0x18c
|
damager_player->ownerID, // *(sender+0x190)+0x18c
|
||||||
Round(message->damageAmount), // FUN_004078fc
|
Round(message->damageAmount), // FUN_004078fc
|
||||||
message->pointSenderLo, // msg+0x2c
|
message->zoneIndex, // msg+0x2c
|
||||||
message->pointSenderHi, // msg+0x28
|
message->vitalHit, // msg+0x28
|
||||||
(int)Now().ticks, // FUN_004dcd94
|
Round(award), // FUN_004dcd94(ST0)
|
||||||
message->auxID // msg+0x30
|
message->subsysID // msg+0x30
|
||||||
);
|
);
|
||||||
|
|
||||||
application->SendMessage( // app+0x20, slot+0x18
|
application->SendMessage( // app+0x20, slot+0x18
|
||||||
@@ -862,6 +932,13 @@ void
|
|||||||
|
|
||||||
case BTPlayer::ScoreMessage::KillScore: // 2
|
case BTPlayer::ScoreMessage::KillScore: // 2
|
||||||
{
|
{
|
||||||
|
// PORT SAFETY: an unresolvable victim mech (removed wreck racing the
|
||||||
|
// cross-node dispatch) or a severed own vehicle skips the arm; the
|
||||||
|
// binary derefs both unguarded (@0x4c037e).
|
||||||
|
if (sender_mech == 0 || our_mech == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
// gauge scoring wave: the attacking mech's owner (NULL for the ownerless
|
// gauge scoring wave: the attacking mech's owner (NULL for the ownerless
|
||||||
// BT_SPAWN_ENEMY dummy). Resolved once, guarded everywhere below.
|
// BT_SPAWN_ENEMY dummy). Resolved once, guarded everywhere below.
|
||||||
BTPlayer *sender_owner = MECH_OWNING_PLAYER(sender_mech); // *(sender+0x190), NULL for the dummy
|
BTPlayer *sender_owner = MECH_OWNING_PLAYER(sender_mech); // *(sender+0x190), NULL for the dummy
|
||||||
@@ -1837,6 +1914,14 @@ int BTPlayerAdvancedDamageOn(void *player_v)
|
|||||||
return (player_v != 0) ? ((BTPlayer *)player_v)->advancedDamageOn2 : 0;
|
return (player_v != 0) ? ((BTPlayer *)player_v)->advancedDamageOn2 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #89 death blast, second gate: suppressConsole (+0x258) is SET by the eject
|
||||||
|
// bookkeeping -- an EJECTED pilot's abandoned mech never blast-splashes
|
||||||
|
// (raw disasm @0x4a0ac8-0x4a0ad6).
|
||||||
|
int BTPlayerConsoleSuppressed(void *player_v)
|
||||||
|
{
|
||||||
|
return (player_v != 0) ? (int)((BTPlayer *)player_v)->suppressConsole : 0;
|
||||||
|
}
|
||||||
|
|
||||||
int BTPilotKills(void *pilot)
|
int BTPilotKills(void *pilot)
|
||||||
{
|
{
|
||||||
int k = pilot ? ((BTPlayer *)pilot)->GetKillCount() : 0;
|
int k = pilot ? ((BTPlayer *)pilot)->GetKillCount() : 0;
|
||||||
@@ -2157,138 +2242,247 @@ Logical BTResolveMessageBoard(Entity * /*tracked_mech*/, int *messageId, BitMap
|
|||||||
}
|
}
|
||||||
|
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
// gauge scoring wave: PRODUCERS -- posted by the combat path (mech4.cpp) to feed
|
// THE AUTHENTIC COMBAT REPORT SENDER (#45/#134 -- 2026-08-05)
|
||||||
// the scoreboard. The inflictor is always the viewpoint mech in bring-up, so the
|
|
||||||
// local (crediting) player is application->GetMissionPlayer(). Bridges (not inline
|
|
||||||
// in mech4.cpp) so the message construction lives in this complete-BTPlayer TU.
|
|
||||||
//
|
//
|
||||||
void BTPostDamageScore(Entity *victim, Scalar damage) // Step 6: per-hit SCORE
|
// Reconstructed from the raw disasm of the Mech::TakeDamageMessageHandler
|
||||||
|
// report tail @0x4a02f4-0x4a0890 (dark-gap region, not in the export). This
|
||||||
|
// RETIRES the bring-up producers BTPostDamageScore / BTPostKillScore (see the
|
||||||
|
// tombstone below). The binary's flow, transcribed:
|
||||||
|
//
|
||||||
|
// resolve: shooterEntity = registry find(msg->inflictingEntity) @0x4a033c
|
||||||
|
// shooterPlayer = shooterEntity+0x190 @0x4a034f
|
||||||
|
// victimPlayer = mech+0x190 @0x4a0358
|
||||||
|
// block A @0x4a04da (was ALIVE at entry, destroyed now):
|
||||||
|
// ScoreMessage{id 0x16, type 2 KillScore,
|
||||||
|
// scoreAward = the APPLIED damage tally,
|
||||||
|
// damageAmount = victim role's killBonus (role+0x1c),
|
||||||
|
// vitalHit, zone, subsysID,
|
||||||
|
// senderMechID = the VICTIM} -> the SHOOTER's player
|
||||||
|
// block B @0x4a05d9 (not newly killed, tally != 0):
|
||||||
|
// same shape, type 0 -- which the only registered 0x16 receiver
|
||||||
|
// (@0x4c02e4) VERIFY-rejects; 1995 then folded an UNINITIALIZED
|
||||||
|
// award. Sent for wire fidelity; our handler banks 0.
|
||||||
|
// block C @0x4a06c0 (tally > 0; runs after A too -- kills included):
|
||||||
|
// ScoreMessage{type 1 DamageReceivedScore,
|
||||||
|
// scoreAward = tally,
|
||||||
|
// damageAmount = INTENDED (burstCount x amount),
|
||||||
|
// senderMechID = the INFLICTOR} -> the victim's player
|
||||||
|
//
|
||||||
|
// Score-model consequence [T1]: shooters are credited ONLY for kills; victims
|
||||||
|
// are PENALIZED for damage received (CalcDamageReceivedScore returns the
|
||||||
|
// negative). Per-hit inflicted credit never existed in the 1995 pod.
|
||||||
|
//
|
||||||
|
// Port accommodations, all guarded: registry/player lookups null-checked (the
|
||||||
|
// binary derefs them raw); killBonus basis 0 for the ownerless dummy victim;
|
||||||
|
// a skipped kill credit keeps the #45 NOCREDIT matchlog forensics.
|
||||||
|
//
|
||||||
|
void BTMechPostCombatReports(
|
||||||
|
void *mech_v, // the VICTIM mech (the handler's this)
|
||||||
|
void *msg_v, // the TakeDamageMessage being handled
|
||||||
|
float damage_tally, // loop local_24: applied damage + crit bonuses
|
||||||
|
int vital_hit, // loop local_2c: a zone entered BurningState
|
||||||
|
int report_zone, // msg->damageZone at loop ENTRY (the binary
|
||||||
|
// reads msg+0x24, never rewritten mid-loop)
|
||||||
|
int newly_destroyed) // alive at entry && IsMechDestroyed() now
|
||||||
{
|
{
|
||||||
if (application == 0 || victim == 0)
|
Mech *victim = (Mech *)mech_v;
|
||||||
|
Entity::TakeDamageMessage *msg = (Entity::TakeDamageMessage *)msg_v;
|
||||||
|
if (application == 0 || victim == 0 || msg == 0)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
BTPlayer *local_player = (BTPlayer *)application->GetMissionPlayer();
|
|
||||||
if (local_player == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// ScoreInflicted (scoreType 0): senderMechID = the mech that TOOK the damage.
|
|
||||||
// -> ScoreInflictedMessageHandler: currentScore += tonnageRatio*CalcInflictedScore.
|
|
||||||
BTPlayer::ScoreMessage message(
|
|
||||||
BTPlayer::ScoreInflictedMessageID, // 0x16
|
|
||||||
sizeof(BTPlayer::ScoreMessage),
|
|
||||||
BTPlayer::ScoreMessage::DamageInflictedScore, // 0
|
|
||||||
0.0f, // scoreAward (unused for inflicted)
|
|
||||||
damage, // damageAmount
|
|
||||||
victim->GetEntityID()); // senderMechID = victim
|
|
||||||
local_player->Dispatch(&message);
|
|
||||||
if (getenv("BT_SCORE_LOG"))
|
|
||||||
DEBUG_STREAM << "[score] +damage " << damage << " -> currentScore="
|
|
||||||
<< (Scalar)local_player->GetScore() << "\n" << std::flush;
|
|
||||||
}
|
|
||||||
|
|
||||||
void BTPostKillScore(Entity *victim, Scalar damage) // Step 7: KILL (+ MP death)
|
BTPlayer *shooter_player = 0;
|
||||||
{
|
Entity *shooter = 0;
|
||||||
if (application == 0 || victim == 0)
|
extern int BTIsRegisteredMech(Entity *e);
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// KILL credit. Runs on the VICTIM's node -- the only node whose mech carries a
|
|
||||||
// populated `lastInflictingID` (damage is applied master-side only: 0 of 8800
|
|
||||||
// corpus DMG rows target a replicant, which is why every `DEATH inst=R` row reads
|
|
||||||
// `killer=0:0`). The credit is dispatched to the killer's Player, which HERE is a
|
|
||||||
// REPLICANT, so `Entity::Dispatch` reroutes it to the owning host
|
|
||||||
// (ENTITY.cpp:244-251) and `++killCount` lands on the killer's OWN machine.
|
|
||||||
//
|
|
||||||
// CORRECTED 2026-07-25 (Gitea #45). The previous banner claimed "every node
|
|
||||||
// maintains LOCAL score copies ... each node witnesses the death transition and
|
|
||||||
// tallies its own copies self-consistently". That was FALSE and it is what hid
|
|
||||||
// this bug: the reroute means exactly ONE node increments, no other node can
|
|
||||||
// observe the killer at all, and nothing carried the value back out -- so every
|
|
||||||
// remote pilot's KILLS read 0 on every other pod, all mission (corpus invariant:
|
|
||||||
// 0 of 18818 DMG rows are inst=R, so no other node can even attribute the kill;
|
|
||||||
// every DEATH inst=R row reads killer=0:0). The counters are
|
|
||||||
// now replicated owner->replicant instead; see Read/WriteUpdateRecord above.
|
|
||||||
BTPlayer *killer_player = 0;
|
|
||||||
if (application->GetHostManager() != 0)
|
if (application->GetHostManager() != 0)
|
||||||
{
|
{
|
||||||
Entity *killer = application->GetHostManager()->GetEntityPointer(
|
shooter = application->GetHostManager()->GetEntityPointer(
|
||||||
((Mech *)victim)->lastInflictingID);
|
msg->inflictingEntity);
|
||||||
extern int BTIsRegisteredMech(Entity *e);
|
if (shooter != 0 && BTIsRegisteredMech(shooter))
|
||||||
if (killer != 0 && killer != victim && BTIsRegisteredMech(killer))
|
shooter_player = (BTPlayer *)((Mech *)shooter)->GetPlayerLink();
|
||||||
killer_player = (BTPlayer *)((Mech *)killer)->GetPlayerLink();
|
|
||||||
}
|
}
|
||||||
if (killer_player != 0)
|
BTPlayer *victim_player = (BTPlayer *)victim->GetPlayerLink();
|
||||||
|
|
||||||
|
if (newly_destroyed)
|
||||||
{
|
{
|
||||||
// KillScore (scoreType 2): senderMechID MUST be the VICTIM (!= the
|
//
|
||||||
// receiver's mech) so the handler credits `killCount++` -- a suicide
|
// Block A: the KILL report, to the SHOOTER's player. On the victim's
|
||||||
// (killer == victim) never reaches here.
|
// node that player is a replicant, so Entity::Dispatch reroutes to the
|
||||||
BTPlayer::ScoreMessage kill(
|
// owning host (ENTITY.cpp:244) and the credit lands on the killer's own
|
||||||
Player::ScoreMessageID, // 0x12
|
// machine -- the #45 flow, unchanged. killer == victim (the eject
|
||||||
|
// charge) IS dispatched: the handler negates the award. That is the
|
||||||
|
// #134 panic-penalty path the old BTPostKillScore filtered out.
|
||||||
|
//
|
||||||
|
Scalar kill_bonus = 0.0f; // basis: victim role+0x1c
|
||||||
|
if (victim_player != 0 && victim_player->GetScenarioRole() != 0)
|
||||||
|
kill_bonus = victim_player->GetScenarioRole()->GetKillBonus();
|
||||||
|
if (shooter_player != 0)
|
||||||
|
{
|
||||||
|
BTPlayer::ScoreMessage kill(
|
||||||
|
Player::ScoreMessageID, // 0x16
|
||||||
|
sizeof(BTPlayer::ScoreMessage), // 0x3c
|
||||||
|
BTPlayer::ScoreMessage::KillScore, // 2
|
||||||
|
damage_tally, // +0x1c
|
||||||
|
kill_bonus, // +0x24
|
||||||
|
victim->GetEntityID(), // +0x34 the VICTIM
|
||||||
|
vital_hit, // +0x28
|
||||||
|
report_zone, // +0x2c
|
||||||
|
msg->inflictingSubsystemID); // +0x30
|
||||||
|
shooter_player->Dispatch(&kill);
|
||||||
|
if (getenv("BT_SCORE_LOG"))
|
||||||
|
DEBUG_STREAM << "[score] *** KILL report *** -> shooterPlayer="
|
||||||
|
<< (void *)shooter_player << " tally=" << damage_tally
|
||||||
|
<< " bonus=" << kill_bonus << "\n" << std::flush;
|
||||||
|
}
|
||||||
|
else if (BTMatchLogActive())
|
||||||
|
{
|
||||||
|
// #45 forensics, carried from BTPostKillScore: WHY the credit was
|
||||||
|
// skipped -- missing killer entity, unregistered, or NULL link.
|
||||||
|
BTMatchLog("NOCREDIT",
|
||||||
|
"victim=%d:%d killerID=%d:%d found=%d registered=%d link=%d",
|
||||||
|
BTMatchHostOf(victim->GetEntityID()), (int)victim->GetEntityID(),
|
||||||
|
BTMatchHostOf(msg->inflictingEntity), (int)msg->inflictingEntity,
|
||||||
|
(int)(shooter != 0),
|
||||||
|
(int)(shooter != 0 && BTIsRegisteredMech(shooter)),
|
||||||
|
(int)(shooter != 0 && ((Mech *)shooter)->GetPlayerLink() != 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (damage_tally != 0.0f && shooter_player != 0)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// Block B: the plain inflicted report. Wire fidelity only -- the 0x16
|
||||||
|
// handler Verify-rejects type 0 and banks award 0 (1995 banked an
|
||||||
|
// uninitialized stack float; see the handler's type-0 arm note).
|
||||||
|
//
|
||||||
|
BTPlayer::ScoreMessage inflicted(
|
||||||
|
Player::ScoreMessageID,
|
||||||
sizeof(BTPlayer::ScoreMessage),
|
sizeof(BTPlayer::ScoreMessage),
|
||||||
BTPlayer::ScoreMessage::KillScore, // 2
|
BTPlayer::ScoreMessage::DamageInflictedScore, // 0
|
||||||
0.0f, // scoreAward (killBonus; 0 for bring-up)
|
damage_tally, // +0x1c
|
||||||
damage, // damageAmount (killing-blow)
|
damage_tally, // +0x24 basis = tally
|
||||||
victim->GetEntityID()); // senderMechID = victim
|
victim->GetEntityID(),
|
||||||
killer_player->Dispatch(&kill);
|
vital_hit, report_zone, msg->inflictingSubsystemID);
|
||||||
if (getenv("BT_SCORE_LOG"))
|
shooter_player->Dispatch(&inflicted);
|
||||||
DEBUG_STREAM << "[score] *** KILL *** killerPlayer=" << (void *)killer_player
|
|
||||||
<< " killCount=" << killer_player->GetKillCount()
|
|
||||||
<< " deaths=" << killer_player->GetDeaths()
|
|
||||||
<< " score=" << (Scalar)killer_player->GetScore() << std::endl;
|
|
||||||
}
|
|
||||||
else if (BTMatchLogActive())
|
|
||||||
{
|
|
||||||
//
|
|
||||||
// #45 observability: the credit was SKIPPED. This used to be completely
|
|
||||||
// silent, which is why the bug survived so long -- the counter simply never
|
|
||||||
// moved and no log said why. Record which link in the chain broke so a
|
|
||||||
// single matchlog convicts it: a missing killer entity, an unregistered
|
|
||||||
// mech, a self-kill, or the NULL playerLink hazard.
|
|
||||||
//
|
|
||||||
Entity *k = (application->GetHostManager() != 0)
|
|
||||||
? application->GetHostManager()->GetEntityPointer(
|
|
||||||
((Mech *)victim)->lastInflictingID)
|
|
||||||
: 0;
|
|
||||||
extern int BTIsRegisteredMech(Entity *e);
|
|
||||||
BTMatchLog("NOCREDIT",
|
|
||||||
"victim=%d:%d killerID=%d:%d found=%d self=%d registered=%d link=%d",
|
|
||||||
BTMatchHostOf(victim->GetEntityID()), (int)victim->GetEntityID(),
|
|
||||||
BTMatchHostOf(((Mech *)victim)->lastInflictingID),
|
|
||||||
(int)((Mech *)victim)->lastInflictingID,
|
|
||||||
(int)(k != 0), (int)(k == victim),
|
|
||||||
(int)(k != 0 && BTIsRegisteredMech(k)),
|
|
||||||
(int)(k != 0 && ((Mech *)k)->GetPlayerLink() != 0));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MP DEATH: the victim's death tally.
|
if (damage_tally > 0.0f && victim_player != 0)
|
||||||
//
|
{
|
||||||
// REMOVED 2026-07-30 (#81): this used to dispatch a second
|
//
|
||||||
// `Player::VehicleDeadMessage` to the victim's own player "to credit a
|
// Block C: the RECEIVED report, to the victim's own player -- kills
|
||||||
// death". That message is the RESPAWN-CYCLE TRIGGER, not a scoreboard
|
// included (the binary runs this after block A as well). Carries the
|
||||||
// increment, and the victim already receives one from the mech's death
|
// INTENDED damage (burstCount x amount, @0x4a06d2 fild/fmul) as the
|
||||||
// transition (mech4.cpp:2110) -- which is the hardened, authentic notify
|
// penalty basis and the INFLICTOR as senderMechID; feeds the score
|
||||||
// (it carries the #55 NULL-playerLink fallback). Both fire inside the SAME
|
// penalty and the operator-console VTVDamaged line.
|
||||||
// death transition (BTPostKillScore is called at mech4.cpp:2006, the notify
|
//
|
||||||
// at :2110), so they were always paired: every death dispatched the message
|
int bursts = msg->damageData.burstCount;
|
||||||
// TWICE. The tally itself is credited by the handler's `++deathTally`
|
if (bursts < 1)
|
||||||
// (:538) on the first one, so this dispatch never added anything.
|
bursts = 1; // port guard (mirrors the loop)
|
||||||
//
|
BTPlayer::ScoreMessage received(
|
||||||
// It was invisible because the `deathPending` latch silently deduped it --
|
Player::ScoreMessageID,
|
||||||
// that is what EVERY "death ... SWALLOWED" warning in the field logs
|
sizeof(BTPlayer::ScoreMessage),
|
||||||
// actually was (8 of 8 deaths, a 100% base rate, which is exactly why it
|
BTPlayer::ScoreMessage::DamageReceivedScore, // 1
|
||||||
// correlated with nothing). Once the latch was released to match the
|
damage_tally, // +0x1c
|
||||||
// binary (see the death handler's tail), the duplicate stopped being
|
(Scalar)bursts * msg->damageData.damageAmount, // +0x24 intended
|
||||||
// masked and started a SECOND death cycle: deathCount double-incremented,
|
msg->inflictingEntity, // +0x34 the INFLICTOR
|
||||||
// the first cycle's re-post went stale and tripped the drop-zone
|
vital_hit, report_zone, msg->inflictingSubsystemID);
|
||||||
// `*** MISMATCH ***`, and one respawn burned two cycles. Caught on the
|
victim_player->Dispatch(&received);
|
||||||
// first solo bench of the latch fix.
|
}
|
||||||
//
|
|
||||||
// The kill credit above is unaffected -- it dispatches a ScoreMessage, the
|
|
||||||
// correct message for a scoreboard change.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// The authentic VehicleDead sender (@0x4a07d4-0x4a0890): the respawn-cycle
|
||||||
|
// trigger, dispatched from the death tail of the SAME TakeDamage that killed
|
||||||
|
// the mech, to the mech's own player -- now carrying the killed-by player and
|
||||||
|
// the killing zone (the BT 0x38-byte message extension). Replaces the mech4
|
||||||
|
// death-transition dispatch site, which carried a [T3 -- the binary's exact
|
||||||
|
// sender is undecoded] flag; this IS that sender, decoded. The #55
|
||||||
|
// NULL-playerLink fallback and the DEAD_NOTIFY forensics move here intact.
|
||||||
|
//
|
||||||
|
void BTMechPostVehicleDead(void *mech_v, void *msg_v, int report_zone)
|
||||||
|
{
|
||||||
|
Mech *victim = (Mech *)mech_v;
|
||||||
|
Entity::TakeDamageMessage *msg = (Entity::TakeDamageMessage *)msg_v;
|
||||||
|
if (application == 0 || victim == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Player *owner = victim->GetPlayerLink();
|
||||||
|
// #55 (the David fix), moved intact: resolve the SAME object by the
|
||||||
|
// reverse link the binary's own respawn branch uses when the once-written
|
||||||
|
// playerLink was lost.
|
||||||
|
if (owner == 0)
|
||||||
|
{
|
||||||
|
Player *mission_player = (Player *)application->GetMissionPlayer();
|
||||||
|
if (mission_player != 0
|
||||||
|
&& mission_player->GetPlayerVehicle() == (Entity *)victim)
|
||||||
|
{
|
||||||
|
owner = mission_player;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BTMatchLog("DEAD_NOTIFY", "mech=%d:%d link=%p",
|
||||||
|
BTMatchHostOf(victim->GetEntityID()), (int)victim->GetEntityID(),
|
||||||
|
(void *)owner);
|
||||||
|
if (owner == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// killed-by: the shooter PLAYER's own EntityID (@0x4a07d4 copies
|
||||||
|
// shooterPlayer+0x184); Null when the inflictor or its player is gone.
|
||||||
|
EntityID killed_by = EntityID::Null;
|
||||||
|
if (msg != 0 && application->GetHostManager() != 0)
|
||||||
|
{
|
||||||
|
Entity *shooter = application->GetHostManager()->GetEntityPointer(
|
||||||
|
msg->inflictingEntity);
|
||||||
|
extern int BTIsRegisteredMech(Entity *e);
|
||||||
|
if (shooter != 0 && BTIsRegisteredMech(shooter))
|
||||||
|
{
|
||||||
|
Player *shooter_player = ((Mech *)shooter)->GetPlayerLink();
|
||||||
|
if (shooter_player != 0)
|
||||||
|
killed_by = shooter_player->GetEntityID();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BTPlayer::VehicleDeadMessage vehicle_dead(
|
||||||
|
Player::VehicleDeadMessageID, // 0x17
|
||||||
|
sizeof(BTPlayer::VehicleDeadMessage), // 0x38
|
||||||
|
killed_by,
|
||||||
|
report_zone);
|
||||||
|
owner->Dispatch(&vehicle_dead);
|
||||||
|
if (getenv("BT_DEATH_LOG"))
|
||||||
|
DEBUG_STREAM << "[death] VehicleDead(-1) dispatched to the owning player"
|
||||||
|
<< " (killedBy=" << BTMatchHostOf(killed_by) << ":" << (int)killed_by
|
||||||
|
<< " zone=" << report_zone << ")\n" << std::flush;
|
||||||
|
}
|
||||||
|
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
// TOMBSTONE (2026-08-05): BTPostDamageScore / BTPostKillScore -- the gauge
|
||||||
|
// scoring wave's bring-up producers -- are RETIRED, replaced by the authentic
|
||||||
|
// report tail above (BTMechPostCombatReports / BTMechPostVehicleDead, called
|
||||||
|
// from Mech::TakeDamageMessageHandler at the binary's exact positions).
|
||||||
|
// What changed for the scoreboard:
|
||||||
|
// - per-hit INFLICTED credit is GONE: it was a port invention riding the
|
||||||
|
// never-registered @0x4c0200 handler (the binary's BTPlayer table, byte-
|
||||||
|
// scanned at file 0x112dxx, has 6 entries and none binds it). 1995
|
||||||
|
// scoring = kill awards + received-damage penalties, nothing per-hit.
|
||||||
|
// This also retires the #95 salvo-credit fix that rode it.
|
||||||
|
// - suicides now DISPATCH the kill report and the handler NEGATES the
|
||||||
|
// award -- the #134 panic-eject penalty path the old filter blocked.
|
||||||
|
// - kill credit resolves the shooter from msg->inflictingEntity of the
|
||||||
|
// killing TakeDamage itself; lastInflictingID (stamped from the same
|
||||||
|
// field at handler entry) stays for the LOD router + effect orientation.
|
||||||
|
// - the cross-node reroute story is unchanged: the kill report still
|
||||||
|
// Dispatches to a replicant player and lands on the owner host
|
||||||
|
// (ENTITY.cpp:244-251); KILLS/DEATHS still replicate owner->replicant
|
||||||
|
// (Read/WriteUpdateRecord + the scoreboard heartbeat).
|
||||||
|
// - the #81 single-VehicleDead rule is unchanged: exactly one dispatch per
|
||||||
|
// death, now from the authentic tail instead of the death transition.
|
||||||
|
// The ScoreInflictedMessageID channel and its handler remain registered but
|
||||||
|
// idle (port infra; the binary's analog was dead code in the table).
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
// BTPlayerRoleLocksAdvanced -- complete-type bridge (task #12)
|
// BTPlayerRoleLocksAdvanced -- complete-type bridge (task #12)
|
||||||
@@ -2377,3 +2571,57 @@ Entity *BTPlayerObjectiveMechOf(void *player)
|
|||||||
return 0;
|
return 0;
|
||||||
return (Entity *)((BTPlayer *)player)->GetObjectiveMech(); // @0x284
|
return (Entity *)((BTPlayer *)player)->GetObjectiveMech(); // @0x284
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//#############################################################################
|
||||||
|
// BTPlayerEjectBookkeeping -- complete-type bridge for Mech::EjectPilot
|
||||||
|
// (@0049f854, the PANIC/EJECT punch-out). The handler's two player-side
|
||||||
|
// writes, verbatim from the raw disasm:
|
||||||
|
// * suppressConsole (+0x258) = 1 -- the eject already notified the console
|
||||||
|
// (its own message, the tracked relay tail), so the DEATH that the
|
||||||
|
// returned charge causes must not double-notify;
|
||||||
|
// * the self-destruct amount = ScenarioRole::killBonus (role+0x1c -- the
|
||||||
|
// record map is byte-verified: reader @00429bec dest offsets + the role
|
||||||
|
// ctor's record copy [7]=rec[0]).
|
||||||
|
// Returns the charge; 0 with no player/role (binary derefs unguarded).
|
||||||
|
//#############################################################################
|
||||||
|
Scalar BTPlayerEjectBookkeeping(void *player_v)
|
||||||
|
{
|
||||||
|
BTPlayer *player = (BTPlayer *)player_v;
|
||||||
|
if (player == 0)
|
||||||
|
{
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The console EJECT NOTICE (@0049f88e-0x49f8cf): FUN_004c198c is the
|
||||||
|
// ConsolePlayerMechDeathWithoutHonorMessage ctor -- "death without honor"
|
||||||
|
// IS the punch-out record (4.10 wire id 0xF; our catalogue re-tags it 6,
|
||||||
|
// not load-bearing) -- sent to the console host through the same
|
||||||
|
// ConsoleClientID path as the VTVDamaged notify. The suppressConsole
|
||||||
|
// latch below is its other half: the eject notice REPLACES the death
|
||||||
|
// notify for this death.
|
||||||
|
Host *console_host =
|
||||||
|
application->GetHostManager()->GetConsoleHost(); // FUN_00429078
|
||||||
|
if (console_host != 0)
|
||||||
|
{
|
||||||
|
ConsolePlayerMechDeathWithoutHonorMessage eject_notice(
|
||||||
|
player->ownerID); // this+0x18c
|
||||||
|
application->SendMessage(
|
||||||
|
console_host->GetHostID(), // console_host+0xc
|
||||||
|
NetworkClient::ConsoleClientID, // 5
|
||||||
|
&eject_notice);
|
||||||
|
// Rare + forensic -- always log (the DumpAmmo precedent).
|
||||||
|
DEBUG_STREAM << "[eject] DeathWithoutHonor notice -> console host "
|
||||||
|
<< (int)console_host->GetHostID() << "\n" << std::flush;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "[eject] no console host -- notice skipped\n" << std::flush;
|
||||||
|
}
|
||||||
|
player->suppressConsole = 1; // +0x258
|
||||||
|
const ScenarioRole *role = player->GetScenarioRole(); // +0x208
|
||||||
|
if (role == 0)
|
||||||
|
{
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
return role->GetKillBonus(); // role+0x1c
|
||||||
|
}
|
||||||
|
|||||||
@@ -57,14 +57,26 @@ class DropZone__ReplyMessage;
|
|||||||
// generated the points. Parallels RPPlayer__ScoreMessage, but the BT
|
// generated the points. Parallels RPPlayer__ScoreMessage, but the BT
|
||||||
// score types describe mech combat instead of score zones.
|
// score types describe mech combat instead of score zones.
|
||||||
//
|
//
|
||||||
// Observed message layout (param_2 in the handlers):
|
// Message layout, sizeof 0x3C (field meanings CORRECTED 2026-08-05 from the
|
||||||
// +0x1c scoreAward (Scalar, base Player::ScoreMessage)
|
// raw disasm of the three sender builds in the Mech::TakeDamageMessageHandler
|
||||||
|
// report tail @0x4a04da/@0x4a05d9/@0x4a06c0 -- the old "pointSender" names
|
||||||
|
// were guesses from RP's smaller message, which stops at +0x28):
|
||||||
|
// +0x1c scoreAward (Scalar, base) -- the sender puts the APPLIED
|
||||||
|
// damage tally here; the receiving handler
|
||||||
|
// overwrites it with the computed award before
|
||||||
|
// delegating to Player::ScoreMessageHandler
|
||||||
// +0x20 scoreType (int)
|
// +0x20 scoreType (int)
|
||||||
// +0x24 damageAmount (Scalar) raw damage / point quantity
|
// +0x24 damageAmount (Scalar) the scoring BASIS: type 0 = tally,
|
||||||
// +0x28 pointSenderHi (EntityID word)
|
// type 1 = intended (burstCount x amount),
|
||||||
// +0x2c pointSenderLo (EntityID word)
|
// type 2 = the VICTIM role's killBonus (role+0x1c)
|
||||||
// +0x30 auxID (EntityID / host word)
|
// +0x28 vitalHit (int) a zone reached BurningState during
|
||||||
// +0x34 senderMechID (EntityID -- resolved to the inflicting Mech)
|
// this message's applications (loop local_2c)
|
||||||
|
// +0x2c zoneIndex (int) the struck damage zone (msg+0x24,
|
||||||
|
// the INITIAL resolve -- not the last burst's)
|
||||||
|
// +0x30 subsysID (int) inflictingSubsystemID passed through
|
||||||
|
// from the TakeDamageMessage (+0x5c)
|
||||||
|
// +0x34 senderMechID (EntityID) types 0/2: the VICTIM mech;
|
||||||
|
// type 1: the INFLICTING mech
|
||||||
//
|
//
|
||||||
class BTPlayer__ScoreMessage:
|
class BTPlayer__ScoreMessage:
|
||||||
public Player::ScoreMessage
|
public Player::ScoreMessage
|
||||||
@@ -72,11 +84,16 @@ class DropZone__ReplyMessage;
|
|||||||
public:
|
public:
|
||||||
//
|
//
|
||||||
// Kind of scoring event. Recovered from the branch selector at
|
// Kind of scoring event. Recovered from the branch selector at
|
||||||
// @004c02e4 (this->scoreType, message+0x20) and the dedicated
|
// @004c02e4 (this->scoreType, message+0x20). NOTE: type 0 has NO
|
||||||
// inflicted-damage handler at @004c0200.
|
// scoring arm in the binary -- @004c02e4 Verify-rejects it (line 662)
|
||||||
|
// and @004c0200, the only function that accepts it, appears in no
|
||||||
|
// handler-table entry (byte-scan 2026-08-05: the BTPlayer table at
|
||||||
|
// file 0x112dxx has exactly 6 entries, none binding it). 1995 pod
|
||||||
|
// scoring = kills + received-damage penalties; per-hit inflicted
|
||||||
|
// credit never existed.
|
||||||
//
|
//
|
||||||
enum ScoreType {
|
enum ScoreType {
|
||||||
DamageInflictedScore = 0, // to ScoreInflictedMessageHandler
|
DamageInflictedScore = 0, // sent, but scores nothing (see above)
|
||||||
DamageReceivedScore = 1, // I took damage
|
DamageReceivedScore = 1, // I took damage
|
||||||
KillScore = 2 // I destroyed / was destroyed
|
KillScore = 2 // I destroyed / was destroyed
|
||||||
};
|
};
|
||||||
@@ -85,21 +102,19 @@ class DropZone__ReplyMessage;
|
|||||||
scoreType; // +0x20
|
scoreType; // +0x20
|
||||||
|
|
||||||
Scalar
|
Scalar
|
||||||
damageAmount; // +0x24
|
damageAmount; // +0x24 scoring basis (see layout table)
|
||||||
|
|
||||||
//
|
|
||||||
// Point-source / auxiliary handles carried alongside the damage record
|
|
||||||
// (read by ScoreMessageHandler when building the console feed message).
|
|
||||||
//
|
|
||||||
int
|
int
|
||||||
pointSenderHi; // +0x28
|
vitalHit; // +0x28 zone entered BurningState this message
|
||||||
|
|
||||||
int
|
int
|
||||||
pointSenderLo; // +0x2c
|
zoneIndex; // +0x2c struck damage zone
|
||||||
|
|
||||||
int
|
int
|
||||||
auxID; // +0x30
|
subsysID; // +0x30 inflicting weapon subsystem
|
||||||
|
|
||||||
EntityID
|
EntityID
|
||||||
senderMechID; // +0x34 inflicting mech (point sender)
|
senderMechID; // +0x34 victim (types 0/2) / inflictor (type 1)
|
||||||
|
|
||||||
BTPlayer__ScoreMessage(
|
BTPlayer__ScoreMessage(
|
||||||
Receiver::MessageID message_ID,
|
Receiver::MessageID message_ID,
|
||||||
@@ -107,15 +122,63 @@ class DropZone__ReplyMessage;
|
|||||||
int score_type,
|
int score_type,
|
||||||
Scalar score_award,
|
Scalar score_award,
|
||||||
Scalar damage_amount,
|
Scalar damage_amount,
|
||||||
const EntityID &sender_mech_ID
|
const EntityID &sender_mech_ID,
|
||||||
|
int vital_hit = 0,
|
||||||
|
int zone_index = 0,
|
||||||
|
int subsys_ID = 0
|
||||||
):
|
):
|
||||||
Player::ScoreMessage(message_ID, length, score_award),
|
Player::ScoreMessage(message_ID, length, score_award),
|
||||||
scoreType(score_type),
|
scoreType(score_type),
|
||||||
damageAmount(damage_amount),
|
damageAmount(damage_amount),
|
||||||
|
vitalHit(vital_hit),
|
||||||
|
zoneIndex(zone_index),
|
||||||
|
subsysID(subsys_ID),
|
||||||
senderMechID(sender_mech_ID)
|
senderMechID(sender_mech_ID)
|
||||||
{}
|
{}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//###########################################################################
|
||||||
|
//################# BTPlayer::VehicleDeadMessage ######################
|
||||||
|
//###########################################################################
|
||||||
|
//
|
||||||
|
// BT extension of the engine Player__VehicleDeadMessage (PLAYER.h:
|
||||||
|
// deathCount @0x1c, dropZoneID @0x20). Decoded 2026-08-05 from the death
|
||||||
|
// tail of Mech::TakeDamageMessageHandler (@0x4a07d4-0x4a0890, raw disasm):
|
||||||
|
// the binary builds a 0x38-byte message {size 0x38, id 0x17 ==
|
||||||
|
// Player::VehicleDeadMessageID, pri 1, deathCount -1 (the engine ctor
|
||||||
|
// default), dropZoneID Null, +0x28 = 0, +0x2c = the KILLER PLAYER's own
|
||||||
|
// EntityID (shooterPlayer+0x184), +0x34 = the killing zone (msg+0x24)} and
|
||||||
|
// dispatches it to the dying mech's own player -- the respawn-cycle
|
||||||
|
// trigger, now carrying the killed-by attribution. Whether the @004c05c4
|
||||||
|
// handler consumes the two extension fields is still undecoded (the
|
||||||
|
// function is not in the export); the sender carries them faithfully.
|
||||||
|
//
|
||||||
|
class BTPlayer__VehicleDeadMessage:
|
||||||
|
public Player::VehicleDeadMessage
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
int
|
||||||
|
reserved28; // +0x28 zeroed by the binary (@0x4a0856)
|
||||||
|
|
||||||
|
EntityID
|
||||||
|
killedByPlayerID; // +0x2c the killer PLAYER's EntityID
|
||||||
|
|
||||||
|
int
|
||||||
|
killZone; // +0x34 zone the killing damage struck
|
||||||
|
|
||||||
|
BTPlayer__VehicleDeadMessage(
|
||||||
|
Receiver::MessageID message_ID,
|
||||||
|
size_t length,
|
||||||
|
const EntityID &killed_by_player_ID,
|
||||||
|
int kill_zone
|
||||||
|
):
|
||||||
|
Player::VehicleDeadMessage(message_ID, length), // deathCount -1, dropZone Null
|
||||||
|
reserved28(0),
|
||||||
|
killedByPlayerID(killed_by_player_ID),
|
||||||
|
killZone(kill_zone)
|
||||||
|
{}
|
||||||
|
};
|
||||||
|
|
||||||
//###########################################################################
|
//###########################################################################
|
||||||
//##################### BTPlayer::MakeMessage #########################
|
//##################### BTPlayer::MakeMessage #########################
|
||||||
//###########################################################################
|
//###########################################################################
|
||||||
@@ -202,8 +265,9 @@ class DropZone__ReplyMessage;
|
|||||||
// Player::ScoreMessage.)
|
// Player::ScoreMessage.)
|
||||||
//
|
//
|
||||||
public:
|
public:
|
||||||
typedef BTPlayer__ScoreMessage ScoreMessage;
|
typedef BTPlayer__ScoreMessage ScoreMessage;
|
||||||
typedef BTPlayer__MakeMessage MakeMessage;
|
typedef BTPlayer__MakeMessage MakeMessage;
|
||||||
|
typedef BTPlayer__VehicleDeadMessage VehicleDeadMessage; // BT 0x38 ext (killed-by)
|
||||||
|
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
// Message Support
|
// Message Support
|
||||||
@@ -437,6 +501,8 @@ class DropZone__ReplyMessage;
|
|||||||
friend int BTPlayerExperienceSimLive(void *); // +0x25c reader (issue #2): jams / lights / powersub short path
|
friend int BTPlayerExperienceSimLive(void *); // +0x25c reader (issue #2): jams / lights / powersub short path
|
||||||
friend int BTPlayerExperienceHeatModelOn(void *); // +0x260 reader (issue #2): FUN_004ad7d4 heat-model gate
|
friend int BTPlayerExperienceHeatModelOn(void *); // +0x260 reader (issue #2): FUN_004ad7d4 heat-model gate
|
||||||
friend int BTPlayerAdvancedDamageOn(void *); // +0x268 reader (#83): FUN_0049ffcc collision-rattle gate
|
friend int BTPlayerAdvancedDamageOn(void *); // +0x268 reader (#83): FUN_0049ffcc collision-rattle gate
|
||||||
|
friend Scalar BTPlayerEjectBookkeeping(void *); // +0x258 latch + role killBonus (Mech::EjectPilot @0049f854)
|
||||||
|
friend int BTPlayerConsoleSuppressed(void *); // +0x258 reader (#89): death-blast eject gate @0x4a0ace
|
||||||
// TARGET DESIGNATION (Gitea #48/#57): the mapper used to write the target
|
// TARGET DESIGNATION (Gitea #48/#57): the mapper used to write the target
|
||||||
// RAW at `pilot + 0x284` -- the BINARY's objectiveMech offset, which on our
|
// RAW at `pilot + 0x284` -- the BINARY's objectiveMech offset, which on our
|
||||||
// compiled object is `deathPending` (the respawn latch). These bridges
|
// compiled object is `deathPending` (the respawn latch). These bridges
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
// body recovered from BTL4OPT.EXE (the binary oracle) / the RP analogue. //
|
// body recovered from BTL4OPT.EXE (the binary oracle) / the RP analogue. //
|
||||||
// //
|
// //
|
||||||
// == RUNTIME BRING-UP WORKLIST (replace these) == //
|
// == RUNTIME BRING-UP WORKLIST (replace these) == //
|
||||||
// Mech::GetMissionReviewMode / IsAirborne / SetTargetRange / //
|
// Mech::IsAirborne / SetTargetRange / //
|
||||||
// SetMappingSubsystem / RaiseStatusAlarm //
|
// SetMappingSubsystem / RaiseStatusAlarm //
|
||||||
// Mech__DamageZone::LoadCriticalSubsystems //
|
// Mech__DamageZone::LoadCriticalSubsystems //
|
||||||
// MechSubsystem::TakeDamage / OnAlarmChanged //
|
// MechSubsystem::TakeDamage / OnAlarmChanged //
|
||||||
@@ -97,11 +97,11 @@ void Notify_Objective_Reached(int * /*objective_subsystem*/, Mech * /*mech*/)
|
|||||||
// Mech method stubs.
|
// Mech method stubs.
|
||||||
//===========================================================================//
|
//===========================================================================//
|
||||||
|
|
||||||
// TODO(bring-up): reads mech+0x414 (mission-review playback flag).
|
// (GetMissionReviewMode stub RETIRED 2026-08-03: it was a mislabel of
|
||||||
int Mech::GetMissionReviewMode()
|
// mech+0x414 = ejectPermitted, now served by the inline GetEjectPermitted in
|
||||||
{
|
// mech.hpp.) The REAL mission-review mode is this GLOBAL (DAT_004fd550);
|
||||||
return 0;
|
// the port never enters scrub/review, so it stays 0.
|
||||||
}
|
int gMissionReviewMode = 0;
|
||||||
|
|
||||||
// TODO(bring-up): true while the mech is off the ground (jump-jet / fall state).
|
// TODO(bring-up): true while the mech is off the ground (jump-jet / fall state).
|
||||||
int Mech::IsAirborne()
|
int Mech::IsAirborne()
|
||||||
|
|||||||
@@ -175,9 +175,14 @@ DamageZonePercentTable *
|
|||||||
{
|
{
|
||||||
if (sliceCount <= 0) return 0;
|
if (sliceCount <= 0) return 0;
|
||||||
|
|
||||||
|
Scalar thetaIn = theta;
|
||||||
if (rotateWithTorso) // binary this[3] != 0
|
if (rotateWithTorso) // binary this[3] != 0
|
||||||
{
|
{
|
||||||
theta += owner->TorsoHeading(); // torso+0x1d8
|
// #124 frame adapter, angular half: theta arrives in the 1995 frame
|
||||||
|
// (ResolveHit's z-reflection), so the twist -- measured in OUR frame
|
||||||
|
// -- enters with the OPPOSITE sign (a reflection reverses angular
|
||||||
|
// direction). Binary form: theta += torso+0x1d8.
|
||||||
|
theta -= owner->TorsoHeading(); // torso+0x1d8 (sign: see adapter)
|
||||||
if (theta >= TwoPi) theta -= TwoPi;
|
if (theta >= TwoPi) theta -= TwoPi;
|
||||||
if (theta < 0.0f) theta += TwoPi;
|
if (theta < 0.0f) theta += TwoPi;
|
||||||
}
|
}
|
||||||
@@ -185,6 +190,19 @@ DamageZonePercentTable *
|
|||||||
int index = (int)floorf(theta * (Scalar)sliceCount * (1.0f / TwoPi));
|
int index = (int)floorf(theta * (Scalar)sliceCount * (1.0f / TwoPi));
|
||||||
if (index < 0) index = 0;
|
if (index < 0) index = 0;
|
||||||
if (index > sliceCount - 1) index = sliceCount - 1;
|
if (index > sliceCount - 1) index = sliceCount - 1;
|
||||||
|
|
||||||
|
// #124 twist-sign bench: expose the twist term itself -- the leaf is a
|
||||||
|
// weighted roll, so the SLICE choice (not the rolled zone) is the signal.
|
||||||
|
if (getenv("BT_DMGTABLE_LOG"))
|
||||||
|
{
|
||||||
|
static int s_sl = 0;
|
||||||
|
if (s_sl++ < 400)
|
||||||
|
DEBUG_STREAM << "[slice] rot=" << (int)rotateWithTorso
|
||||||
|
<< " twist=" << (float)owner->TorsoHeading()
|
||||||
|
<< " thetaIn=" << (float)thetaIn
|
||||||
|
<< " thetaAdj=" << (float)theta
|
||||||
|
<< " -> slice " << index << "/" << sliceCount << std::endl;
|
||||||
|
}
|
||||||
return slices[index];
|
return slices[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,6 +295,24 @@ int
|
|||||||
Scalar heightRef = owner->CylinderReferenceHeight(); // owner+0x2ec[+0xc]
|
Scalar heightRef = owner->CylinderReferenceHeight(); // owner+0x2ec[+0xc]
|
||||||
if (heightRef <= 0.0f) return -1;
|
if (heightRef <= 0.0f) return -1;
|
||||||
|
|
||||||
|
// PORT FRAME ADAPTER (#124, corrected 2026-08-03 by the MUZZLE-ANCHOR
|
||||||
|
// probe). The 1995 resolver frame: FORWARD = +Z (the authored ring puts
|
||||||
|
// Front* cells in the +Z arc) and RIGHT = +X (W0/W7 = "Right*") [T1
|
||||||
|
// slice-name data]. Our engine frame, measured with the mech's own
|
||||||
|
// asymmetric geometry as the anchor (BT_ASPECT_TEST muzzle probes -- the
|
||||||
|
// LEFT missile pod LRM15_1 sits at raw local x=-2.32, the RIGHT pod
|
||||||
|
// LRM15_2 at x=+2.32): RIGHT = +X (SAME as 1995) but FORWARD = -Z
|
||||||
|
// (drive-code convention, "forward = -Z at heading 0"). The frames
|
||||||
|
// therefore differ by a Z-NEGATION ONLY -- a REFLECTION, not the pi
|
||||||
|
// rotation first committed (that crossed the pods: left muzzle resolved
|
||||||
|
// rtorso). y (the band axis) untouched.
|
||||||
|
//
|
||||||
|
// A reflection reverses the angular walk direction, so the OTHER angular
|
||||||
|
// input -- the torso-twist term SelectSlice adds -- must flip sign with
|
||||||
|
// it (applied there, same adapter). Twist was 0 in every probe; the
|
||||||
|
// twisted-torso verify is the follow-up on #124.
|
||||||
|
local.z = -local.z;
|
||||||
|
|
||||||
// --- height -> layer (penetration depth) ---
|
// --- height -> layer (penetration depth) ---
|
||||||
Scalar depth = local.y; // binary local_2c
|
Scalar depth = local.y; // binary local_2c
|
||||||
int layerIndex = (int)floorf((Scalar)layerCount * (depth / heightRef));
|
int layerIndex = (int)floorf((Scalar)layerCount * (depth / heightRef));
|
||||||
@@ -305,12 +341,22 @@ int
|
|||||||
{
|
{
|
||||||
static int s_rh = 0;
|
static int s_rh = 0;
|
||||||
if (s_rh++ < 400)
|
if (s_rh++ < 400)
|
||||||
DEBUG_STREAM << "[dmgresolve] localY=" << local.y
|
{
|
||||||
|
extern int BTMechZoneSegAndName(void *mech_v, int zone_idx,
|
||||||
|
int *seg_out, const char **name_out);
|
||||||
|
int seg = -1; const char *zname = 0;
|
||||||
|
if (resolved >= 0)
|
||||||
|
BTMechZoneSegAndName((void *)owner, resolved, &seg, &zname);
|
||||||
|
DEBUG_STREAM << "[dmgresolve] impact=(" << impact.x << ","
|
||||||
|
<< impact.y << "," << impact.z << ")"
|
||||||
|
<< " local=(" << local.x << "," << local.y << "," << local.z << ")"
|
||||||
<< " heightRef=" << heightRef
|
<< " heightRef=" << heightRef
|
||||||
<< " frac=" << (depth / heightRef)
|
<< " frac=" << (depth / heightRef)
|
||||||
<< " layer=" << layerIndex << "/" << layerCount
|
<< " layer=" << layerIndex << "/" << layerCount
|
||||||
<< " theta=" << theta
|
<< " theta=" << theta
|
||||||
<< " -> zone " << resolved << std::endl;
|
<< " -> zone " << resolved
|
||||||
|
<< " '" << (zname ? zname : "?") << "'" << std::endl;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|||||||
+87
-13
@@ -359,22 +359,40 @@ Condenser::~Condenser()
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
//###########################################################################
|
//###########################################################################
|
||||||
// ResetToInitialState -- Condenser (HEAT.TCP)
|
// ResetToInitialState -- Condenser
|
||||||
|
//
|
||||||
|
// RE-TRANSCRIBED FROM THE BINARY (respawn-reset audit 2026-08-04; the function
|
||||||
|
// is MISSING from the Ghidra export -- disassembled raw, tools/disas2.py
|
||||||
|
// 0x4ae534 0x60). The old body chained HeatableSubsystem (temp+heatLoad only,
|
||||||
|
// per the stale HEAT.TCP shard) -- the binary chains HEATSINK (call 0x4ad760),
|
||||||
|
// so the coolant refill DOES run for a condenser; then, RESPAWN-side only:
|
||||||
|
//
|
||||||
|
// test esi, esi ; je ... -> if (powered) {
|
||||||
|
// [this+0x1d0] = 1 -> valveState = 1 (default detent --
|
||||||
|
// the pod RESETS every coolant valve)
|
||||||
|
// eax = [this+0x1d8]; [0x160] = eax -> massScale = refrigerationFactor
|
||||||
|
// (restore the refrigeration output) }
|
||||||
|
//
|
||||||
|
// Mech::Reset's tail then re-runs BTRecomputeCondenserValves (@0049f788), so
|
||||||
|
// the flow fractions rebuild from the reset detents (mech4.cpp). This is the
|
||||||
|
// operator-reported "coolant valves survive a respawn" gap [was T3, now T1].
|
||||||
//
|
//
|
||||||
void
|
void
|
||||||
Condenser::ResetToInitialState(Logical /*powered*/)
|
Condenser::ResetToInitialState(Logical powered)
|
||||||
{
|
{
|
||||||
HeatableSubsystem::ResetToInitialState(True);
|
HeatSink::ResetToInitialState(powered); // call 0x4ad760
|
||||||
|
if (powered)
|
||||||
|
{
|
||||||
|
if (getenv("BT_DEATH_LOG") && valveState != 1)
|
||||||
|
DEBUG_STREAM << "[respawn] " << GetName() << " valve detent "
|
||||||
|
<< valveState << " -> 1 (authentic @004ae534 restore)\n" << std::flush;
|
||||||
|
valveState = 1; // @0x1D0 = 1
|
||||||
|
massScale = refrigerationFactor; // @0x160 = @0x1D8
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// DeathReset (Gitea #55): the respawn sweep's entry for a Condenser -- a coolant
|
// DeathReset (Gitea #55): the respawn sweep's entry for a Condenser.
|
||||||
// loop's valve. NOTE the body above chains HeatableSubsystem's (temperature +
|
|
||||||
// heatLoad) and so does NOT run HeatSink's coolant refill, even though Condenser
|
|
||||||
// derives from HeatSink. That mirrors the existing reconstruction; whether the
|
|
||||||
// binary's Condenser reset also restores the VALVE SETTING is not established from
|
|
||||||
// the decomp yet, so valve detents may still persist across a respawn.
|
|
||||||
// [T3 -- do NOT claim valves are fixed; tracked on the issue.]
|
|
||||||
//
|
//
|
||||||
void
|
void
|
||||||
Condenser::DeathReset(int reset_command)
|
Condenser::DeathReset(int reset_command)
|
||||||
@@ -777,7 +795,7 @@ void
|
|||||||
{
|
{
|
||||||
heatEnergy += pendingHeat;
|
heatEnergy += pendingHeat;
|
||||||
currentTemperature = heatEnergy / thermalMass;
|
currentTemperature = heatEnergy / thermalMass;
|
||||||
UpdateHeatLoad();
|
UpdateHeatLoad(time_slice); // 28 Hz filter cadence (#119)
|
||||||
|
|
||||||
// DIAG census (BT_HEAT_LOG, viewpoint mech): PER-INSTANCE 5-s timers --
|
// DIAG census (BT_HEAT_LOG, viewpoint mech): PER-INSTANCE 5-s timers --
|
||||||
// the old shared static timer aliased to whichever instance crossed the
|
// the old shared static timer aliased to whichever instance crossed the
|
||||||
@@ -850,8 +868,22 @@ void
|
|||||||
// @004ad7f0 -- recompute the radiated/instantaneous heat and feed it through
|
// @004ad7f0 -- recompute the radiated/instantaneous heat and feed it through
|
||||||
// the running-average filter to produce the smoothed heatLoad reading.
|
// the running-average filter to produce the smoothed heatLoad reading.
|
||||||
//
|
//
|
||||||
|
// POD-CADENCE SAMPLING (#119, 2026-08-02). The binary adds ONE sample per
|
||||||
|
// Perform -- a dt-less per-frame term (the myomer-kinetic class, FUN_0041c018
|
||||||
|
// proves Performs ride the frame rate). 15 samples at the pod's 28 Hz = a
|
||||||
|
// 0.536 s smoothing window; sampled at the port's ~59 fps the window halves
|
||||||
|
// and heatLoad turns 2x as twitchy -- which doubles the cadence of the
|
||||||
|
// ReportLeak relaxation oscillation at a drained tank (draw = damage x
|
||||||
|
// heatLoad hunting across the authored 0.0025/0.003 hysteresis band), and
|
||||||
|
// THAT is the rapidly-clipping "warning coo-- war--" leak voice Oracle
|
||||||
|
// reproduced twice in 716. Sim calls pass their slice and samples accrue at
|
||||||
|
// 28 Hz on any machine; ctor/reset prime calls (negative dt) sample
|
||||||
|
// unconditionally. Per-instance clock lives in a static map (the census
|
||||||
|
// precedent above) because the heat-family layouts are factory-size-locked
|
||||||
|
// (sizeof(Myomers) == 0x358 exact) -- no new members.
|
||||||
|
//
|
||||||
void
|
void
|
||||||
HeatSink::UpdateHeatLoad()
|
HeatSink::UpdateHeatLoad(Scalar time_slice)
|
||||||
{
|
{
|
||||||
radiatedHeat = currentTemperature * coolantLevel;
|
radiatedHeat = currentTemperature * coolantLevel;
|
||||||
|
|
||||||
@@ -865,7 +897,31 @@ void
|
|||||||
sample = HeatLoadMaximum;
|
sample = HeatLoadMaximum;
|
||||||
}
|
}
|
||||||
|
|
||||||
heatFilter.AddSample(sample); // FUN_0043ade4
|
if (time_slice >= 0.0f)
|
||||||
|
{
|
||||||
|
static std::map<const void *, Scalar> s_filterClock;
|
||||||
|
Scalar &clock = s_filterClock[this];
|
||||||
|
clock += time_slice;
|
||||||
|
const Scalar kPodTick = 1.0f / 28.0f; // [T1] pod Perform cadence
|
||||||
|
if (clock < kPodTick)
|
||||||
|
{
|
||||||
|
return; // heatLoad holds the last average
|
||||||
|
}
|
||||||
|
int catchUp = 0;
|
||||||
|
while (clock >= kPodTick && ++catchUp <= 15) // >15 samples saturate the window
|
||||||
|
{
|
||||||
|
clock -= kPodTick;
|
||||||
|
heatFilter.AddSample(sample); // FUN_0043ade4
|
||||||
|
}
|
||||||
|
if (catchUp > 15)
|
||||||
|
{
|
||||||
|
clock = 0.0f; // hitch: window already saturated
|
||||||
|
}
|
||||||
|
heatLoad = heatFilter.Average(); // FUN_0043ae0b
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
heatFilter.AddSample(sample); // FUN_0043ade4 (prime call)
|
||||||
heatLoad = heatFilter.Average(); // FUN_0043ae0b
|
heatLoad = heatFilter.Average(); // FUN_0043ae0b
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1372,3 +1428,21 @@ Derivation *
|
|||||||
{
|
{
|
||||||
return HeatSink::GetClassDerivations();
|
return HeatSink::GetClassDerivations();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//===========================================================================//
|
||||||
|
// BTHeatSinkBankCoolantFraction -- complete-type bridge for the EJECT-
|
||||||
|
// permission evaluator (Mech::EvaluateEjectPermission @0049fa1c). The binary
|
||||||
|
// clause: classID 0xBBE (the AGGREGATE heat-sink bank -- the factory case
|
||||||
|
// carries the known "Sensor" mislabel; ctor-address truth per CLASSMAP rule),
|
||||||
|
// fraction = coolantLevel(@0x12C) / thermalCapacity(@0x128). Eject unlocks
|
||||||
|
// below 0.05 -- i.e. the mech has LEAKED nearly dry. Returns 0 = not the
|
||||||
|
// bank, else 1 with *out filled.
|
||||||
|
//===========================================================================//
|
||||||
|
int BTHeatSinkBankCoolantFraction(Subsystem *sub, Scalar *out)
|
||||||
|
{
|
||||||
|
if (sub == 0 || (int)sub->GetClassID() != 0xBBE)
|
||||||
|
return 0;
|
||||||
|
HeatSink *bank = (HeatSink *)sub;
|
||||||
|
*out = bank->CoolantFractionOf();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|||||||
@@ -512,8 +512,11 @@ inline int
|
|||||||
// Internal model helpers
|
// Internal model helpers
|
||||||
//
|
//
|
||||||
public:
|
public:
|
||||||
|
// time_slice >= 0: sample the filter at the POD's 28 Hz cadence (#119
|
||||||
|
// -- the per-Perform filter is frame-rate-dependent, the myomer-kinetic
|
||||||
|
// class); negative (the ctor/reset prime calls) samples unconditionally.
|
||||||
void
|
void
|
||||||
UpdateHeatLoad(); // @004ad7f0
|
UpdateHeatLoad(Scalar time_slice = -1.0f); // @004ad7f0
|
||||||
void
|
void
|
||||||
ClearHeatFilter(); // @004ad884
|
ClearHeatFilter(); // @004ad884
|
||||||
void
|
void
|
||||||
@@ -567,6 +570,11 @@ inline int
|
|||||||
Scalar filterDecay; // @0x150 init 0.15f (@004b8fec) -- ALSO the
|
Scalar filterDecay; // @0x150 init 0.15f (@004b8fec) -- ALSO the
|
||||||
// leak gauge's full-scale divisor (#97)
|
// leak gauge's full-scale divisor (#97)
|
||||||
Scalar LeakGaugeFullScale() const { return filterDecay; }
|
Scalar LeakGaugeFullScale() const { return filterDecay; }
|
||||||
|
// Eject evaluator (@0049fa1c): the bank's remaining-coolant fraction
|
||||||
|
// (@0x12C / @0x128). Capacity is authored nonzero (ctor init 1.0);
|
||||||
|
// the guard only shields a malformed stream from a NaN.
|
||||||
|
Scalar CoolantFractionOf() const
|
||||||
|
{ return (thermalCapacity > 0.0f) ? (coolantLevel / thermalCapacity) : 0.0f; }
|
||||||
Scalar thermalMass; // @0x154 resource +0xF4
|
Scalar thermalMass; // @0x154 resource +0xF4
|
||||||
Scalar heatEnergy; // @0x158 init = thermalMass * startingTemperature
|
Scalar heatEnergy; // @0x158 init = thermalMass * startingTemperature
|
||||||
Scalar coolantFlowScale; // @0x15C init 1.0f (== "word57")
|
Scalar coolantFlowScale; // @0x15C init 1.0f (== "word57")
|
||||||
|
|||||||
@@ -1233,7 +1233,7 @@ void
|
|||||||
}
|
}
|
||||||
heatEnergy += pendingHeat; // [0x56] += [0x72]
|
heatEnergy += pendingHeat; // [0x56] += [0x72]
|
||||||
currentTemperature = heatEnergy / thermalMass; // [0x45] = [0x56]/[0x55]
|
currentTemperature = heatEnergy / thermalMass; // [0x45] = [0x56]/[0x55]
|
||||||
UpdateHeatLoad(); // FUN_004ad7f0
|
UpdateHeatLoad(time_slice); // FUN_004ad7f0 (28 Hz cadence, #119)
|
||||||
pendingHeat = 0.0f;
|
pendingHeat = 0.0f;
|
||||||
|
|
||||||
Scalar target = 300.0f
|
Scalar target = 300.0f
|
||||||
|
|||||||
+228
-19
@@ -466,6 +466,7 @@ const Receiver::HandlerEntry
|
|||||||
MESSAGE_ENTRY(Mech, PlayerLink),
|
MESSAGE_ENTRY(Mech, PlayerLink),
|
||||||
MESSAGE_ENTRY(Mech, BalanceCoolant), // id 0x16 @0049f728 (issue #20)
|
MESSAGE_ENTRY(Mech, BalanceCoolant), // id 0x16 @0049f728 (issue #20)
|
||||||
MESSAGE_ENTRY(Mech, DuckRequest), // id 0x1a @0049fa00 (CROUCH, 2026-07-26)
|
MESSAGE_ENTRY(Mech, DuckRequest), // id 0x1a @0049fa00 (CROUCH, 2026-07-26)
|
||||||
|
MESSAGE_ENTRY(Mech, EjectPilot), // id 0x19 @0049f854 (PANIC/EJECT, 2026-08-02)
|
||||||
};
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -513,6 +514,137 @@ void
|
|||||||
DEBUG_STREAM << "[duck] DuckRequest: duckState -> 1" << std::endl << std::flush;
|
DEBUG_STREAM << "[duck] DuckRequest: duckState -> 1" << std::endl << std::flush;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// @0049fa1c -- the EJECT-PERMISSION evaluator (writes ejectPermitted @0x414).
|
||||||
|
// Raw decomp, transcribed clause for clause: walk the roster from index 2 --
|
||||||
|
// classID 0xBBE (HeatSinkBank): coolant fraction = coolantLevel/thermalCapacity
|
||||||
|
// classID 0xBC1 (Generator): count live (not destroyed && state != 4)
|
||||||
|
// MechWeapon-derived: count live (not destroyed; ammo-fed weapons
|
||||||
|
// additionally need weaponAlarm != 7 = NoAmmo)
|
||||||
|
// then permitted = weapons < ejectMinWeapons || generators == 0
|
||||||
|
// || coolant < 0.05 (@0049fb50)
|
||||||
|
// || (MovementMode 3/4 [leg-gimped] && player simLive == 0).
|
||||||
|
// The binary refreshes this per frame from an UNEXPORTED caller; the port
|
||||||
|
// evaluates on demand (the handler + any future panic-lamp consumer).
|
||||||
|
//
|
||||||
|
int
|
||||||
|
Mech::EvaluateEjectPermission()
|
||||||
|
{
|
||||||
|
extern int BTWeaponCountsForEject(Subsystem *sub); // projweap.cpp (-1 = not a weapon)
|
||||||
|
extern int BTGeneratorCountsForEject(Subsystem *sub); // powersub.cpp (-1 = not a generator)
|
||||||
|
extern int BTHeatSinkBankCoolantFraction(Subsystem *sub, Scalar *out); // heat.cpp
|
||||||
|
|
||||||
|
int liveWeapons = 0;
|
||||||
|
int liveGenerators = 0;
|
||||||
|
Scalar coolantFrac = 0.0f; // local_10 (0 when no bank streams)
|
||||||
|
|
||||||
|
for (int i = 2; i < subsystemCount; ++i) // binary: from index 2
|
||||||
|
{
|
||||||
|
Subsystem *s = (Subsystem *)subsystemArray[i];
|
||||||
|
if (s == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Scalar frac;
|
||||||
|
if (BTHeatSinkBankCoolantFraction(s, &frac))
|
||||||
|
{
|
||||||
|
coolantFrac = frac;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
int g = BTGeneratorCountsForEject(s);
|
||||||
|
if (g >= 0)
|
||||||
|
{
|
||||||
|
liveGenerators += g;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
int w = BTWeaponCountsForEject(s);
|
||||||
|
if (w >= 0)
|
||||||
|
{
|
||||||
|
liveWeapons += w;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int gimped = (MovementMode() == 3 || MovementMode() == 4); // mech+0x40 - 3U < 2
|
||||||
|
extern int BTPlayerExperienceSimLive(void *owner_mech); // btplayer.cpp (+0x25c;
|
||||||
|
int noviceSim = (BTPlayerExperienceSimLive(this) == 0); // NULL-player reads LIVE)
|
||||||
|
|
||||||
|
// (A short-lived coolant-clause HYSTERESIS was tried and REMOVED
|
||||||
|
// 2026-08-03, same night, for strict fidelity: the binary's clause is
|
||||||
|
// this plain `< 0.05` compare, evaluated per frame by the master
|
||||||
|
// performance (FUN_004a9b5c+0x10) -- the ARCADE ran exactly this, so a
|
||||||
|
// bank fraction hovering at the threshold flapped the panic-arm mode at
|
||||||
|
// frame rate there too. The flap is cosmetic churn (mode mask + lamp;
|
||||||
|
// hundreds of transitions ran crash-free on the bench) and was NOT the
|
||||||
|
// operator's audio tick -- that was the autofire scalpel spamming the
|
||||||
|
// per-pull jam click, no game bug. Keep the authentic compare.)
|
||||||
|
ejectPermitted =
|
||||||
|
(liveWeapons < ejectMinWeapons) // @0x448 floor (inert at 0)
|
||||||
|
|| (liveGenerators == 0)
|
||||||
|
|| (coolantFrac < 0.05f) // _DAT_0049fb50
|
||||||
|
|| (gimped && noviceSim);
|
||||||
|
return ejectPermitted;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// @0049f854 -- EjectPilot (id 0x19): the cockpit PANIC/EJECT punch-out.
|
||||||
|
// See the header comment (mech.hpp) for the byte-level story.
|
||||||
|
//
|
||||||
|
void
|
||||||
|
Mech::EjectPilotMessageHandler(ReceiverDataMessageOf<int> *message)
|
||||||
|
{
|
||||||
|
if (message->dataContents <= 0) // press only (msg+0xc)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (EvaluateEjectPermission() == 0) // @0x414 gate: healthy mechs refuse
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "[eject] " << GetEntityID()
|
||||||
|
<< " REFUSED (mech not crippled enough)" << std::endl << std::flush;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (IsDisabled()) // @0049fb54 -- no ejecting from a wreck
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "[eject] " << GetEntityID()
|
||||||
|
<< " REFUSED (disabled: movementMode "
|
||||||
|
<< MovementMode() << ")" << std::endl << std::flush;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Binary @0049f88e-0x49f8cf: build the console eject notice (FUN_004c198c
|
||||||
|
// from ownerID @0x18c) and send it to the console host. CORE: forensic
|
||||||
|
// log; the relay wire is the tracked tail. The suppressConsole latch set
|
||||||
|
// below is the authentic other half (the following DEATH must not also
|
||||||
|
// notify the console).
|
||||||
|
extern Scalar BTPlayerEjectBookkeeping(void *player); // btplayer.cpp:
|
||||||
|
void *player = GetPlayerLink(); // suppressConsole=1,
|
||||||
|
Scalar charge = (player != 0) // returns role killBonus
|
||||||
|
? BTPlayerEjectBookkeeping(player) : 0.0f;
|
||||||
|
|
||||||
|
DEBUG_STREAM << "[eject] " << GetEntityID()
|
||||||
|
<< " PUNCH-OUT: charge=" << charge
|
||||||
|
<< " (role killBonus)" << std::endl << std::flush;
|
||||||
|
|
||||||
|
graphicAlarm.SetLevel(10); // FUN_0041bbd8(this+0x2C, 0xA) -- EJECT state
|
||||||
|
|
||||||
|
Damage dmg; // FUN_0041db7c (Damage::Damage)
|
||||||
|
dmg.damageType = (Enumeration)2; // Explosive
|
||||||
|
dmg.damageAmount = charge; // role+0x1c (killBonus)
|
||||||
|
dmg.impactPoint = localOrigin.linearPosition; // binary copies this+0x100
|
||||||
|
dmg.burstCount = 1; // binary writes 0; our victim-side
|
||||||
|
// guard clamps 0->1 -- same outcome
|
||||||
|
Entity::TakeDamageMessage take_damage(
|
||||||
|
Entity::TakeDamageMessageID, sizeof(Entity::TakeDamageMessage),
|
||||||
|
GetEntityID(), // inflicting = SELF (this+0x184)
|
||||||
|
-1, // unaimed -> cylinder resolves
|
||||||
|
dmg,
|
||||||
|
-1); // inflictingSubsystemID = -1
|
||||||
|
Dispatch(&take_damage); // vtbl+0xC self-dispatch
|
||||||
|
}
|
||||||
|
|
||||||
Receiver::MessageHandlerSet
|
Receiver::MessageHandlerSet
|
||||||
Mech::MessageHandlers(
|
Mech::MessageHandlers(
|
||||||
ELEMENTS(Mech::MessageHandlerEntries),
|
ELEMENTS(Mech::MessageHandlerEntries),
|
||||||
@@ -931,24 +1063,40 @@ void
|
|||||||
(float)(message->damageData.impactPoint.z - localOrigin.linearPosition.z));
|
(float)(message->damageData.impactPoint.z - localOrigin.linearPosition.z));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Death-edge latch, MOVED 2026-08-05 to the authentic position (binary
|
||||||
|
// @0x4a0303: captured at handler ENTRY, before the collision divert). It
|
||||||
|
// arms the whole death tail -- kill report, VehicleDead, death blast.
|
||||||
|
// Capturing it after the divert (where #89 first placed it) meant a
|
||||||
|
// COLLISION death could never arm the tail.
|
||||||
|
//
|
||||||
|
const int deathBlastArmed = !IsMechDestroyed(); // [ebp-0x10], inverted
|
||||||
|
//
|
||||||
|
// The zone the reports + VehicleDead carry: msg+0x24 as of loop entry.
|
||||||
|
// The binary never rewrites msg+0x24 after the initial cylinder resolve;
|
||||||
|
// our loop-exit matchlog write (damageZone = LAST burst's zone) must not
|
||||||
|
// leak into them.
|
||||||
|
//
|
||||||
|
int reportZone = message->damageZone;
|
||||||
|
DamageLookupTable *table = (DamageLookupTable *)damageLookupTable; // named member (Wword absorbs!)
|
||||||
|
|
||||||
//
|
//
|
||||||
// @0x4a0368 -- the COLLISION DIVERT (#83, closing the #82 chain). Damage
|
// @0x4a0368 -- the COLLISION DIVERT (#83, closing the #82 chain). Damage
|
||||||
// type 0 == the mover's collision damage (what the crash response policy
|
// type 0 == the mover's collision damage (what the crash response policy
|
||||||
// forwards). It NEVER reaches the zone/armor loop below: it is priced +
|
// forwards). It NEVER reaches the zone/armor loop below: it is priced +
|
||||||
// distributed as INTERNAL RATTLE by DistributeCollisionDamage (@0049ffcc)
|
// distributed as INTERNAL RATTLE by DistributeCollisionDamage (@0049ffcc).
|
||||||
// and the handler is done. This was the missing half of the crash
|
// CORRECTED 2026-08-05: the binary does NOT return here -- it JUMPS TO THE
|
||||||
// self-damage reconstruction -- without the divert, the raw kinetic-energy
|
// DEATH TAIL (raw disasm @0x4a0375: jmp 0x4a07b5), so a mech that dies of
|
||||||
// amounts (a 60-ton mech prices ~1000+ per wall tap) fell through into the
|
// the rattle still posts VehicleDead (the respawn trigger) and still
|
||||||
// WEAPON loop and a single bump killed the mech (run-14 field report).
|
// blasts. The early `return` this block shipped with was a latent
|
||||||
|
// "wall-death strands the pilot" hazard. No score reports on this path --
|
||||||
|
// the jump bypasses them; a collision death credits no one. Authentic.
|
||||||
//
|
//
|
||||||
if (message->damageData.damageType == 0)
|
if (message->damageData.damageType == 0)
|
||||||
{
|
{
|
||||||
DistributeCollisionDamage(&message->damageData);
|
DistributeCollisionDamage(&message->damageData);
|
||||||
Check_Fpu();
|
goto death_tail;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DamageLookupTable *table = (DamageLookupTable *)damageLookupTable; // named member (Wword absorbs!)
|
|
||||||
// #92: when this gate fails the zone keeps whatever the message carried --
|
// #92: when this gate fails the zone keeps whatever the message carried --
|
||||||
// which for an unaimed weapon hit is 0, i.e. EVERY shot lands on zone 0.
|
// which for an unaimed weapon hit is 0, i.e. EVERY shot lands on zone 0.
|
||||||
if (getenv("BT_DMGTABLE_LOG"))
|
if (getenv("BT_DMGTABLE_LOG"))
|
||||||
@@ -973,6 +1121,7 @@ void
|
|||||||
<< message->damageData.impactPoint.z << ")\n" << std::flush;
|
<< message->damageData.impactPoint.z << ")\n" << std::flush;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
reportZone = message->damageZone; // post-resolve (@0x4a0396 write)
|
||||||
//
|
//
|
||||||
// #80 -- the faithful application loop (binary @0x4a0423-0x4a04d8), which
|
// #80 -- the faithful application loop (binary @0x4a0423-0x4a04d8), which
|
||||||
// REPLACES the engine-base single application. Three things the base
|
// REPLACES the engine-base single application. Three things the base
|
||||||
@@ -1044,14 +1193,20 @@ void
|
|||||||
}
|
}
|
||||||
message->damageZone = zoneIndex; // matchlog sees the LAST zone
|
message->damageZone = zoneIndex; // matchlog sees the LAST zone
|
||||||
|
|
||||||
// Binary tail (@0x4a04da-0x4a07b2, decoded + deferred): builds id-0x16
|
//
|
||||||
// damage/kill REPORT messages -- {tally, zone, zoneDestroyed flag,
|
// THE REPORT TAIL (@0x4a04da-0x4a07b2), reconstructed 2026-08-05 [T1]:
|
||||||
// inflicting subsystem, victim name} -- to the shooter's player (with a
|
// the kill / inflicted / received score reports to the shooter's and
|
||||||
// kill-flagged variant when this damage NEWLY disabled the mech) and to
|
// victim's players. Sender lives in btplayer.cpp (complete-BTPlayer
|
||||||
// the victim's player. That is the authentic stats plumbing (#45); the
|
// TU, per the databinding rule); it receives the APPLIED tally, the
|
||||||
// port's matchlog + BTPostDamageScore cover the bookkeeping today [T3].
|
// vital-wreck flag, and the loop-ENTRY zone. This retires
|
||||||
(void)zoneDestroyed;
|
// BTPostDamageScore/BTPostKillScore -- see the btplayer.cpp tombstone.
|
||||||
(void)damageTally;
|
//
|
||||||
|
{
|
||||||
|
extern void BTMechPostCombatReports(void *, void *, float, int, int, int);
|
||||||
|
BTMechPostCombatReports((void *)this, (void *)message,
|
||||||
|
(float)damageTally, zoneDestroyed, reportZone,
|
||||||
|
deathBlastArmed && IsMechDestroyed());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MP MATCH FORENSICS (matchlog.hpp): the victim-side authoritative damage
|
// MP MATCH FORENSICS (matchlog.hpp): the victim-side authoritative damage
|
||||||
@@ -1080,6 +1235,45 @@ void
|
|||||||
(float)message->damageData.impactPoint.y,
|
(float)message->damageData.impactPoint.y,
|
||||||
(float)message->damageData.impactPoint.z);
|
(float)message->damageData.impactPoint.z);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// THE DEATH TAIL (@0x4a07b5-0x4a0bda, raw disasm -- the un-exported tail
|
||||||
|
// of THIS handler). Gate [T1]: mech was ALIVE at entry ([ebp-0x10]) and
|
||||||
|
// is dead(9)/eject(10) now. Sequence:
|
||||||
|
// 1. Player::VehicleDeadMessage (id 0x17; the BT 0x38-byte extension
|
||||||
|
// carrying killed-by player + kill zone) -> the mech's OWN player:
|
||||||
|
// the respawn-cycle trigger. CORRECTED 2026-08-05: the #89 banner
|
||||||
|
// previously read this build as "SetBurningState (mech id 0x17)" --
|
||||||
|
// it is a PLAYER-table id: the binary's BTPlayer handler table (file
|
||||||
|
// 0x112dxx) binds 0x17 -> @0x4c05c4 VehicleDeadMessageHandler; the
|
||||||
|
// mech-table 0x17 (@0x49f674) is unrelated. Dispatched from HERE --
|
||||||
|
// this is the decoded sender the mech4 transition site's
|
||||||
|
// [T3 sender-undecoded] flag was waiting for.
|
||||||
|
// 2. the death Explosion (id 3 MakeMessage, 0x5C bytes, model 0x31 at
|
||||||
|
// the mech origin, @0x4a08bd) -- the port fires the authored per-mech
|
||||||
|
// death list from the death transition instead (#42 history): same
|
||||||
|
// once-per-death edge, not double-spawned.
|
||||||
|
// 3. SplashDamage (#89), gated advancedDamageOn (+0x264) AND NOT
|
||||||
|
// suppressConsole (+0x258 -- ejected pilots' mechs never blast):
|
||||||
|
// Damage{type=2 Explosive, amount=deathSplashDamage@0x520,
|
||||||
|
// impact=mech origin,
|
||||||
|
// burstCount=round(0.001 * moverMass@0x20c * 15.0)}
|
||||||
|
// radius=deathSplashRadius@0x524, excluded=the dying mech;
|
||||||
|
// falloff bursts/dist^1.25 per victim inside the shared core.
|
||||||
|
// 4. ForceUpdate(1).
|
||||||
|
// The gate predicate here is IsMechDestroyed() (graphicAlarm >= 9, the
|
||||||
|
// structural flag @0x49fb54) -- the binary tests movementMode 9|10, but
|
||||||
|
// the death transition sets mode 9 synchronously with the structural flag
|
||||||
|
// on every path through here, so the edges coincide; this exact predicate
|
||||||
|
// is the one the #89 blast benches verified both ways.
|
||||||
|
death_tail:
|
||||||
|
if (deathBlastArmed && IsMechDestroyed())
|
||||||
|
{
|
||||||
|
extern void BTMechPostVehicleDead(void *, void *, int);
|
||||||
|
BTMechPostVehicleDead((void *)this, (void *)message, reportZone);
|
||||||
|
|
||||||
|
extern void BTApplyDeathSplash(void *mech_v);
|
||||||
|
BTApplyDeathSplash((void *)this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -1249,6 +1443,8 @@ Mech::Mech(
|
|||||||
mechNameFilter.Initialize(); // FUN_00435a7c(this+0xdb)
|
mechNameFilter.Initialize(); // FUN_00435a7c(this+0xdb)
|
||||||
masterAlarm = AlarmIndicator(0x21); // FUN_0041b9ec(this+0xe7,0x21)
|
masterAlarm = AlarmIndicator(0x21); // FUN_0041b9ec(this+0xe7,0x21)
|
||||||
rearFiring = 0; // (task #68) ORed from the weapons below
|
rearFiring = 0; // (task #68) ORed from the weapons below
|
||||||
|
ejectPermitted = 0; // @0x414 (refreshed by EvaluateEjectPermission)
|
||||||
|
ejectMinWeapons = 0; // @0x448 (no exported writer -- zero, clause inert [T3])
|
||||||
// (F7 correction) the binary's 0x400 = FLT_MAX init is DistanceToMissile's
|
// (F7 correction) the binary's 0x400 = FLT_MAX init is DistanceToMissile's
|
||||||
// "no missile" far default (attr id 56), NOT a maxSpeed -- the old member
|
// "no missile" far default (attr id 56), NOT a maxSpeed -- the old member
|
||||||
// is retired; distanceToMissile (init below) owns the slot's meaning.
|
// is retired; distanceToMissile (init below) owns the slot's meaning.
|
||||||
@@ -1764,8 +1960,11 @@ Mech::Mech(
|
|||||||
|
|
||||||
Wword(0x68) = model->cameraOffset; // FUN_00408440(this+0x1a0, rec+0xA8)
|
Wword(0x68) = model->cameraOffset; // FUN_00408440(this+0x1a0, rec+0xA8)
|
||||||
Wword(0x130) = model->deathEffectResourceID; // rec+0x74 -> mech+0x4c0
|
Wword(0x130) = model->deathEffectResourceID; // rec+0x74 -> mech+0x4c0
|
||||||
Wword(0x148) = model->deathSplashDamage; // rec+0x78 -> mech+0x520
|
deathSplashDamage = model->deathSplashDamage; // rec+0x78 -> mech+0x520 (#89:
|
||||||
Wword(0x149) = model->deathSplashRadius; // rec+0x7C -> mech+0x524
|
deathSplashRadius = model->deathSplashRadius; // rec+0x7C -> mech+0x524 named
|
||||||
|
// members -- the Wword bank is
|
||||||
|
// GLOBAL and clobbered per-mech
|
||||||
|
// values chassis-to-chassis)
|
||||||
Wword(0x195) = Wword(0x196) = 0;
|
Wword(0x195) = Wword(0x196) = 0;
|
||||||
Wword(0xfd) = 0;
|
Wword(0xfd) = 0;
|
||||||
masterAlarm.SetLevel(0); // FUN_0041bbd8(this+0xe7,0)
|
masterAlarm.SetLevel(0); // FUN_0041bbd8(this+0xe7,0)
|
||||||
@@ -1919,6 +2118,9 @@ Mech::Mech(
|
|||||||
ramLastVictim = 0; // ram contact-edge state
|
ramLastVictim = 0; // ram contact-edge state
|
||||||
ramContactLinger = 0.0f;
|
ramContactLinger = 0.0f;
|
||||||
lastInflictingDamage = 0.0f; // task #60: killing-blow magnitude (set on hit)
|
lastInflictingDamage = 0.0f; // task #60: killing-blow magnitude (set on hit)
|
||||||
|
mapPosture = 0; // binary @0x3f8 (the ctor zero-block @0x4b3d18 zeroes the band)
|
||||||
|
myomerEffectiveness = 1.0f; // binary @0x79c: 0-init + per-frame MAX; the port holds the
|
||||||
|
// healthy 1.0 until the Myomers Performance feeder lands [T3]
|
||||||
if (GroundReal() && GetCollisionVolumeCount() > 0
|
if (GroundReal() && GetCollisionVolumeCount() > 0
|
||||||
&& collisionTemplate != 0 && collisionVolume != 0)
|
&& collisionTemplate != 0 && collisionVolume != 0)
|
||||||
{
|
{
|
||||||
@@ -2290,6 +2492,13 @@ Logical
|
|||||||
void
|
void
|
||||||
Mech::ReadUpdateRecord(Simulation::UpdateRecord *message)
|
Mech::ReadUpdateRecord(Simulation::UpdateRecord *message)
|
||||||
{
|
{
|
||||||
|
// #108 ghost forensics: stamp the staleness detector on every applied
|
||||||
|
// record (replicants only -- the detector lives on the death handler).
|
||||||
|
if (GetInstance() == ReplicantInstance)
|
||||||
|
{
|
||||||
|
extern void BTMechNoteUpdateApplied(void *mech_v);
|
||||||
|
BTMechNoteUpdateApplied((void *)this);
|
||||||
|
}
|
||||||
// RECORD-CADENCE probe (BT_RXJIT): quantify how EVENLY records arrive (wall-clock
|
// RECORD-CADENCE probe (BT_RXJIT): quantify how EVENLY records arrive (wall-clock
|
||||||
// ms between arrivals) -- the jitter that makes the peer's corrections random.
|
// ms between arrivals) -- the jitter that makes the peer's corrections random.
|
||||||
if (getenv("BT_RXJIT") && GetInstance() == ReplicantInstance)
|
if (getenv("BT_RXJIT") && GetInstance() == ReplicantInstance)
|
||||||
|
|||||||
@@ -357,7 +357,11 @@ struct ShotDescriptor
|
|||||||
enum ResetMode { MissionReviewReset = 0 }; // reset-mode selector (Reset arg, value 0)
|
enum ResetMode { MissionReviewReset = 0 }; // reset-mode selector (Reset arg, value 0)
|
||||||
|
|
||||||
void SetMappingSubsystem(Subsystem *mapper); // btl4app.cpp:567
|
void SetMappingSubsystem(Subsystem *mapper); // btl4app.cpp:567
|
||||||
Logical GetMissionReviewMode(); // reads this+0x414 (btl4mppr.cpp:366)
|
// ejectPermitted (@0x414) accessor -- the panic-arm watcher's read
|
||||||
|
// (btl4mppr InterpretControls @004d196c). [The old GetMissionReviewMode
|
||||||
|
// here was a mislabel of the same cell; the real review mode is the
|
||||||
|
// GLOBAL DAT_004fd550 (btl4pb.cpp). Corrected 2026-08-03.]
|
||||||
|
int GetEjectPermitted() const { return ejectPermitted; }
|
||||||
void SetTargetRange(Scalar range); // writes this+0x404 (btl4mppr.cpp:407)
|
void SetTargetRange(Scalar range); // writes this+0x404 (btl4mppr.cpp:407)
|
||||||
void Reset(const Origin &origin, int mode); // btl4pb.cpp:555 (FUN_0049fb74)
|
void Reset(const Origin &origin, int mode); // btl4pb.cpp:555 (FUN_0049fb74)
|
||||||
|
|
||||||
@@ -650,6 +654,12 @@ public:
|
|||||||
// binary part_012.c:9938-9940 + 9974-9975). By-name access only; declared
|
// binary part_012.c:9938-9940 + 9974-9975). By-name access only; declared
|
||||||
// after the layout-locked fields so nothing shifts.
|
// after the layout-locked fields so nothing shifts.
|
||||||
Scalar standingTemplateMaxY; // binary @0x518 collisionTemplate->maxY at ctor
|
Scalar standingTemplateMaxY; // binary @0x518 collisionTemplate->maxY at ctor
|
||||||
|
// #89 DEATH BLAST (raw disasm @0x4a07b8-0x4a0bda): the authored death-
|
||||||
|
// explosion pair, model "gamedata" keys -> ctor. PROMOTED from the Wword
|
||||||
|
// scratch bank (the bank is one GLOBAL array -- per-mech authored values
|
||||||
|
// were silently clobbered to the last-loaded chassis).
|
||||||
|
Scalar deathSplashDamage; // binary @0x520 <- model rec+0x78 "DeathSplashDamage"
|
||||||
|
Scalar deathSplashRadius; // binary @0x524 <- model rec+0x7C "DeathSplashRadius"
|
||||||
Scalar duckedTemplateMaxY; // binary @0x51c 0.6 x standing (duck preset)
|
Scalar duckedTemplateMaxY; // binary @0x51c 0.6 x standing (duck preset)
|
||||||
Scalar templateBottomLift; // binary @0x4b8 0.05 x (volume maxX-minX)
|
Scalar templateBottomLift; // binary @0x4b8 0.05 x (volume maxX-minX)
|
||||||
|
|
||||||
@@ -739,6 +749,15 @@ protected:
|
|||||||
// `stateFlags`): OR of every mounted weapon's rearFiring flag -- "this
|
// `stateFlags`): OR of every mounted weapon's rearFiring flag -- "this
|
||||||
// mech carries a rear arsenal". The look-back view arms those weapons.
|
// mech carries a rear arsenal". The look-back view arms those weapons.
|
||||||
int rearFiring; // @0x410 this[0x104]
|
int rearFiring; // @0x410 this[0x104]
|
||||||
|
int ejectPermitted; // @0x414 EvaluateEjectPermission (@0049fa1c) result:
|
||||||
|
// 1 = crippled enough that the PANIC/EJECT
|
||||||
|
// punch-out is allowed (binary refreshes per
|
||||||
|
// frame from an unexported caller; the port
|
||||||
|
// evaluates on demand in the 0x19 handler)
|
||||||
|
int ejectMinWeapons; // @0x448 live-weapon floor for the evaluator's first
|
||||||
|
// clause. NO writer in the exported decomp --
|
||||||
|
// zero-filled in the binary, so the clause is
|
||||||
|
// inert there too [T3]; init 0 to match.
|
||||||
// (task #68) the authored look-view angles (model record +0x50..0x5c,
|
// (task #68) the authored look-view angles (model record +0x50..0x5c,
|
||||||
// ctor converts degrees->radians; binary @0x564..0x570 -- previously
|
// ctor converts degrees->radians; binary @0x564..0x570 -- previously
|
||||||
// parked in the Wword scratch bank). Consumed by the look-state
|
// parked in the Wword scratch bank). Consumed by the look-state
|
||||||
@@ -1013,6 +1032,17 @@ protected:
|
|||||||
Scalar gimpStrideLength; // @0x350
|
Scalar gimpStrideLength; // @0x350
|
||||||
AlarmIndicator legStateAlarm; // @0x39c
|
AlarmIndicator legStateAlarm; // @0x39c
|
||||||
int legAnimationState; // @0x3b0
|
int legAnimationState; // @0x3b0
|
||||||
|
int mapPosture; // binary @0x3f8 -- the map-legend/duck posture arbiter
|
||||||
|
// (0 none / 1 may-duck / 2 holding-squat), computed per
|
||||||
|
// master frame (FUN_004a9b5c @0x4a9f61-0x4aa007; the
|
||||||
|
// CROUCH reconstruction, 2026-08-05)
|
||||||
|
Scalar myomerEffectiveness; // binary @0x79c -- the master-perf myomer chain MAX
|
||||||
|
// (speedEffect@0x31C over the myomer subsystems),
|
||||||
|
// published LIVE each mapper update by mechmppr.cpp's
|
||||||
|
// drive-scale block (the 2026-07-31 seek-audit recon of
|
||||||
|
// the same @0x4a9cf2-0x4a9da4 bytes). Consumed by the
|
||||||
|
// crouch posture gate (mech4). Ctor 1.0 = pre-first-
|
||||||
|
// update default only.
|
||||||
Scalar gimpSpeedMax; // @0x52c
|
Scalar gimpSpeedMax; // @0x52c
|
||||||
Scalar standSpeed; // @0x530
|
Scalar standSpeed; // @0x530
|
||||||
Scalar walkStrideLength; // @0x534
|
Scalar walkStrideLength; // @0x534
|
||||||
@@ -1106,6 +1136,9 @@ protected:
|
|||||||
// from its exported consumers + the RP VTV::DeathShutdown analog) ---
|
// from its exported consumers + the RP VTV::DeathShutdown analog) ---
|
||||||
Logical IsMechDestroyed(); // damage-side death flag: graphicAlarm level >= 9
|
Logical IsMechDestroyed(); // damage-side death flag: graphicAlarm level >= 9
|
||||||
void UpdateDeathState(Scalar dt); // death freeze + subsystem DeathShutdown + wreck smoke
|
void UpdateDeathState(Scalar dt); // death freeze + subsystem DeathShutdown + wreck smoke
|
||||||
|
// --- #108 ghost-forensics accessors (PORT; the fields are protected) ---
|
||||||
|
void *DeathHandlerPtr() { return (void *)deathHandler; }
|
||||||
|
int WreckBuried() const { return collisionVolumeCount == 0; }
|
||||||
|
|
||||||
// --- clip loaders (mech3) -------------------------------------------
|
// --- clip loaders (mech3) -------------------------------------------
|
||||||
ResourceDescription::ResourceID *
|
ResourceDescription::ResourceID *
|
||||||
@@ -1237,6 +1270,29 @@ protected:
|
|||||||
enum { DuckRequestMessageID = 0x1a };
|
enum { DuckRequestMessageID = 0x1a };
|
||||||
void DuckRequestMessageHandler(ReceiverDataMessageOf<int> *message);
|
void DuckRequestMessageHandler(ReceiverDataMessageOf<int> *message);
|
||||||
|
|
||||||
|
// @0049f854 -- "EjectPilot" (Mech table id 0x19): the cockpit PANIC/
|
||||||
|
// EJECT punch-out (raw disasm 2026-08-02 -- the handler sat in the
|
||||||
|
// 0x49f854-0x49fa00 export gap). Press-only (msg+0xc > 0), gated on
|
||||||
|
// ejectPermitted (@0x414) and !IsDisabled(). Body: notify the operator
|
||||||
|
// console of the eject (FUN_004c198c ctor -> network sender; port core
|
||||||
|
// LOGS it, the relay wire is the tail) and latch the player's
|
||||||
|
// suppressConsole (+0x258) so the death that follows doesn't double-
|
||||||
|
// notify; graphicAlarm -> 10 (the EJECT state, one past death's 9);
|
||||||
|
// then SELF-dispatch a TakeDamageMessage {inflicting = SELF, zone -1,
|
||||||
|
// Explosive, amount = ScenarioRole::killBonus (role+0x1c -- field map
|
||||||
|
// byte-verified via the role reader @00429bec dest offsets + the ctor
|
||||||
|
// record copy), burst 0 (binary; our victim guard clamps to 1 --
|
||||||
|
// identical outcome: the charge is lethal)}. The punch-out IS a
|
||||||
|
// death: wreck, respawn cycle and replication all ride the normal
|
||||||
|
// damage chain.
|
||||||
|
enum { EjectPilotMessageID = 0x19 };
|
||||||
|
void EjectPilotMessageHandler(ReceiverDataMessageOf<int> *message);
|
||||||
|
// @0049fa1c -- the eject-permission evaluator (ejectPermitted @0x414):
|
||||||
|
// roster walk -- live weapons < ejectMinWeapons(@0x448) | zero live
|
||||||
|
// generators | HeatSinkBank coolant fraction < 0.05 (@0049fb50) |
|
||||||
|
// (leg-gimped state 3/4 AND novice, player simLive == 0).
|
||||||
|
int EvaluateEjectPermission();
|
||||||
|
|
||||||
// --- damage-routing support (mechdmg / mech4) -----------------------
|
// --- damage-routing support (mechdmg / mech4) -----------------------
|
||||||
// Typed access to the inherited Entity::damageZones[] (engine stores DamageZone*;
|
// Typed access to the inherited Entity::damageZones[] (engine stores DamageZone*;
|
||||||
// our entries are Mech__DamageZone, populated by the Mech ctor). Defined in
|
// our entries are Mech__DamageZone, populated by the Mech ctor). Defined in
|
||||||
|
|||||||
@@ -220,6 +220,16 @@ enum MechAnimationState
|
|||||||
void
|
void
|
||||||
Mech::SetLegAnimation(int state)
|
Mech::SetLegAnimation(int state)
|
||||||
{
|
{
|
||||||
|
// DIAG (BT_DUCK_LOG): every leg re-arm with the caller's return address --
|
||||||
|
// the crouch-clobber hunt (who re-arms after the squat parks?).
|
||||||
|
if (getenv("BT_DUCK_LOG"))
|
||||||
|
{
|
||||||
|
char dbuf[96];
|
||||||
|
sprintf(dbuf, "[duck] SetLegAnimation(%d) ra=btl4+0x%lx", state,
|
||||||
|
(unsigned long)_ReturnAddress()
|
||||||
|
- (unsigned long)GetModuleHandleA(0));
|
||||||
|
DEBUG_STREAM << dbuf << std::endl << std::flush;
|
||||||
|
}
|
||||||
legAnimation.SelectSequence( // FUN_004277a8(this+0x65c, ...)
|
legAnimation.SelectSequence( // FUN_004277a8(this+0x65c, ...)
|
||||||
animationClips[state], // *(this+0x5cc + state*4)
|
animationClips[state], // *(this+0x5cc + state*4)
|
||||||
// The real leg finished-callback PTR_LAB_0050d6f0 == FUN_004a6928
|
// The real leg finished-callback PTR_LAB_0050d6f0 == FUN_004a6928
|
||||||
@@ -520,6 +530,8 @@ Scalar
|
|||||||
return 0.0f;
|
return 0.0f;
|
||||||
// slot10 state 2 (0x4a6b37): SetLevel(1).
|
// slot10 state 2 (0x4a6b37): SetLevel(1).
|
||||||
case 2:
|
case 2:
|
||||||
|
if (getenv("BT_DUCK_LOG"))
|
||||||
|
DEBUG_STREAM << "[duck] squat clip parked (case 2 -> leg alarm 1)\n" << std::flush;
|
||||||
m->legStateAlarm.SetLevel(1); return 0.0f;
|
m->legStateAlarm.SetLevel(1); return 0.0f;
|
||||||
// slot2 (0x4a6b21) + slot1 state 32 (0x4a6b4d) + slot9 state 4 (0x4a6d6e): SetLevel(0).
|
// slot2 (0x4a6b21) + slot1 state 32 (0x4a6b4d) + slot9 state 4 (0x4a6d6e): SetLevel(0).
|
||||||
case 3: case 4: case 8: case 9: case 20: case 21:
|
case 3: case 4: case 8: case 9: case 20: case 21:
|
||||||
|
|||||||
+885
-76
File diff suppressed because it is too large
Load Diff
@@ -1193,8 +1193,21 @@ extern void BTSpawnDamageEffect(Mech *mech, int effect_resource, int segment_ind
|
|||||||
// unported "RemakeEntity" render state). Entity* param -> Mech* binds fine.
|
// unported "RemakeEntity" render state). Entity* param -> Mech* binds fine.
|
||||||
extern void BTRemakeMechModel(Entity *entity);
|
extern void BTRemakeMechModel(Entity *entity);
|
||||||
|
|
||||||
|
//
|
||||||
|
// #108 bridge: Mech::ReadUpdateRecord (mech.cpp, incomplete-type TU for the
|
||||||
|
// handler) stamps the ghost detector through this.
|
||||||
|
//
|
||||||
|
void
|
||||||
|
BTMechNoteUpdateApplied(void *mech_v)
|
||||||
|
{
|
||||||
|
Mech *m = (Mech *)mech_v;
|
||||||
|
if (m != 0 && m->DeathHandlerPtr() != 0)
|
||||||
|
((MechDeathHandler *)m->DeathHandlerPtr())->NoteUpdateApplied();
|
||||||
|
}
|
||||||
|
|
||||||
MechDeathHandler::MechDeathHandler(Mech *mech) // @0042a984
|
MechDeathHandler::MechDeathHandler(Mech *mech) // @0042a984
|
||||||
: owner(mech), prevMode(0)
|
: owner(mech), prevMode(0), ghostFrames(0), ghostLogged(0),
|
||||||
|
skatePrevX(0.0f), skatePrevZ(0.0f), skateFrames(0), skateLogged(0)
|
||||||
{
|
{
|
||||||
// per-zone last-damage cache, zeroed (binary this[0x10], size mech+0x11c).
|
// per-zone last-damage cache, zeroed (binary this[0x10], size mech+0x11c).
|
||||||
int count = (mech != 0 && mech->damageZoneCount > 0) ? mech->damageZoneCount : 0;
|
int count = (mech != 0 && mech->damageZoneCount > 0) ? mech->damageZoneCount : 0;
|
||||||
@@ -1217,6 +1230,93 @@ void
|
|||||||
if (owner == 0)
|
if (owner == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// #52 SKATE DETECTOR (PORT, ungated -- the night-12 forensic): a REPLICANT
|
||||||
|
// whose position advances while its LEG CHANNEL is not cycling is the
|
||||||
|
// field "skating" signature. One line per episode, carrying the three
|
||||||
|
// inputs that split the failure: bodyTargetSpeed (the replicated commanded
|
||||||
|
// speed the leg SM feeds from -- ~0 means the RECORD side starved the
|
||||||
|
// gait), legCycleSpeed (~0 with a live demand means the SM is STUCK), and
|
||||||
|
// IsMechDestroyed (1 means the gait feed block was SKIPPED -- the stale
|
||||||
|
// post-respawn flag suspect). ~90 frames of sustained skate to trip; the
|
||||||
|
// threshold 0.08 u/frame ~ 2.4 u/s at 30 fps, well above reckoner jitter.
|
||||||
|
if (owner->GetInstance() == Entity::ReplicantInstance)
|
||||||
|
{
|
||||||
|
const float px = (float)owner->localOrigin.linearPosition.x;
|
||||||
|
const float pz = (float)owner->localOrigin.linearPosition.z;
|
||||||
|
const float dx = px - skatePrevX, dz = pz - skatePrevZ;
|
||||||
|
const float step = sqrtf(dx * dx + dz * dz);
|
||||||
|
skatePrevX = px; skatePrevZ = pz;
|
||||||
|
// CHANNEL-AGNOSTIC (corrected same-day): the peer's joints are posed by
|
||||||
|
// the BODY channel in the current authentic architecture (mech4
|
||||||
|
// s_peerLegCh=0 -> AdvanceBodyAnimation mj=1), so legCycleSpeed==0 is
|
||||||
|
// NORMAL there -- the first detector build false-fired on every healthy
|
||||||
|
// movement phase. Skating = moving with BOTH channels idle.
|
||||||
|
const float cyc = fabsf((float)owner->legCycleSpeed)
|
||||||
|
+ fabsf((float)owner->bodyCycleSpeed);
|
||||||
|
const int movingNoLegs = (step > 0.08f && step < 5.0f // 5+: teleport/warp
|
||||||
|
&& cyc < 0.05f) ? 1 : 0;
|
||||||
|
if (movingNoLegs)
|
||||||
|
{
|
||||||
|
if (++skateFrames > 90 && !skateLogged)
|
||||||
|
{
|
||||||
|
skateLogged = 1;
|
||||||
|
DEBUG_STREAM << "[skate] replicant " << owner->GetEntityID()
|
||||||
|
<< " SKATING: " << skateFrames << " frames moving ("
|
||||||
|
<< step << " u/frame) with legCyc="
|
||||||
|
<< (float)owner->legCycleSpeed
|
||||||
|
<< " bodyCyc=" << (float)owner->bodyCycleSpeed
|
||||||
|
<< " bodyTargetSpeed=" << (float)owner->bodyTargetSpeed
|
||||||
|
<< " destroyed=" << (int)owner->IsMechDestroyed()
|
||||||
|
<< " mode=" << (int)owner->MovementMode()
|
||||||
|
<< " at (" << px << "," << pz << ")\n" << std::flush;
|
||||||
|
if (BTMatchLogActive())
|
||||||
|
BTMatchLog("SKATE", "mech=%d:%d frames=%d step=%.3f cyc=%.3f "
|
||||||
|
"cmdSpd=%.2f destroyed=%d mode=%d x=%.1f z=%.1f",
|
||||||
|
BTMatchHostOf(owner->GetEntityID()), (int)owner->GetEntityID(),
|
||||||
|
skateFrames, step, cyc,
|
||||||
|
(float)owner->bodyTargetSpeed,
|
||||||
|
(int)owner->IsMechDestroyed(), (int)owner->MovementMode(),
|
||||||
|
px, pz);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (skateFrames > 0)
|
||||||
|
{
|
||||||
|
if (skateLogged)
|
||||||
|
DEBUG_STREAM << "[skate] replicant " << owner->GetEntityID()
|
||||||
|
<< " recovered after " << skateFrames << " frames\n" << std::flush;
|
||||||
|
skateFrames = 0; skateLogged = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// #108 GHOST DETECTOR (PORT, ungated -- the night-9 forensic): a visible
|
||||||
|
// REPLICANT that stops receiving update records is exactly what "everyone
|
||||||
|
// shoots thin air" looks like from the other side. ReadUpdateRecord
|
||||||
|
// stamps arrivals (NoteUpdateApplied); ~20s of silence on an unburied
|
||||||
|
// replicant logs ONE [ghost] line per episode with the identity + last
|
||||||
|
// position, then re-arms when records resume. Frame-counted (Tick has no
|
||||||
|
// dt); 600 frames ~ 10-20s at field rates.
|
||||||
|
if (owner->GetInstance() == Entity::ReplicantInstance)
|
||||||
|
{
|
||||||
|
++ghostFrames;
|
||||||
|
if (ghostFrames > 600 && !ghostLogged
|
||||||
|
&& !owner->WreckBuried()) // buried wrecks are expected-silent
|
||||||
|
{
|
||||||
|
ghostLogged = 1;
|
||||||
|
DEBUG_STREAM << "[ghost] replicant " << owner->GetEntityID()
|
||||||
|
<< " has received NO update records for " << ghostFrames
|
||||||
|
<< " frames (mode " << (int)owner->MovementMode()
|
||||||
|
<< ") last pos (" << owner->localOrigin.linearPosition.x
|
||||||
|
<< "," << owner->localOrigin.linearPosition.z
|
||||||
|
<< ") -- stale-replicant / ghost signature (#108)\n" << std::flush;
|
||||||
|
if (BTMatchLogActive())
|
||||||
|
BTMatchLog("GHOST", "mech=%d:%d frames=%d mode=%d x=%.1f z=%.1f",
|
||||||
|
BTMatchHostOf(owner->GetEntityID()), (int)owner->GetEntityID(),
|
||||||
|
ghostFrames, (int)owner->MovementMode(),
|
||||||
|
(float)owner->localOrigin.linearPosition.x,
|
||||||
|
(float)owner->localOrigin.linearPosition.z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// REPLICANT UN-WRECK (#94): fire on the DEATH-STATE EXIT edge of the
|
// REPLICANT UN-WRECK (#94): fire on the DEATH-STATE EXIT edge of the
|
||||||
// replicated SimulationState dial (MovementMode 9/2 are the death modes;
|
// replicated SimulationState dial (MovementMode 9/2 are the death modes;
|
||||||
// they never revert except through Mech::Reset, whose SetMovementMode(0)
|
// they never revert except through Mech::Reset, whose SetMovementMode(0)
|
||||||
@@ -1239,15 +1339,25 @@ void
|
|||||||
{
|
{
|
||||||
extern void BTRebuildMechModel(Entity *entity);
|
extern void BTRebuildMechModel(Entity *entity);
|
||||||
extern void BTStartWarpEffect(float x, float y, float z);
|
extern void BTStartWarpEffect(float x, float y, float z);
|
||||||
|
// #129 (fresh spawns smoking, peers' screens only): the wreck plume
|
||||||
|
// is re-armed LOCALLY on every node (entity-attached, 10s windows),
|
||||||
|
// and only the master's Mech::Reset ran the @004d0c14 effect
|
||||||
|
// cleanup -- the observer's last window rode the teleport and kept
|
||||||
|
// puffing on the fresh mech for up to ~16s. Kill the entity's
|
||||||
|
// effects on the un-wreck edge, same as the master path.
|
||||||
|
extern void BTStopEntityPfx(void *entity_v);
|
||||||
|
BTStopEntityPfx((void *)owner);
|
||||||
BTRebuildMechModel((Entity *)owner);
|
BTRebuildMechModel((Entity *)owner);
|
||||||
BTStartWarpEffect((float)owner->localOrigin.linearPosition.x,
|
BTStartWarpEffect((float)owner->localOrigin.linearPosition.x,
|
||||||
(float)owner->localOrigin.linearPosition.y,
|
(float)owner->localOrigin.linearPosition.y,
|
||||||
(float)owner->localOrigin.linearPosition.z);
|
(float)owner->localOrigin.linearPosition.z);
|
||||||
if (getenv("BT_DEATH_LOG"))
|
// #108 forensics: UNGATED -- one line per peer respawn; a field
|
||||||
DEBUG_STREAM << "[respawn] replicant un-wrecked + warp (mode "
|
// night's ghost triage needs these without asking players for envs.
|
||||||
<< oldMode << "->" << mode << ") at ("
|
DEBUG_STREAM << "[respawn] replicant " << owner->GetEntityID()
|
||||||
<< owner->localOrigin.linearPosition.x << ","
|
<< " un-wrecked + warp (mode "
|
||||||
<< owner->localOrigin.linearPosition.z << ")\n" << std::flush;
|
<< oldMode << "->" << mode << ") at ("
|
||||||
|
<< owner->localOrigin.linearPosition.x << ","
|
||||||
|
<< owner->localOrigin.linearPosition.z << ")\n" << std::flush;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1275,6 +1385,20 @@ void
|
|||||||
continue;
|
continue;
|
||||||
lastLevel[i] = level;
|
lastLevel[i] = level;
|
||||||
|
|
||||||
|
// AUTHENTIC (FUN_0042aa2c @0042aa2c, the binary's effect-watcher level
|
||||||
|
// branch): when the rise CROSSES a band-descriptor threshold
|
||||||
|
// (FUN_0042a5f4) and this node owns the master, raise the entity's
|
||||||
|
// damage-zone update flag (*(entity+0x18) |= 2, master-gated on
|
||||||
|
// (entity+0x28 & 0xc) == 0) -- the zone update records ship this
|
||||||
|
// zone's damageLevel to every observer, so replicant dolls/hull tint
|
||||||
|
// track the fight at band granularity. This send was MISSING from
|
||||||
|
// the port (only the graphic-state branch below raised it): masters
|
||||||
|
// consumed damage correctly while every observer's copy sat at 0.0
|
||||||
|
// until destruction (the "no discoloration in MP" report, #87).
|
||||||
|
if (zone->DescriptorCrossed(prev, level)
|
||||||
|
&& owner->GetInstance() == Entity::MasterInstance)
|
||||||
|
owner->ForceUpdate(Entity::DamageZoneUpdateModelFlag);
|
||||||
|
|
||||||
// AUTHENTIC GATE (decomp re-verified 2026-07-12, workflow): the binary
|
// AUTHENTIC GATE (decomp re-verified 2026-07-12, workflow): the binary
|
||||||
// fires the CURRENT band descriptor's effect whenever the zone's
|
// fires the CURRENT band descriptor's effect whenever the zone's
|
||||||
// damage-CHANGED flag is set (+0xb8 & 4, set by DamageZone::TakeDamage,
|
// damage-CHANGED flag is set (+0xb8 & 4, set by DamageZone::TakeDamage,
|
||||||
@@ -1326,7 +1450,12 @@ void
|
|||||||
// The single-threaded frame loop makes the immediate swap safe.
|
// The single-threaded frame loop makes the immediate swap safe.
|
||||||
if (d->graphicState != DamageZone::ExistsGraphicState)
|
if (d->graphicState != DamageZone::ExistsGraphicState)
|
||||||
{
|
{
|
||||||
owner->ForceUpdate(Entity::DamageZoneUpdateModelFlag);
|
// Master-gated like the binary's gstate branch (@0042aa2c:
|
||||||
|
// (entity+0x28 & 0xc) == 0 guards the |= 2); the mesh swap
|
||||||
|
// runs on EVERY instance -- an observer node executes this
|
||||||
|
// same path when the replicated level/gstate lands.
|
||||||
|
if (owner->GetInstance() == Entity::MasterInstance)
|
||||||
|
owner->ForceUpdate(Entity::DamageZoneUpdateModelFlag);
|
||||||
BTRemakeMechModel(owner); // RemakeEntity: swap in the destroyed mesh
|
BTRemakeMechModel(owner); // RemakeEntity: swap in the destroyed mesh
|
||||||
}
|
}
|
||||||
if (getenv("BT_DEATH_LOG"))
|
if (getenv("BT_DEATH_LOG"))
|
||||||
@@ -1438,3 +1567,22 @@ void BTAmmoExplosionFanOut(
|
|||||||
((Mech *)owner_mech)->AmmoExplosionFanOut(
|
((Mech *)owner_mech)->AmmoExplosionFanOut(
|
||||||
(::Subsystem *)inflicting_subsystem, inflicting_subsystem_id, total_damage);
|
(::Subsystem *)inflicting_subsystem, inflicting_subsystem_id, total_damage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// #124 ZONE WALKER bridge: a zone's carrier SEGMENT index + name, complete-
|
||||||
|
// type TU (Mech__DamageZone). Replicant-safe -- the zone table streams on
|
||||||
|
// every instance, so the SHOOTER can enumerate its target's zones locally.
|
||||||
|
// Returns 0 past the end (the walker wraps on that).
|
||||||
|
//
|
||||||
|
int BTMechZoneSegAndName(void *mech_v, int zone_idx, int *seg_out, const char **name_out)
|
||||||
|
{
|
||||||
|
Mech *m = (Mech *)mech_v;
|
||||||
|
if (m == 0 || zone_idx < 0 || zone_idx >= m->damageZoneCount)
|
||||||
|
return 0;
|
||||||
|
Mech__DamageZone *dz = m->Zone(zone_idx);
|
||||||
|
if (dz == 0)
|
||||||
|
return 0;
|
||||||
|
*seg_out = dz->EffectSegmentIndex(); // public accessor (@0x194)
|
||||||
|
*name_out = (const char *)dz->damageZoneName; // engine base, public
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|||||||
@@ -363,6 +363,11 @@ class MechDeathHandler
|
|||||||
MechDeathHandler(Mech *mech); // @0042a984
|
MechDeathHandler(Mech *mech); // @0042a984
|
||||||
~MechDeathHandler(); // @0042a9f4
|
~MechDeathHandler(); // @0042a9f4
|
||||||
void Tick(); // @0042aa2c (the Performance)
|
void Tick(); // @0042aa2c (the Performance)
|
||||||
|
// #108 forensics (PORT, ungated): the replicant staleness detector.
|
||||||
|
// Mech::ReadUpdateRecord stamps every applied record; Tick counts
|
||||||
|
// frames since -- a visible replicant starved of records IS the ghost
|
||||||
|
// signature, logged once per episode with id + age + last position.
|
||||||
|
void NoteUpdateApplied() { ghostFrames = 0; ghostLogged = 0; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Mech *owner; // @0x14
|
Mech *owner; // @0x14
|
||||||
@@ -378,6 +383,14 @@ class MechDeathHandler
|
|||||||
// authentic un-wreck rides the type-0 graphic-state hook
|
// authentic un-wreck rides the type-0 graphic-state hook
|
||||||
// (Mech::ReadUpdateRecord case 0), which our port has not reconstructed.
|
// (Mech::ReadUpdateRecord case 0), which our port has not reconstructed.
|
||||||
int prevMode;
|
int prevMode;
|
||||||
|
// #108 ghost detector state (PORT)
|
||||||
|
int ghostFrames;
|
||||||
|
int ghostLogged;
|
||||||
|
// #52 skate detector state (PORT): position advancing while the leg
|
||||||
|
// channel is not cycling = the field "skating" signature.
|
||||||
|
float skatePrevX, skatePrevZ;
|
||||||
|
int skateFrames;
|
||||||
|
int skateLogged;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -840,6 +840,15 @@ void
|
|||||||
extern float gBTElevAxis;
|
extern float gBTElevAxis;
|
||||||
stickPosition.y = gBTElevAxis;
|
stickPosition.y = gBTElevAxis;
|
||||||
}
|
}
|
||||||
|
// #124 twist-sign bench probe: is the axis routed, and in what mode?
|
||||||
|
if (getenv("BT_TORSO_LOG"))
|
||||||
|
{
|
||||||
|
static int s_mp = 0;
|
||||||
|
if ((s_mp++ % 60) == 0 && s_mp < 1200)
|
||||||
|
DEBUG_STREAM << "[mppr] mode=" << (int)controlMode
|
||||||
|
<< " twistAxis=" << gBTTwistAxis
|
||||||
|
<< " keyTurn=" << key_turn << "\n" << std::flush;
|
||||||
|
}
|
||||||
if (controlMode == BasicMode)
|
if (controlMode == BasicMode)
|
||||||
{
|
{
|
||||||
stickPosition.x = key_turn;
|
stickPosition.x = key_turn;
|
||||||
@@ -1009,6 +1018,11 @@ void
|
|||||||
if (myomers > 0)
|
if (myomers > 0)
|
||||||
{
|
{
|
||||||
speedDemand *= drive; // @0x4a9d63: in-place demand scale
|
speedDemand *= drive; // @0x4a9d63: in-place demand scale
|
||||||
|
// The binary keeps the chain MAX in mech+0x79C (@0x4a9d00-0x4a9d39)
|
||||||
|
// -- publish it so the CROUCH gate (mech4 posture block, 2026-08-06)
|
||||||
|
// reads the same live factor (dead/overheated myomers cannot squat
|
||||||
|
// or rise; the posture selector tests |factor| <= 1e-4).
|
||||||
|
mech->myomerEffectiveness = drive;
|
||||||
if (fabsf(drive) <= 1.0e-4f) // @0x4a9d89 vs _DAT_004ab16c
|
if (fabsf(drive) <= 1.0e-4f) // @0x4a9d89 vs _DAT_004ab16c
|
||||||
turnDemand = 0.0f; // @0x4a9d9e: mapper+0x12C -- the FREEZE
|
turnDemand = 0.0f; // @0x4a9d9e: mapper+0x12C -- the FREEZE
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -287,6 +287,14 @@ void MechSubsystem::ForceCriticalFailure()
|
|||||||
{
|
{
|
||||||
((DamageZone *)damageZone)->SetDamageZoneState(1); // zone+0x10
|
((DamageZone *)damageZone)->SetDamageZoneState(1); // zone+0x10
|
||||||
}
|
}
|
||||||
|
// (A MarkGeneratorOut call lived here 2026-08-03 for a few hours --
|
||||||
|
// REMOVED same night when the alarm dig completed: the gauge-alarm
|
||||||
|
// conditions are STATUS-FLAG BITS scanned by MechTech, not stateAlarm
|
||||||
|
// levels. Generator death raises bit 6 (!HasVoltage) AUTHENTICALLY ->
|
||||||
|
// the gotoEngineering + engBusMode invite; conditions 4/5 (engEject)
|
||||||
|
// are the WEAPON's AmmoBurning/Jammed bits -- the ammo-purge invite,
|
||||||
|
// never a pilot-eject one. stateAlarm 4's real producer remains
|
||||||
|
// unfound (export holes) and nothing observable needs it.)
|
||||||
}
|
}
|
||||||
|
|
||||||
// issue #22 (PORT): the fresh-mech respawn repair -- the exact inverse of the
|
// issue #22 (PORT): the fresh-mech respawn repair -- the exact inverse of the
|
||||||
|
|||||||
@@ -341,6 +341,13 @@ class Damage;
|
|||||||
public:
|
public:
|
||||||
static Logical
|
static Logical
|
||||||
LookupStatusType(const char *name, int *out_value);
|
LookupStatusType(const char *name, int *out_value);
|
||||||
|
|
||||||
|
// EJECT-evaluator support (@0049fa1c bridges): the both-cells
|
||||||
|
// destroyed test (gotcha #22 -- the binary's ONE cell @0x40 is split
|
||||||
|
// into statusAlarm + simulationState here, so every cross-family
|
||||||
|
// destroyed check must read BOTH).
|
||||||
|
int SimulationStateCell() const { return simulationState; }
|
||||||
|
int StatusAlarmLevel() { return statusAlarm.GetLevel(); }
|
||||||
protected:
|
protected:
|
||||||
|
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|||||||
@@ -197,6 +197,15 @@ MechTech::MechTech(
|
|||||||
wooHooMinimumDuration = model->wooHooMinimumDuration; // res +0x34 -> this+0xF4
|
wooHooMinimumDuration = model->wooHooMinimumDuration; // res +0x34 -> this+0xF4
|
||||||
wooHooDurationRange = model->wooHooDurationRange; // res +0x38 -> this+0xF8
|
wooHooDurationRange = model->wooHooDurationRange; // res +0x38 -> this+0xF8
|
||||||
wooHooChance = model->wooHooChance; // res +0x3C -> this+0xFC
|
wooHooChance = model->wooHooChance; // res +0x3C -> this+0xFC
|
||||||
|
// DIAG (#119 leak-voice dig): the AUTHORED anti-spam latch tuning.
|
||||||
|
// No armer for the wooHoo latch exists anywhere in the flat export --
|
||||||
|
// if the authored chance is 0 the latch was dormant in 4.10 too and
|
||||||
|
// the repeating warning voice under multiple leaks is AUTHENTIC.
|
||||||
|
if (getenv("BT_LAMP_LOG"))
|
||||||
|
DEBUG_STREAM << "[techstat] wooHoo authored: minDur="
|
||||||
|
<< wooHooMinimumDuration << " range=" << wooHooDurationRange
|
||||||
|
<< " chance=" << wooHooChance
|
||||||
|
<< " alarmModel=" << alarmModel << "\n" << std::flush;
|
||||||
|
|
||||||
//
|
//
|
||||||
// Walk the owner's subsystem roster and attach a monitor to every
|
// Walk the owner's subsystem roster and attach a monitor to every
|
||||||
|
|||||||
@@ -740,15 +740,12 @@ void
|
|||||||
target->Dispatch(&message); // no manager: direct (bring-up)
|
target->Dispatch(&message); // no manager: direct (bring-up)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PORT bookkeeping (score lived in the retired mech4 fire block): credit
|
// SCORE: handled by the victim's own TakeDamage handler now -- the
|
||||||
// the delivered amount to the shooter-side score exactly as before.
|
// dispatch above lands in the authentic report tail (#45), which posts
|
||||||
extern void BTPostDamageScore(Entity *victim, Scalar damage);
|
// the kill/received reports itself. The shooter-side BTPostDamageScore
|
||||||
extern Logical BTIsRegisteredMech(Entity *e);
|
// call that lived here was RETIRED 2026-08-05 (per-hit inflicted credit
|
||||||
if (BTIsRegisteredMech(target)
|
// never existed in the 1995 pod; kills + received-damage penalties only).
|
||||||
&& !((Mech *)target)->IsMechDestroyed())
|
extern Logical BTIsRegisteredMech(Entity *e); // still read by the matchlog below
|
||||||
{
|
|
||||||
BTPostDamageScore(target, damageData.damageAmount);
|
|
||||||
}
|
|
||||||
|
|
||||||
// MP MATCH FORENSICS (matchlog.hpp): the shooter-side per-shot damage
|
// MP MATCH FORENSICS (matchlog.hpp): the shooter-side per-shot damage
|
||||||
// submission -- pairs with the victim's DMG application line. mech=1
|
// submission -- pairs with the victim's DMG application line. mech=1
|
||||||
@@ -994,3 +991,13 @@ void BTWeaponSetViewFireEnable(Subsystem *sub, int enable)
|
|||||||
if (sub != 0 && sub->IsDerivedFrom(MechWeapon::ClassDerivations))
|
if (sub != 0 && sub->IsDerivedFrom(MechWeapon::ClassDerivations))
|
||||||
((MechWeapon *)sub)->SetViewFireEnable(enable);
|
((MechWeapon *)sub)->SetViewFireEnable(enable);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #124 aspect probe: a weapon's physical muzzle WORLD position (the pod's own
|
||||||
|
// asymmetric geometry as a left/right anchor). -1-family returns 0.
|
||||||
|
int BTWeaponMuzzleWorld(Subsystem *sub, Point3D *out)
|
||||||
|
{
|
||||||
|
if (sub == 0 || out == 0 || !sub->IsDerivedFrom(MechWeapon::ClassDerivations))
|
||||||
|
return 0;
|
||||||
|
((MechWeapon *)sub)->MuzzlePoint(*out); // the public beam-render alias
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|||||||
@@ -360,6 +360,7 @@ class CockpitHud;
|
|||||||
int PipExtendedRange() const { return pipExtendedRange; }
|
int PipExtendedRange() const { return pipExtendedRange; }
|
||||||
void *WithinRangePtr() { return &targetWithinRange; }
|
void *WithinRangePtr() { return &targetWithinRange; }
|
||||||
void *WeaponAlarmPtr() { return &weaponAlarm; }
|
void *WeaponAlarmPtr() { return &weaponAlarm; }
|
||||||
|
int WeaponAlarmLevel() { return weaponAlarm.GetLevel(); } // @0x364 (eject evaluator: 7 = NoAmmo)
|
||||||
void *SimulationStatePtr() { return &simulationState; } // attr 1 ("SimulationState" -- damage state; 1 = Destroyed)
|
void *SimulationStatePtr() { return &simulationState; } // attr 1 ("SimulationState" -- damage state; 1 = Destroyed)
|
||||||
Scalar *RechargeLevelPtr() { return &rechargeLevel; } // the recharge-dial source (1.0 == fully charged); LIVE on BOTH families
|
Scalar *RechargeLevelPtr() { return &rechargeLevel; } // the recharge-dial source (1.0 == fully charged); LIVE on BOTH families
|
||||||
// (projectile weapons write it via slot 17 @004b9c9c — Gitea #12)
|
// (projectile weapons write it via slot 17 @004b9c9c — Gitea #12)
|
||||||
|
|||||||
@@ -762,35 +762,36 @@ void Myomers::MyomersDriveHeat(Scalar time_slice)
|
|||||||
// exists because the binary's per-tick value depends on the pod's frame
|
// exists because the binary's per-tick value depends on the pod's frame
|
||||||
// rate, which our port does not run at.
|
// rate, which our port does not run at.
|
||||||
//
|
//
|
||||||
// THE CLOCK ORIGIN, traced 2026-08-02 (was the open question here):
|
// THE CLOCK -- CLOSED 2026-08-02, all [T1] from bytes:
|
||||||
// the DOS binary keeps its engine frame rate in the global DAT_0052140c,
|
// * DAT_0052140c = SystemClock ticksPerSecond, 28.0 nominal (0x401ace) /
|
||||||
// set once at startup --
|
// 18.2065 BIOS fallback (0x401ada).
|
||||||
// 0x401ace: mov [0x52140c], 0x41E00000 = 28.0f (nominal)
|
// * The arcade Simulation::PerformAndWatch is FUN_0041c018:
|
||||||
// 0x401ada: mov [0x52140c], 0x4191A6E0 = 18.2065f (BIOS-tick fallback)
|
// slice = (till.ticks - lastPerformance.ticks) / DAT_0052140c;
|
||||||
// -- and consumed by ~90 fmul/fdiv sites across the image as THE
|
// lastPerformance = till; (*performance)(this, slice);
|
||||||
// per-frame<->per-second conversion. The DOS main passes it straight into
|
// -- VARIABLE-STEP, slice in real SECONDS, line-for-line the WinTesla
|
||||||
// the ApplicationManager ctor (0x401189: push [0x52140c]). So the pod's
|
// SIMULATE.cpp body. Every dt-carrying term is frame-rate independent
|
||||||
// nominal Performance cadence was 28 Hz, byte-proven. [T1]
|
// in BOTH the binary and the port; no unit conversion is missing.
|
||||||
|
// * This term's ONE caller is the unexported MyomersSimulation body
|
||||||
|
// (0x4b8b9a-0x4b8d0d, raw disasm 2026-08-02 -- the export gap hid it):
|
||||||
|
// it calls 0x4b8d18 once per Perform with the RAW slice, gated only on
|
||||||
|
// measuredVoltage > 0. So the kinetic energy pulse fires once per
|
||||||
|
// FRAME, and the pod's frame cadence is its tick rate: 28 Hz when
|
||||||
|
// holding the deadline, sagging under load (fewer pulses/s -- an
|
||||||
|
// EMERGENT hardware mercy, not a constant in the code).
|
||||||
//
|
//
|
||||||
// CALIBRATION FACTOR 0.5 -- the pod's EFFECTIVE rate under load, now
|
// The faithful reference is therefore the binary's own nominal cadence,
|
||||||
// corroborated from three directions (2026-08-02):
|
// 28 Hz, full stop. The old 0.5 "loaded-rate" calibration [T3 witness
|
||||||
// * Oracle (pod veteran), asked if the frame rate was rock solid:
|
// bracket] is RETIRED: it modeled the sag as a constant, which is wrong in
|
||||||
// "I think I could do as well with a flipbook"; heavy fights "would
|
// exactly the reported scenario (open-field cruising is a LIGHT scene --
|
||||||
// turn to a slide show".
|
// the pod held ~28 there). Measured at 28 Hz (cruise benches 2026-08-02):
|
||||||
// * Lynx (original-era): "RP had a target frame rate of 30FPS. It
|
// recommended gear equilibrates ~1450 (soft 20% output governor inside the
|
||||||
// wasn't hard coded or locked. BT never had a locked frame rate in
|
// authored degradation band, weapons unaffected, zero jams); supercharge
|
||||||
// 1st release testing... coming from 3.0, where 20FPS was a miracle."
|
// ~1770 (deep governor, the manual's own warning). That IS the authored
|
||||||
// * The engine's own pacer DESIGN: end_of_frame = Now() + frameDuration
|
// system: degradeT=1000 is a governor onset, failT=2000 the cliff, and the
|
||||||
// with background fill and NO overrun catch-up (APPMGR.cpp, T0) --
|
// pod's coolant-loop controls (flush/balance, Myomers on loop 5) are the
|
||||||
// "target 28 but allowed to slip" is the construction, and the
|
// player's lever. BT_MYO_HZ=<hz> remains as the bracketing override.
|
||||||
// 18.2065 BIOS fallback anticipated degraded timing.
|
|
||||||
// Corollary: a choking 486 generated LESS heat/s exactly during the
|
|
||||||
// biggest fights -- veteran memory of seek-4 endurance encodes the LOADED
|
|
||||||
// rate, so bracketing by their consensus is the correct instrument.
|
|
||||||
// Effective default 28 * 0.5 = 14 Hz [T2 multi-witness]; BT_MYO_HZ=<hz>
|
|
||||||
// overrides absolutely for bracketing.
|
|
||||||
static const Scalar kPodFrameHz = 28.0f; // byte-proven nominal [T1]
|
static const Scalar kPodFrameHz = 28.0f; // byte-proven nominal [T1]
|
||||||
static const Scalar kSeekHeatCalib = 0.5f; // Oracle bracket 2026-08-02 [T3]
|
static const Scalar kSeekHeatCalib = 1.0f; // faithful (0.5 bracket retired 2026-08-02)
|
||||||
static Scalar s_hz = -1.0f;
|
static Scalar s_hz = -1.0f;
|
||||||
if (s_hz < 0.0f)
|
if (s_hz < 0.0f)
|
||||||
{
|
{
|
||||||
@@ -800,7 +801,7 @@ void Myomers::MyomersDriveHeat(Scalar time_slice)
|
|||||||
if (getenv("BT_MYO_LOG"))
|
if (getenv("BT_MYO_LOG"))
|
||||||
DEBUG_STREAM << "[myoheat] kinetic-term reference rate: " << s_hz
|
DEBUG_STREAM << "[myoheat] kinetic-term reference rate: " << s_hz
|
||||||
<< " Hz (pod nominal 28 [T1] x calib " << kSeekHeatCalib
|
<< " Hz (pod nominal 28 [T1] x calib " << kSeekHeatCalib
|
||||||
<< " [T3]" << (hv ? ", ENV OVERRIDE" : "") << ")" << std::endl;
|
<< (hv ? ", ENV OVERRIDE" : "") << ")" << std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
Scalar termClimb = velComplement * vy * mass * gravity * time_slice;
|
Scalar termClimb = velComplement * vy * mass * gravity * time_slice;
|
||||||
|
|||||||
@@ -1305,10 +1305,10 @@ void
|
|||||||
{
|
{
|
||||||
// The THERMAL BREAKER: output collapses to 0 until the generator cools
|
// The THERMAL BREAKER: output collapses to 0 until the generator cools
|
||||||
// past NormalHeat and re-spins (task #11 diag below shows the duty cycle).
|
// past NormalHeat and re-spins (task #11 diag below shows the duty cycle).
|
||||||
if (stateAlarm.GetLevel() != GeneratorRecovered && getenv("BT_HEAT_LOG"))
|
if (stateAlarm.GetLevel() != GeneratorOut && getenv("BT_HEAT_LOG"))
|
||||||
DEBUG_STREAM << "[gen] " << GetName() << " TRIPPED (T="
|
DEBUG_STREAM << "[gen] " << GetName() << " TRIPPED (T="
|
||||||
<< currentTemperature << ")" << std::endl;
|
<< currentTemperature << ")" << std::endl;
|
||||||
stateAlarm.SetLevel(GeneratorRecovered); // SetLevel 4
|
stateAlarm.SetLevel(GeneratorOut); // SetLevel 4
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (stateAlarm.GetLevel()) // this[0x84] @0x210
|
switch (stateAlarm.GetLevel()) // this[0x84] @0x210
|
||||||
@@ -1337,7 +1337,7 @@ void
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case GeneratorRecovered: // 4
|
case GeneratorOut: // 4
|
||||||
outputVoltage = 0.0f;
|
outputVoltage = 0.0f;
|
||||||
if (heatAlarm.GetLevel() == HeatSink::NormalHeat) // this+0x184 == 0
|
if (heatAlarm.GetLevel() == HeatSink::NormalHeat) // this+0x184 == 0
|
||||||
{
|
{
|
||||||
@@ -1778,3 +1778,30 @@ int BTSubsystemIsGenerator(::Subsystem *sub)
|
|||||||
{
|
{
|
||||||
return (sub != 0 && sub->IsDerivedFrom(*Generator::GetClassDerivations())) ? 1 : 0;
|
return (sub != 0 && sub->IsDerivedFrom(*Generator::GetClassDerivations())) ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Destruction -> OUT display state (see Generator::MarkGeneratorOut, the [T3
|
||||||
|
// inferred link]): a destroyed generator must reach stateAlarm 4 or the
|
||||||
|
// authored gotoEngineering/engEject FLASH invite (alarm conditions 4/5) never
|
||||||
|
// fires and the eject evaluator's state clause never sees it. Called from
|
||||||
|
// MechSubsystem::ForceCriticalFailure (the one crit-death chokepoint).
|
||||||
|
void BTGeneratorMarkOut(::Subsystem *sub)
|
||||||
|
{
|
||||||
|
if (sub != 0 && sub->IsDerivedFrom(*Generator::GetClassDerivations()))
|
||||||
|
((Generator *)sub)->MarkGeneratorOut();
|
||||||
|
}
|
||||||
|
|
||||||
|
//===========================================================================//
|
||||||
|
// BTGeneratorCountsForEject -- complete-type bridge for the EJECT-permission
|
||||||
|
// evaluator (Mech::EvaluateEjectPermission @0049fa1c). Binary clause: classID
|
||||||
|
// 0xBC1, not destroyed (+0x40 != 1; both-cells here per gotcha #22), and
|
||||||
|
// stateAlarm level (@0x210) != 4. Returns -1 = not a generator, else 0/1.
|
||||||
|
//===========================================================================//
|
||||||
|
int BTGeneratorCountsForEject(Subsystem *sub)
|
||||||
|
{
|
||||||
|
if (sub == 0 || sub->GetClassID() != RegisteredClass::GeneratorClassID) // +4 == 0xbc1
|
||||||
|
return -1;
|
||||||
|
Generator *g = (Generator *)sub;
|
||||||
|
if (g->SimulationStateCell() == 1 || g->StatusAlarmLevel() == 1)
|
||||||
|
return 0; // destroyed
|
||||||
|
return (g->GeneratorStateOf() != 4) ? 1 : 0; // stateAlarm @0x210 != 4
|
||||||
|
}
|
||||||
|
|||||||
@@ -379,7 +379,12 @@ class Generator;
|
|||||||
GeneratorIdle = 1, // off / no output
|
GeneratorIdle = 1, // off / no output
|
||||||
GeneratorReady = 2, // producing RatedVoltage
|
GeneratorReady = 2, // producing RatedVoltage
|
||||||
GeneratorShorted = 3, // shorted; shortTimer -> shortRecoveryTime
|
GeneratorShorted = 3, // shorted; shortTimer -> shortRecoveryTime
|
||||||
GeneratorRecovered = 4 // TODO: confirm 4th/5th level names
|
GeneratorOut = 4 // generator DEAD/OUT: the eject evaluator counts
|
||||||
|
// stateAlarm==4 as not-live [T1 @0049fa1c], and the
|
||||||
|
// authored gauge-alarm streams flash gotoEngineering +
|
||||||
|
// engEject on conditions 4/5 [T1 content dump
|
||||||
|
// 2026-08-03] -- the "punch out now" invite.
|
||||||
|
// (Old name "GeneratorRecovered" was a placeholder.)
|
||||||
};
|
};
|
||||||
|
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
@@ -462,6 +467,20 @@ class Generator;
|
|||||||
// Source-state queries used by PoweredSubsystem / PowerWatcher.
|
// Source-state queries used by PoweredSubsystem / PowerWatcher.
|
||||||
int
|
int
|
||||||
GeneratorStateOf() { return stateAlarm.GetLevel(); } // source+0x210
|
GeneratorStateOf() { return stateAlarm.GetLevel(); } // source+0x210
|
||||||
|
|
||||||
|
// UNUSED -- and now PROVEN unnecessary (2026-08-04 dig): stateAlarm 4
|
||||||
|
// (GeneratorOut) is the THERMAL BREAKER state, produced by
|
||||||
|
// GeneratorSimulation itself (heatAlarm==FailureHeat -> SetLevel 4,
|
||||||
|
// byte-matched vs FUN_004b1f7c) -- never by destruction. The gauge
|
||||||
|
// alarms scan STATUS-FLAG BITS via MechTech (bit 6 = !HasVoltage);
|
||||||
|
// a generator destroyed IN PLACE keeps its stale Ready voltage in
|
||||||
|
// the BINARY TOO (the crit distributor @0049c9a8 touches nothing
|
||||||
|
// electrical) until any transition recomputes output through the
|
||||||
|
// (1 - damage) formula. A short-lived ForceCriticalFailure bridge
|
||||||
|
// to this method was removed as unfounded; keep the method only as
|
||||||
|
// documentation of the state's meaning.
|
||||||
|
void
|
||||||
|
MarkGeneratorOut() { stateAlarm.SetLevel(GeneratorOut); }
|
||||||
Scalar
|
Scalar
|
||||||
MeasuredVoltage() { return outputVoltage; } // source+0x1dc
|
MeasuredVoltage() { return outputVoltage; } // source+0x1dc
|
||||||
Scalar
|
Scalar
|
||||||
|
|||||||
@@ -268,6 +268,17 @@ void *
|
|||||||
? (void *)((ProjectileWeapon *)weapon)->ammoBinLink.Resolve() : 0;
|
? (void *)((ProjectileWeapon *)weapon)->ammoBinLink.Resolve() : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Eject-wipe gauge bridge (#118): the BallisticWeaponCluster's
|
||||||
|
// BitMapInverseWipeScalar watches the weapon's PercentOfEject (@0x3F8, the
|
||||||
|
// binary passes param_8+0x3f8 straight into the @004c61c8 ctor). Complete-
|
||||||
|
// type TU per the databinding rule -- never raw-offset a compiled object.
|
||||||
|
Scalar *
|
||||||
|
BTWeaponPercentOfEjectPtr(void *weapon)
|
||||||
|
{
|
||||||
|
return (weapon != 0)
|
||||||
|
? &((ProjectileWeapon *)weapon)->percentOfEject : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
// Construction / Destruction
|
// Construction / Destruction
|
||||||
@@ -1242,3 +1253,25 @@ Subsystem *CreateProjectileWeaponSubsystem(Mech *owner, int id, void *seg)
|
|||||||
return (Subsystem *) new (Memory::Allocate(0x448))
|
return (Subsystem *) new (Memory::Allocate(0x448))
|
||||||
ProjectileWeapon(owner, id, (ProjectileWeapon::SubsystemResource *)seg, ProjectileWeapon::DefaultData);
|
ProjectileWeapon(owner, id, (ProjectileWeapon::SubsystemResource *)seg, ProjectileWeapon::DefaultData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//===========================================================================//
|
||||||
|
// BTWeaponCountsForEject -- complete-type bridge for the EJECT-permission
|
||||||
|
// evaluator (Mech::EvaluateEjectPermission, @0049fa1c; issue: eject/panic).
|
||||||
|
// The binary's weapon clause: MechWeapon-derived (0x511830), not destroyed,
|
||||||
|
// and -- for the ProjectileWeapon family only (0x5121a8) -- weaponAlarm
|
||||||
|
// (@0x364) != 7 (NoAmmo): a dry ballistic rack is not a fighting weapon.
|
||||||
|
// Destroyed is the BOTH-CELLS test (gotcha #22: statusAlarm level + the
|
||||||
|
// port's split simulationState). Returns -1 = not a weapon, else 0/1.
|
||||||
|
//===========================================================================//
|
||||||
|
int BTWeaponCountsForEject(Subsystem *sub)
|
||||||
|
{
|
||||||
|
if (sub == 0 || !sub->IsDerivedFrom(MechWeapon::ClassDerivations))
|
||||||
|
return -1;
|
||||||
|
MechWeapon *w = (MechWeapon *)sub;
|
||||||
|
if (w->SimulationStateCell() == 1 || w->StatusAlarmLevel() == 1)
|
||||||
|
return 0; // destroyed (both cells)
|
||||||
|
if (sub->IsDerivedFrom(ProjectileWeapon::ClassDerivations)
|
||||||
|
&& ((ProjectileWeapon *)sub)->WeaponAlarmLevel() == 7)
|
||||||
|
return 0; // NoAmmo (@0x364 == 7)
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|||||||
@@ -360,6 +360,7 @@ class NotationFile;
|
|||||||
AmmoBinConnection
|
AmmoBinConnection
|
||||||
ammoBinLink; // @0x43C embedded 0xC connection to the AmmoBin subsystem
|
ammoBinLink; // @0x43C embedded 0xC connection to the AmmoBin subsystem
|
||||||
friend void *BTWeaponAmmoBin(void *weapon); // panel ammo-bin bridge (AFC100-counter fix)
|
friend void *BTWeaponAmmoBin(void *weapon); // panel ammo-bin bridge (AFC100-counter fix)
|
||||||
|
friend Scalar *BTWeaponPercentOfEjectPtr(void *weapon); // eject-wipe gauge bridge (#118)
|
||||||
// (ctor @004bcbb0, dtor @004bcbcf, link vtable
|
// (ctor @004bcbb0, dtor @004bcbcf, link vtable
|
||||||
// @00512424; resolved via the resource AmmoBin
|
// @00512424; resolved via the resource AmmoBin
|
||||||
// index at +0x1C0) -- LAST field, object ends 0x448
|
// index at +0x1C0) -- LAST field, object ends 0x448
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ Searchlight::SharedData
|
|||||||
//
|
//
|
||||||
// Chains to the PowerWatcher ctor (FUN_004b18a4) with &DAT_00511198, installs
|
// Chains to the PowerWatcher ctor (FUN_004b18a4) with &DAT_00511198, installs
|
||||||
// the Searchlight vtable (PTR_FUN_005114d4), builds the 2-level state
|
// the Searchlight vtable (PTR_FUN_005114d4), builds the 2-level state
|
||||||
// AlarmIndicator (@0x1E4) and seeds commandedOn from the segment field
|
// AlarmIndicator (@0x1E4) and seeds mountSegment from the segment field
|
||||||
// (+0x28). When this is a live (non-copy) segment with the has-performance
|
// (+0x28). When this is a live (non-copy) segment with the has-performance
|
||||||
// flag set it registers SearchlightSimulation (this[7..9] <- PTR_FUN_00511200);
|
// flag set it registers SearchlightSimulation (this[7..9] <- PTR_FUN_00511200);
|
||||||
// otherwise it marks the instance "no per-frame performance" (flag bit 2).
|
// otherwise it marks the instance "no per-frame performance" (flag bit 2).
|
||||||
@@ -103,7 +103,7 @@ Searchlight::Searchlight(
|
|||||||
lightState = 0; // @0x1D8
|
lightState = 0; // @0x1D8
|
||||||
requestedOn = 0; // @0x1E0
|
requestedOn = 0; // @0x1E0
|
||||||
stateAlarm.Initialize(2); // FUN_0041b9ec(this+0x1E4, 2)
|
stateAlarm.Initialize(2); // FUN_0041b9ec(this+0x1E4, 2)
|
||||||
commandedOn = subsystem_resource->segmentIndex; // @0x1DC <- inherited resource +0x28
|
mountSegment = subsystem_resource->segmentIndex; // @0x1DC <- inherited resource +0x28 (the spot-cone mount joint)
|
||||||
|
|
||||||
// GATE FIX (the functional bug the de-shim unblocks): the binary reads OWNER
|
// GATE FIX (the functional bug the de-shim unblocks): the binary reads OWNER
|
||||||
// simulationFlags (param_2+0x28, raw part_013.c:6012), NOT the old shadow
|
// simulationFlags (param_2+0x28, raw part_013.c:6012), NOT the old shadow
|
||||||
@@ -311,8 +311,8 @@ void
|
|||||||
if (!announced)
|
if (!announced)
|
||||||
{
|
{
|
||||||
announced = 1;
|
announced = 1;
|
||||||
DEBUG_STREAM << "[light] SearchlightSimulation RUNNING (commandedOn "
|
DEBUG_STREAM << "[light] SearchlightSimulation RUNNING (mountSegment "
|
||||||
<< commandedOn << ", requestedOn " << requestedOn
|
<< mountSegment << ", requestedOn " << requestedOn
|
||||||
<< ", lightState " << lightState << ")" << std::endl;
|
<< ", lightState " << lightState << ")" << std::endl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -323,7 +323,7 @@ void
|
|||||||
|
|
||||||
if (getenv("BT_FIRE_LOG") || getenv("BT_HEAT_LOG"))
|
if (getenv("BT_FIRE_LOG") || getenv("BT_HEAT_LOG"))
|
||||||
DEBUG_STREAM << "[light] reported lightState " << previous << " -> " << lightState
|
DEBUG_STREAM << "[light] reported lightState " << previous << " -> " << lightState
|
||||||
<< " (commandedOn " << commandedOn << ", requestedOn " << requestedOn << ")"
|
<< " (mountSegment " << mountSegment << ", requestedOn " << requestedOn << ")"
|
||||||
<< std::endl;
|
<< std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,7 +405,7 @@ int
|
|||||||
struct SearchlightLayoutCheck
|
struct SearchlightLayoutCheck
|
||||||
{
|
{
|
||||||
static_assert(offsetof(Searchlight, lightState) == 0x1D8, "Searchlight lightState @0x1D8 (PowerWatcher ends 0x1D8)");
|
static_assert(offsetof(Searchlight, lightState) == 0x1D8, "Searchlight lightState @0x1D8 (PowerWatcher ends 0x1D8)");
|
||||||
static_assert(offsetof(Searchlight, commandedOn) == 0x1DC, "Searchlight commandedOn @0x1DC (res+0x28)");
|
static_assert(offsetof(Searchlight, mountSegment) == 0x1DC, "Searchlight mountSegment @0x1DC (res+0x28 segmentIndex)");
|
||||||
static_assert(offsetof(Searchlight, requestedOn) == 0x1E0, "Searchlight requestedOn @0x1E0");
|
static_assert(offsetof(Searchlight, requestedOn) == 0x1E0, "Searchlight requestedOn @0x1E0");
|
||||||
static_assert(offsetof(Searchlight, stateAlarm) == 0x1E4, "Searchlight stateAlarm @0x1E4 (proves base ends 0x1D8)");
|
static_assert(offsetof(Searchlight, stateAlarm) == 0x1E4, "Searchlight stateAlarm @0x1E4 (proves base ends 0x1D8)");
|
||||||
static_assert(sizeof(Searchlight) <= 0x238, "sizeof(Searchlight) must fit the factory Memory::Allocate(0x238)");
|
static_assert(sizeof(Searchlight) <= 0x238, "sizeof(Searchlight) must fit the factory Memory::Allocate(0x238)");
|
||||||
@@ -423,3 +423,39 @@ Subsystem *CreateSearchlightSubsystem(Mech *owner, int id, void *seg)
|
|||||||
return (Subsystem *) new (Memory::Allocate(0x238))
|
return (Subsystem *) new (Memory::Allocate(0x238))
|
||||||
Searchlight(owner, id, (Searchlight::SubsystemResource *)seg, Searchlight::DefaultData);
|
Searchlight(owner, id, (Searchlight::SubsystemResource *)seg, Searchlight::DefaultData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//===========================================================================//
|
||||||
|
// Video-layer probe (2026-08-05, the spot-cone/fog reconstruction). The 1995
|
||||||
|
// MakeMechRenderables walk (@004cef28) hits `case 0xbd8` per roster subsystem
|
||||||
|
// and pulls the "LightOn" attribute + the mount joint; our port's tree builder
|
||||||
|
// does not walk the roster, so btl4vid calls this bridge instead (complete-
|
||||||
|
// Searchlight TU, per the databinding rule). Returns the number of
|
||||||
|
// searchlights found (0..2 reported -- the binary's cockpit branch keeps TWO
|
||||||
|
// attr slots, @004cef28 local_118/local_11c; more than 2 exist on no chassis).
|
||||||
|
//===========================================================================//
|
||||||
|
int BTMechSearchlightProbe(
|
||||||
|
void *mech_v,
|
||||||
|
int **light_on_out, // [2] "LightOn" attribute pointers (int*)
|
||||||
|
int *mount_seg_out) // [2] mount segment slots (resource segmentIndex)
|
||||||
|
{
|
||||||
|
Mech *mech = (Mech *)mech_v;
|
||||||
|
if (mech == 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int found = 0;
|
||||||
|
for (int i = 0; i < mech->GetSubsystemCount() && found < 2; ++i)
|
||||||
|
{
|
||||||
|
Subsystem *sub = mech->GetSubsystem(i);
|
||||||
|
if (sub == 0
|
||||||
|
|| (int)sub->GetClassID() != (int)RegisteredClass::SearchlightClassID) // 0xBD8
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Searchlight *light = (Searchlight *)sub;
|
||||||
|
light_on_out[found] = light->LightOnPointer(); // == GetAttributePointer("LightOn")
|
||||||
|
mount_seg_out[found] = light->MountSegmentIndex(); // @0x1DC (res+0x28 segmentIndex)
|
||||||
|
++found;
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|||||||
@@ -67,13 +67,13 @@
|
|||||||
//
|
//
|
||||||
// Empty extension of the PowerWatcher resource (CreateStreamedSubsystem
|
// Empty extension of the PowerWatcher resource (CreateStreamedSubsystem
|
||||||
// @004b85a8 reads no light-specific fields; it only stamps classID/model
|
// @004b85a8 reads no light-specific fields; it only stamps classID/model
|
||||||
// size). The ctor seeds commandedOn from the inherited segment field (+0x28).
|
// size). The ctor seeds mountSegment from the inherited segment field (+0x28).
|
||||||
//
|
//
|
||||||
struct Searchlight__SubsystemResource:
|
struct Searchlight__SubsystemResource:
|
||||||
public PowerWatcher::SubsystemResource
|
public PowerWatcher::SubsystemResource
|
||||||
{
|
{
|
||||||
// RESOURCE_AUDIT.md: Searchlight adds NO resource field. The ctor seeds
|
// RESOURCE_AUDIT.md: Searchlight adds NO resource field. The ctor seeds
|
||||||
// commandedOn from the INHERITED segmentIndex (resource +0x28); the old
|
// mountSegment from the INHERITED segmentIndex (resource +0x28); the old
|
||||||
// `int segmentDefault;` was appended after the base (compiled at ~0xF4, OOB)
|
// `int segmentDefault;` was appended after the base (compiled at ~0xF4, OOB)
|
||||||
// and read garbage. Empty extension = correct 0xF4 model size.
|
// and read garbage. Empty extension = correct 0xF4 model size.
|
||||||
};
|
};
|
||||||
@@ -121,13 +121,20 @@
|
|||||||
//
|
//
|
||||||
protected:
|
protected:
|
||||||
int lightState; // @0x1D8 reported on/off (gated by power + heat each frame); "LightState"
|
int lightState; // @0x1D8 reported on/off (gated by power + heat each frame); "LightState"
|
||||||
int commandedOn; // @0x1DC ctor-seeded from the subsystem resource +0x28 (@004b84dc).
|
int mountSegment; // @0x1DC ctor-seeded from the subsystem resource +0x28 (@004b84dc).
|
||||||
// NOT the toggle target and NOT read by @004b841c; it measured
|
// IDENTIFIED 2026-08-05 (was "commandedOn -- role still
|
||||||
// 21 live, so it is not a boolean -- role still unidentified.
|
// unidentified"): resource+0x28 IS segmentIndex, and the live
|
||||||
|
// value 21 is the searchlight's mount SEGMENT slot -- the joint
|
||||||
|
// the spot-cone renderable hangs from (MakeMechRenderables
|
||||||
|
// @004cef28 case 0xbd8). Not a boolean, not the toggle target.
|
||||||
int requestedOn; // @0x1E0 init 0; THE on/off input consumed by SearchlightSimulation
|
int requestedOn; // @0x1E0 init 0; THE on/off input consumed by SearchlightSimulation
|
||||||
// (@004b841c reads +0x1E0) and toggled by ToggleLamp @004b838c
|
// (@004b841c reads +0x1E0) and toggled by ToggleLamp @004b838c
|
||||||
BtAlarm stateAlarm; // @0x1E4 2-level light indicator (size 0x54)
|
BtAlarm stateAlarm; // @0x1E4 2-level light indicator (size 0x54)
|
||||||
|
|
||||||
|
public:
|
||||||
|
int MountSegmentIndex() const { return mountSegment; } // @0x1DC (see above)
|
||||||
|
int *LightOnPointer() { return &lightState; } // the "LightOn" attribute target
|
||||||
|
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
// BT segment / base-state compatibility shims (CROSS-FAMILY).
|
// BT segment / base-state compatibility shims (CROSS-FAMILY).
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -587,6 +587,48 @@ void
|
|||||||
static int s_sweep = 0;
|
static int s_sweep = 0;
|
||||||
analogTwistAxis = ((++s_sweep / 90) & 1) ? -1.0f : 1.0f; // +/- every ~90 frames
|
analogTwistAxis = ((++s_sweep / 90) & 1) ? -1.0f : 1.0f; // +/- every ~90 frames
|
||||||
}
|
}
|
||||||
|
// #124 twist-sign bench (BT_FORCE_TWIST=<-1..1>): HOLD the analog axis at a
|
||||||
|
// constant (the sweep above alternates) -- the sim integrates currentTwist
|
||||||
|
// to the per-mech limit and stays, giving the cylinder-table bench a known
|
||||||
|
// twist state at the SIM level, independent of the input plumbing.
|
||||||
|
{
|
||||||
|
static float s_forceTwist = -2.0f;
|
||||||
|
if (s_forceTwist < -1.5f)
|
||||||
|
{
|
||||||
|
const char *ft = getenv("BT_FORCE_TWIST");
|
||||||
|
s_forceTwist = ft ? (float)atof(ft) : -3.0f;
|
||||||
|
}
|
||||||
|
if (s_forceTwist >= -1.0f)
|
||||||
|
{
|
||||||
|
effectiveTwistRate = baseTwistRate;
|
||||||
|
analogTwistAxis = s_forceTwist;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// #131 LOCK-SWEEP (BT_LOCK_SWEEP=<axis 0..1>, default 0.12): pan the torso
|
||||||
|
// in a slow triangle wave between the twist limits, so the REAL boresight
|
||||||
|
// sweeps across a stand-off target and past both edges -- the operator-
|
||||||
|
// visible lock-envelope instrument (watch the ring light/die; the
|
||||||
|
// [locksweep] log in mech4 records the twist at each lock transition).
|
||||||
|
{
|
||||||
|
static float s_lockSweep = -1.0f;
|
||||||
|
if (s_lockSweep < 0.0f)
|
||||||
|
{
|
||||||
|
const char *lsw = getenv("BT_LOCK_SWEEP");
|
||||||
|
s_lockSweep = lsw ? (float)atof(lsw) : 0.0f;
|
||||||
|
if (lsw != 0 && s_lockSweep <= 0.0f) s_lockSweep = 0.12f;
|
||||||
|
if (s_lockSweep > 1.0f) s_lockSweep = 1.0f;
|
||||||
|
}
|
||||||
|
if (s_lockSweep > 0.0f)
|
||||||
|
{
|
||||||
|
effectiveTwistRate = baseTwistRate;
|
||||||
|
// flip direction at the limits (small margin so we visibly cross
|
||||||
|
// past the target and dwell OFF it at the extremes)
|
||||||
|
static float s_dir = 1.0f;
|
||||||
|
if (currentTwist >= horizontalLimitLeft - 0.05f) s_dir = -1.0f;
|
||||||
|
if (currentTwist <= horizontalLimitRight + 0.05f) s_dir = 1.0f;
|
||||||
|
analogTwistAxis = s_dir * s_lockSweep;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TORSO GATE PROBE (BT_TORSO_LOG): why is the twist rate zero?
|
// TORSO GATE PROBE (BT_TORSO_LOG): why is the twist rate zero?
|
||||||
if (getenv("BT_TORSO_LOG"))
|
if (getenv("BT_TORSO_LOG"))
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
# Phase 04 — The Export Gap Census (#60)
|
||||||
|
|
||||||
|
**Date:** 2026-08-06 · **Trigger:** export-gap blindness caused 4 wrong conclusions
|
||||||
|
(reconstruction-gotchas §20), 2 falsely [T1]. **Goal:** turn "we trip over gaps when we
|
||||||
|
arrive at them" into a known inventory.
|
||||||
|
|
||||||
|
## Method
|
||||||
|
`tools/gapcensus.py` (deterministic; re-run after any re-export):
|
||||||
|
1. **Index coverage** — `functions_index.tsv` (6,267 functions with sizes) interval-merged
|
||||||
|
against the PE `.text` extent → raw dark regions (bytes belonging to NO indexed function).
|
||||||
|
2. **Export coverage** — `/* @ADDR */` headers in `all/part_*.c` vs the index.
|
||||||
|
3. **Discovery** — function starts inside dark regions from E8 call targets + data-section
|
||||||
|
code pointers (vtables/handler tables/Performance pointers), with caller counts.
|
||||||
|
4. **Annotation** — pad-byte exclusion (CC/90/00), nearest TU attribution from `file=` tags,
|
||||||
|
and `visited` flags (address cited anywhere in game/ context/ docs/).
|
||||||
|
|
||||||
|
## Headline results
|
||||||
|
- `.text` = 892 KB. Index covers **87.3%**. Dark: **428 regions, 105 KB raw / 90 KB real code**.
|
||||||
|
- **Indexed-but-unexported = 0** — the entire gap class is "Ghidra never made it a function"
|
||||||
|
(consistent with #60's original find: VehicleDead was missing from the *index*, not filtered
|
||||||
|
from the export).
|
||||||
|
- 332 function starts discovered inside dark regions (call-graph + data-pointer evidence).
|
||||||
|
- **By TU family (real code):** munga/ 28.5 KB (engine — source in repo, LOW priority),
|
||||||
|
**bt/ 24.0 KB + bt_l4/ 14.9 KB + boundary 15.6 KB = ~54 KB of GAME-side dark**, munga_l4/ 9.5 KB.
|
||||||
|
- **Game-side dark regions: 66 already visited by past digs, 159 never touched.**
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
All six known dark celebrities land inside census regions: the VehicleDead handler
|
||||||
|
(`0x4c05c4`), Searchlight ToggleLamp (`0x4b838c`), the TakeDamage death tail (`0x4a07b8`,
|
||||||
|
inside the famous `0x49ffc8-0x4a1232` stretch — 89 repo citations), the master-perf region
|
||||||
|
(`0x4a9b5a-0x4ab188` — 69 citations; crouch/myomers/posture all came from here), the myomer
|
||||||
|
integrator (`0x4b8be3`), the duck consumer (`0x4aa011`). The two most-cited dark regions are
|
||||||
|
exactly the two that produced the most reconstructions — the census finds the right things.
|
||||||
|
|
||||||
|
## Top uncharted leads (zero citations, real code, spot-checked)
|
||||||
|
1. **`0x480a2a-0x487ca0` cluster (~9 KB across 6 regions), l4splr|btmssn boundary** — head
|
||||||
|
spot-check @0x486467: real `enter 0x3c` function driven by a global-index dispatch table
|
||||||
|
(`[0x4fef88]` → table `0x501588`) — mission-state-machine flavored. Likely the un-exported
|
||||||
|
heart of **BTMission** (round lifecycle / mode carousel).
|
||||||
|
2. **`0x4c08fc-0x4c0bc8` (613 B), btplayer, immediately before the BTPlayer ctor** — real
|
||||||
|
function reading `player+0x24c`; prime suspect for the deferred **mission-review post
|
||||||
|
(id 0x18)** sender and/or death-cycle siblings.
|
||||||
|
3. **`0x4d41ae-0x4ddc7c` btl4app regions (~2.6 KB)** — app/session plumbing (attract/config?).
|
||||||
|
4. **`0x4af691-0x4af9cf` (707 B), heat|mechmppr boundary** — heat-family tail or mapper head.
|
||||||
|
5. **`0x4bdd7b-0x4be078` (673 B), mechweap|btplayer boundary** — weapon-family virtuals.
|
||||||
|
|
||||||
|
## PART 2 — THE RE-EXPORT (same day, 2026-08-06): the dark map, cut in half
|
||||||
|
Installed (no admin, project tool convention `%LOCALAPPDATA%t411-tools`, beside DXSDK/cmake):
|
||||||
|
**JDK 21.0.12** + **Ghidra 12.1.2**. ⚠ Ghidra's launcher `.bat` expands `%JAVA_HOME%`
|
||||||
|
UNQUOTED, so the space in the user profile ("Joe Laptop") kills it — the runner uses 8.3 SHORT
|
||||||
|
paths (`JOELAP~1`, `BT411-~1`). Zips deleted after extract.
|
||||||
|
|
||||||
|
New tooling (committed, repeatable):
|
||||||
|
- `reference/ghidra_scripts/ExportGaps.java` — keeps ExportAll's EXACT output contract (shard
|
||||||
|
format, index columns) so every tool/grep/`@ADDR` citation still works, and adds the gap-fill
|
||||||
|
pass first: force disassembly + `createFunction` at (a) E8 call targets outside any function,
|
||||||
|
(b) data→code pointers at a plausible prologue, (c) the census's discovered starts — iterated
|
||||||
|
to a fixpoint. Logs what it created to `gapfill_report.tsv`.
|
||||||
|
- `tools/ghidra_reexport.sh` — headless runner (`reprocess` re-runs the script on the saved
|
||||||
|
project without re-analysis). `tools/gapdiff.py` — scores any two censused exports.
|
||||||
|
|
||||||
|
**Results (`tools/gapdiff.py`):**
|
||||||
|
| Metric | Before | After |
|
||||||
|
|---|---|---|
|
||||||
|
| Index coverage of .text | 87.3% | **93.5%** |
|
||||||
|
| Dark REAL code | 90.4 KB | **40.8 KB** (−49.5 KB, **54.8% recovered**) |
|
||||||
|
| Game-side dark (bt/ + bt_l4/) | 53.1 KB | **21.1 KB** (−32.0 KB) |
|
||||||
|
| Dark regions | 428 | 321 |
|
||||||
|
| Functions / pseudocode | 6,267 | **6,472** (+205, **0 decompile failures**) |
|
||||||
|
Gap fill converged in 2 rounds (205 created: 195 from census starts, 6 call targets, 4 data
|
||||||
|
pointers) covering 56.1 KB. Ghidra 12 auto-analysis alone also found 42 more than the old export.
|
||||||
|
|
||||||
|
**Every historically-bitten dark function now has pseudocode**, including `@0x4c05c4`
|
||||||
|
VehicleDead — the function whose absence OPENED this issue — plus ToggleLamp `@0x4b838c`, the
|
||||||
|
TakeDamage death tail, the master-perf region (crouch/myomers/posture), the myomer integrator,
|
||||||
|
and the `btplayer` hole `@0x4c0904`.
|
||||||
|
|
||||||
|
### Validation: the re-export CONFIRMS this week's hand reconstructions
|
||||||
|
The master-perf pseudocode matches the crouch transcription field-for-field —
|
||||||
|
`param_1[0xfe]`=mapPosture(0x3f8), `[0xe6]`=duckState(0x398), `[0x161]`=squatCapable,
|
||||||
|
`[0x1e7]`=myomerEffectiveness(0x79c), `[0x10]`=movementMode, the `player+0x25c` novice gate,
|
||||||
|
`FUN_004a7fc4(2/3)`=SetLegAnimation sqd/squ, `FUN_004a4c54(8)+(1)`=ForceUpdate, the
|
||||||
|
`+0x131` stability alarm, and the duckState clear. Independent confirmation of a raw-disasm
|
||||||
|
reconstruction, and of the census method that found it.
|
||||||
|
|
||||||
|
### …and it caught one thing the raw-disasm pass MISSED (fixed same day)
|
||||||
|
The hand pass stopped at `0x4aa0af`; the pseudocode tail carries an **airborne auto-rise**:
|
||||||
|
`if ((mode == 3 || mode == 4) && legState == 1) → SetLegAnimation(3) + ForceUpdate(8)+(1) +
|
||||||
|
stability 1` — a mech that leaves the ground while parked in the squat is forced up,
|
||||||
|
unconditional on duckState/squatCapable. Now in `mech4.cpp`; crouch re-benched un-regressed.
|
||||||
|
|
||||||
|
## Promotion + archive
|
||||||
|
The re-export is now the CANONICAL `reference/decomp/` (index, shards, vtables, census). The
|
||||||
|
previous export is preserved verbatim at **`reference/decomp/archive_2025export/`** so older
|
||||||
|
KB citations of the form `part_0NN.c:LINE` stay resolvable — ⚠ **line numbers and shard
|
||||||
|
membership shifted** in the new export (addresses did not). Cite `@ADDR` (stable), not
|
||||||
|
part/line, going forward.
|
||||||
|
|
||||||
|
## Remaining dark (40.8 KB) — the next leads
|
||||||
|
- `0x480a2a-0x487ca0` (~9 KB, l4splr|btmssn boundary) — still dark even after the fill: no call
|
||||||
|
or data reference reaches it, which itself is informative (dead code? data misparsed as code?
|
||||||
|
a jump-table-only region?). Worth a targeted look with the census's byte-scan.
|
||||||
|
- New from the re-export: **`@0x4c0904` is the MASTER BTPlayer Performance** — team resolution
|
||||||
|
by name ("Team ... does not exist"), the mission-end "Sending EndMission" console post, and
|
||||||
|
the periodic console score heartbeat (corroborating our hand-built one). Our port attributes
|
||||||
|
`@0x4c083c` as PlayerSimulation; that attribution needs a re-check.
|
||||||
|
- The remaining munga/munga_l4 dark (~19 KB) is low priority — we compile the real engine source.
|
||||||
|
|
||||||
|
## Systemic fix status — BOTH HALVES DONE
|
||||||
|
Inventory + re-export shipped. Rule (gotcha §20) stands and is now cheap to obey: check
|
||||||
|
`GAP_CENSUS.md` before any "absent from the export" claim; if still dark, byte-scan.
|
||||||
|
Re-run after any future re-export: `tools/ghidra_reexport.sh` → `tools/gapcensus.py` →
|
||||||
|
`tools/gapdiff.py`.
|
||||||
@@ -27,6 +27,16 @@ rem Clear any INHERITED BT_LOG: with it set the exe uses that path verbatim
|
|||||||
rem and, with no BT_LOG_APPEND, TRUNCATES it -- the exact evidence loss this
|
rem and, with no BT_LOG_APPEND, TRUNCATES it -- the exact evidence loss this
|
||||||
rem change removes. Empty forces the per-day self-named file + append.
|
rem change removes. Empty forces the per-day self-named file + append.
|
||||||
set BT_LOG=
|
set BT_LOG=
|
||||||
|
rem #108 FORENSICS (night-11): every field session records the match ledger
|
||||||
|
rem (matchlog also auto-arms on -net; forcing covers menu-relaunch paths) and
|
||||||
|
rem the score/death chain lines -- the ghost/K-D triage needs them from every
|
||||||
|
rem player, not just the ones we can reach for env setup.
|
||||||
|
set BT_MATCHLOG=1
|
||||||
|
set BT_SCORE_LOG=1
|
||||||
|
set BT_DEATH_LOG=1
|
||||||
|
rem #135: lamp/annunciator edges (techstat) -- one line per state change,
|
||||||
|
rem near-zero noise; would have answered the leak-no-flash report in one grep.
|
||||||
|
set BT_LAMP_LOG=1
|
||||||
cd content
|
cd content
|
||||||
rem JOIN-GAME menu (2026-07-22): pick your CALLSIGN + MECH, click JOIN --
|
rem JOIN-GAME menu (2026-07-22): pick your CALLSIGN + MECH, click JOIN --
|
||||||
rem your choice is sent to the operator and lands in the mission roster.
|
rem your choice is sent to the operator and lands in the mission roster.
|
||||||
|
|||||||
@@ -30,6 +30,16 @@ rem Clear any INHERITED BT_LOG: with it set the exe uses that path verbatim
|
|||||||
rem and, with no BT_LOG_APPEND, TRUNCATES it -- the exact evidence loss this
|
rem and, with no BT_LOG_APPEND, TRUNCATES it -- the exact evidence loss this
|
||||||
rem change removes. Empty forces the per-day self-named file + append.
|
rem change removes. Empty forces the per-day self-named file + append.
|
||||||
set BT_LOG=
|
set BT_LOG=
|
||||||
|
rem #108 FORENSICS (night-11): every field session records the match ledger
|
||||||
|
rem (matchlog also auto-arms on -net; forcing covers menu-relaunch paths) and
|
||||||
|
rem the score/death chain lines -- the ghost/K-D triage needs them from every
|
||||||
|
rem player, not just the ones we can reach for env setup.
|
||||||
|
set BT_MATCHLOG=1
|
||||||
|
set BT_SCORE_LOG=1
|
||||||
|
set BT_DEATH_LOG=1
|
||||||
|
rem #135: lamp/annunciator edges (techstat) -- one line per state change,
|
||||||
|
rem near-zero noise; would have answered the leak-no-flash report in one grep.
|
||||||
|
set BT_LAMP_LOG=1
|
||||||
cd content
|
cd content
|
||||||
rem JOIN-GAME menu (2026-07-22): pick your CALLSIGN + MECH, click JOIN --
|
rem JOIN-GAME menu (2026-07-22): pick your CALLSIGN + MECH, click JOIN --
|
||||||
rem your choice is sent to the operator and lands in the mission roster.
|
rem your choice is sent to the operator and lands in the mission roster.
|
||||||
|
|||||||
@@ -29,6 +29,16 @@ rem Clear any INHERITED BT_LOG: with it set the exe uses that path verbatim
|
|||||||
rem and, with no BT_LOG_APPEND, TRUNCATES it -- the exact evidence loss this
|
rem and, with no BT_LOG_APPEND, TRUNCATES it -- the exact evidence loss this
|
||||||
rem change removes. Empty forces the per-day self-named file + append.
|
rem change removes. Empty forces the per-day self-named file + append.
|
||||||
set BT_LOG=
|
set BT_LOG=
|
||||||
|
rem #108 FORENSICS (night-11): every field session records the match ledger
|
||||||
|
rem (matchlog also auto-arms on -net; forcing covers menu-relaunch paths) and
|
||||||
|
rem the score/death chain lines -- the ghost/K-D triage needs them from every
|
||||||
|
rem player, not just the ones we can reach for env setup.
|
||||||
|
set BT_MATCHLOG=1
|
||||||
|
set BT_SCORE_LOG=1
|
||||||
|
set BT_DEATH_LOG=1
|
||||||
|
rem #135: lamp/annunciator edges (techstat) -- one line per state change,
|
||||||
|
rem near-zero noise; would have answered the leak-no-flash report in one grep.
|
||||||
|
set BT_LAMP_LOG=1
|
||||||
cd content
|
cd content
|
||||||
rem ------------------------------------------------------------------
|
rem ------------------------------------------------------------------
|
||||||
rem OUR GENERATION ONLY (2026-07-27). The wait loop below used to hold
|
rem OUR GENERATION ONLY (2026-07-27). The wait loop below used to hold
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# GAP CENSUS -- the export dark-region inventory (#60)
|
||||||
|
|
||||||
|
Generated by `tools/gapcensus.py` (deterministic; re-run after any re-export).
|
||||||
|
Machine-readable twin: `gap_census.tsv`.
|
||||||
|
|
||||||
|
## Headline
|
||||||
|
| Metric | Value |
|
||||||
|
|---|---|
|
||||||
|
| Code section (.text) | `0x401000`-`0x4e0000` (892 KB) |
|
||||||
|
| Indexed functions (functions_index.tsv) | 6472, covering 833 KB (93.5%) |
|
||||||
|
| **RAW DARK (no indexed function at all)** | **321 regions >= 16B, 49 KB (5.5%) -- 40 KB REAL CODE after padding** |
|
||||||
|
| Exported pseudocode functions (part_*.c) | 6472 |
|
||||||
|
| **Indexed but NOT exported** (Ghidra knew it; no pseudocode) | **0 functions, 0 KB** |
|
||||||
|
| Function starts discovered inside dark regions | 135 (call-graph + data-pointer evidence) |
|
||||||
|
|
||||||
|
Reading the tables: `visited` = the address is already cited somewhere in
|
||||||
|
game/ context/ docs/ (a prior dig reached it); everything else is UNCHARTED.
|
||||||
|
`tu` = nearest file-tagged export neighbors (`before|after` when they disagree).
|
||||||
|
|
||||||
|
## Dark code by suspected TU family
|
||||||
|
|
||||||
|
| Family | Regions | Real code bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| munga/ (engine core -- source in repo) | 117 | 14544 |
|
||||||
|
| BOUNDARY/mixed | 53 | 10346 |
|
||||||
|
| bt/ (GAME -- reconstruction target) | 60 | 5848 |
|
||||||
|
| munga_l4/ (engine L4 -- source in repo) | 34 | 5563 |
|
||||||
|
| bt_l4/ (GAME video/glue -- reconstruction target) | 57 | 5492 |
|
||||||
|
|
||||||
|
## Top 40 dark regions by REAL CODE bytes (padding excluded)
|
||||||
|
|
||||||
|
| # | Range | Code bytes | Fn starts | Call-ins | Visited | Suspected TU |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| 1 | `0x4596ab`-`0x459eb8` | 1795 | 0 | 0 | 1: `4599ae` | munga_l4/l4vidrnd.cpp|munga_l4/l4video.cpp |
|
||||||
|
| 2 | `0x480a2a`-`0x48116d` | 1521 | 0 | 0 | - | munga_l4/l4splr.cpp|bt/btmssn.cpp |
|
||||||
|
| 3 | `0x40c82a`-`0x40cdb4` | 1322 | 0 | 0 | - | munga/scalar.cpp|munga/boxsolid.cpp |
|
||||||
|
| 4 | `0x40b68c`-`0x40bbe8` | 1293 | 0 | 0 | - | munga/scalar.cpp|munga/boxsolid.cpp |
|
||||||
|
| 5 | `0x481f77`-`0x48255c` | 1267 | 0 | 0 | - | munga_l4/l4splr.cpp|bt/btmssn.cpp |
|
||||||
|
| 6 | `0x486467`-`0x486a0e` | 1252 | 0 | 0 | - | munga_l4/l4splr.cpp|bt/btmssn.cpp |
|
||||||
|
| 7 | `0x47ae1b`-`0x47b2ec` | 1060 | 0 | 0 | - | munga_l4/l4ctrl.cpp|munga_l4/l4app.cpp |
|
||||||
|
| 8 | `0x405b1e`-`0x405f44` | 972 | 0 | 0 | - | munga/filestrm.hpp|munga/scalar.cpp |
|
||||||
|
| 9 | `0x427fd0`-`0x4283b8` | 931 | 0 | 0 | - | munga/network.cpp|munga/watcher.cpp |
|
||||||
|
| 10 | `0x4d41ae`-`0x4d45b0` | 765 | 0 | 0 | - | bt_l4/btl4app.cpp|? |
|
||||||
|
| 11 | `0x4af691`-`0x4af9cf` | 707 | 0 | 0 | - | bt/heat.cpp|bt/mechmppr.cpp |
|
||||||
|
| 12 | `0x4829ab`-`0x482c9f` | 687 | 0 | 0 | - | munga_l4/l4splr.cpp|bt/btmssn.cpp |
|
||||||
|
| 13 | `0x487a38`-`0x487ca0` | 615 | 0 | 0 | - | munga_l4/l4splr.cpp|bt/btmssn.cpp |
|
||||||
|
| 14 | `0x483b9b`-`0x483df0` | 520 | 0 | 0 | - | munga_l4/l4splr.cpp|bt/btmssn.cpp |
|
||||||
|
| 15 | `0x4d95b6`-`0x4d9800` | 487 | 0 | 0 | - | bt_l4/btl4app.cpp|? |
|
||||||
|
| 16 | `0x423202`-`0x423414` | 460 | 0 | 0 | - | munga/network.cpp|munga/watcher.cpp |
|
||||||
|
| 17 | `0x45472a`-`0x454924` | 451 | 0 | 0 | - | munga_l4/l4vidrnd.cpp |
|
||||||
|
| 18 | `0x4b8837`-`0x4b8a48` | 448 | 2 | 0 | 1: `4b8837` | bt/powersub.cpp|bt/mechweap.cpp |
|
||||||
|
| 19 | `0x474671`-`0x474855` | 421 | 0 | 0 | - | munga_l4/l4gauge.cpp|munga_l4/l4ctrl.cpp |
|
||||||
|
| 20 | `0x4d2813`-`0x4d29fc` | 414 | 1 | 0 | - | bt_l4/btl4vid.cpp|bt_l4/btl4mssn.cpp |
|
||||||
|
| 21 | `0x4816b2`-`0x48186d` | 404 | 0 | 0 | - | munga_l4/l4splr.cpp|bt/btmssn.cpp |
|
||||||
|
| 22 | `0x42f80b`-`0x42f9bc` | 394 | 0 | 0 | - | munga/player.cpp|munga/explode.cpp |
|
||||||
|
| 23 | `0x443cf5`-`0x443e94` | 380 | 0 | 0 | - | munga/watcher.hpp|munga/gauge.cpp |
|
||||||
|
| 24 | `0x4b2c11`-`0x4b2d8c` | 324 | 0 | 0 | 11: `4b2c2d` `4b2c75` `4b2d00` `4b2d23`... | bt/powersub.cpp|bt/mechweap.cpp |
|
||||||
|
| 25 | `0x4d993f`-`0x4d9c38` | 321 | 12 | 0 | - | bt_l4/btl4app.cpp|? |
|
||||||
|
| 26 | `0x4deec5`-`0x4df048` | 321 | 0 | 0 | - | bt_l4/btl4app.cpp|? |
|
||||||
|
| 27 | `0x4c651c`-`0x4c668d` | 318 | 0 | 0 | - | bt_l4/btl4gaug.cpp|bt_l4/btl4vid.cpp |
|
||||||
|
| 28 | `0x409c41`-`0x409d9c` | 312 | 0 | 0 | - | munga/scalar.cpp|munga/boxsolid.cpp |
|
||||||
|
| 29 | `0x484ed6`-`0x485020` | 307 | 0 | 0 | - | munga_l4/l4splr.cpp|bt/btmssn.cpp |
|
||||||
|
| 30 | `0x43223f`-`0x4323a8` | 306 | 0 | 0 | - | munga/explode.cpp|munga/registry.cpp |
|
||||||
|
| 31 | `0x40f6a7`-`0x40f7d8` | 283 | 0 | 0 | - | munga/boxsolid.cpp|munga/iterator.cpp |
|
||||||
|
| 32 | `0x4b1a84`-`0x4b1bb1` | 251 | 0 | 0 | - | bt/powersub.cpp|bt/mechweap.cpp |
|
||||||
|
| 33 | `0x481565`-`0x481675` | 247 | 0 | 0 | - | munga_l4/l4splr.cpp|bt/btmssn.cpp |
|
||||||
|
| 34 | `0x45888e`-`0x4589a0` | 241 | 0 | 0 | - | munga_l4/l4vidrnd.cpp |
|
||||||
|
| 35 | `0x4d2016`-`0x4d2150` | 241 | 0 | 0 | - | bt_l4/btl4vid.cpp|bt_l4/btl4mssn.cpp |
|
||||||
|
| 36 | `0x466ac2`-`0x466bc4` | 235 | 0 | 0 | - | munga_l4/l4audwtr.cpp|munga_l4/l4audres.cpp |
|
||||||
|
| 37 | `0x4310c3`-`0x4311dc` | 234 | 2 | 0 | - | munga/explode.cpp|munga/registry.cpp |
|
||||||
|
| 38 | `0x466702`-`0x466804` | 234 | 0 | 0 | - | munga_l4/l4audwtr.cpp|munga_l4/l4audres.cpp |
|
||||||
|
| 39 | `0x484076`-`0x48416d` | 229 | 0 | 0 | - | munga_l4/l4splr.cpp|bt/btmssn.cpp |
|
||||||
|
| 40 | `0x480899`-`0x4809ba` | 222 | 0 | 0 | - | munga_l4/l4splr.cpp|bt/btmssn.cpp |
|
||||||
|
|
||||||
|
## Indexed-but-unexported functions (first 60 by address)
|
||||||
|
|
||||||
|
These have index rows (address + size) but no pseudocode in `all/part_*.c` --
|
||||||
|
decompile them individually (raw disasm or a targeted re-export) when a dig arrives.
|
||||||
|
|
||||||
|
| Addr | Size | Index name | Cited in repo |
|
||||||
|
|---|---|---|---|
|
||||||
@@ -49,6 +49,24 @@ void __cdecl __GetExceptDLLinfo(undefined4 *param_1)
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* @00401074 file=? name=FUN_00401074 */
|
||||||
|
|
||||||
|
int FUN_00401074(void)
|
||||||
|
|
||||||
|
{
|
||||||
|
char *pcVar1;
|
||||||
|
int iVar2;
|
||||||
|
|
||||||
|
pcVar1 = (char *)FUN_004dee74(s_HEAPSIZE_004e0373);
|
||||||
|
if (pcVar1 == (char *)0x0) {
|
||||||
|
return 0x600000;
|
||||||
|
}
|
||||||
|
iVar2 = FUN_004dd1e0(pcVar1);
|
||||||
|
return iVar2;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* @0040109c file=? name=FUN_0040109c */
|
/* @0040109c file=? name=FUN_0040109c */
|
||||||
|
|
||||||
undefined4 __cdecl FUN_0040109c(int param_1,undefined4 param_2)
|
undefined4 __cdecl FUN_0040109c(int param_1,undefined4 param_2)
|
||||||
@@ -9601,6 +9619,43 @@ float * __cdecl FUN_0040a4d8(float *param_1,float *param_2,float *param_3,float
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* @0040a67c file=? name=FUN_0040a67c */
|
||||||
|
|
||||||
|
void __cdecl FUN_0040a67c(int *param_1,float *param_2)
|
||||||
|
|
||||||
|
{
|
||||||
|
float fVar1;
|
||||||
|
float fVar2;
|
||||||
|
float fVar3;
|
||||||
|
float fVar4;
|
||||||
|
int *piVar5;
|
||||||
|
byte bVar6;
|
||||||
|
byte bVar7;
|
||||||
|
byte bVar8;
|
||||||
|
byte bVar9;
|
||||||
|
|
||||||
|
bVar9 = 0x3e;
|
||||||
|
fVar1 = param_2[3];
|
||||||
|
bVar8 = 0x2c;
|
||||||
|
fVar2 = param_2[2];
|
||||||
|
bVar7 = 0x2c;
|
||||||
|
fVar3 = param_2[1];
|
||||||
|
bVar6 = 0x2c;
|
||||||
|
fVar4 = *param_2;
|
||||||
|
piVar5 = FUN_004db92c(param_1,0x3c);
|
||||||
|
piVar5 = FUN_004db634(piVar5,(float10)fVar4);
|
||||||
|
piVar5 = FUN_004db92c(piVar5,bVar6);
|
||||||
|
piVar5 = FUN_004db634(piVar5,(float10)fVar3);
|
||||||
|
piVar5 = FUN_004db92c(piVar5,bVar7);
|
||||||
|
piVar5 = FUN_004db634(piVar5,(float10)fVar2);
|
||||||
|
piVar5 = FUN_004db92c(piVar5,bVar8);
|
||||||
|
piVar5 = FUN_004db634(piVar5,(float10)fVar1);
|
||||||
|
FUN_004db92c(piVar5,bVar9);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* @0040a738 file=? name=FUN_0040a738 */
|
/* @0040a738 file=? name=FUN_0040a738 */
|
||||||
|
|
||||||
undefined4 FUN_0040a738(void)
|
undefined4 FUN_0040a738(void)
|
||||||
@@ -9897,86 +9952,3 @@ float * __cdecl FUN_0040ac6c(float *param_1,float *param_2)
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* @0040ad2c file=? name=FUN_0040ad2c */
|
|
||||||
|
|
||||||
float * __cdecl FUN_0040ad2c(float *param_1,float *param_2)
|
|
||||||
|
|
||||||
{
|
|
||||||
float local_1c;
|
|
||||||
float local_18;
|
|
||||||
float local_14;
|
|
||||||
float local_10;
|
|
||||||
float local_c;
|
|
||||||
float local_8;
|
|
||||||
|
|
||||||
FUN_00408328(&local_c,param_2 + 1);
|
|
||||||
FUN_00408328(&local_14,param_2);
|
|
||||||
FUN_00408328(&local_1c,param_2 + 2);
|
|
||||||
*param_1 = local_c * local_14 * local_1c + local_10 * local_18;
|
|
||||||
param_1[4] = local_8 * local_1c;
|
|
||||||
param_1[8] = local_c * local_10 * local_1c - local_14 * local_18;
|
|
||||||
param_1[1] = local_c * local_14 * local_18 - local_10 * local_1c;
|
|
||||||
param_1[5] = local_8 * local_18;
|
|
||||||
param_1[9] = local_c * local_10 * local_18 + local_14 * local_1c;
|
|
||||||
param_1[2] = local_8 * local_14;
|
|
||||||
param_1[6] = -local_c;
|
|
||||||
param_1[10] = local_8 * local_10;
|
|
||||||
return param_1;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* @0040adec file=? name=FUN_0040adec */
|
|
||||||
|
|
||||||
/* WARNING: Globals starting with '_' overlap smaller symbols at the same address */
|
|
||||||
|
|
||||||
float * __cdecl FUN_0040adec(float *param_1,float *param_2)
|
|
||||||
|
|
||||||
{
|
|
||||||
float fVar1;
|
|
||||||
float fVar2;
|
|
||||||
float fVar3;
|
|
||||||
float fVar4;
|
|
||||||
float fVar5;
|
|
||||||
float fVar6;
|
|
||||||
float fVar7;
|
|
||||||
float fVar8;
|
|
||||||
float fVar9;
|
|
||||||
float fVar10;
|
|
||||||
float fVar11;
|
|
||||||
float fVar12;
|
|
||||||
float fVar13;
|
|
||||||
float fVar14;
|
|
||||||
float fVar15;
|
|
||||||
float fVar16;
|
|
||||||
|
|
||||||
fVar1 = *param_2;
|
|
||||||
fVar2 = param_2[1];
|
|
||||||
fVar3 = param_2[1];
|
|
||||||
fVar4 = param_2[2];
|
|
||||||
fVar5 = param_2[2];
|
|
||||||
fVar6 = *param_2;
|
|
||||||
fVar7 = param_2[3];
|
|
||||||
fVar8 = *param_2;
|
|
||||||
fVar9 = param_2[3];
|
|
||||||
fVar10 = param_2[1];
|
|
||||||
fVar11 = param_2[3];
|
|
||||||
fVar12 = param_2[2];
|
|
||||||
fVar13 = param_2[3] * param_2[3];
|
|
||||||
fVar14 = *param_2 * *param_2;
|
|
||||||
fVar15 = param_2[1] * param_2[1];
|
|
||||||
fVar16 = param_2[2] * param_2[2];
|
|
||||||
*param_1 = ((fVar13 + fVar14) - fVar15) - fVar16;
|
|
||||||
param_1[1] = (fVar1 * fVar2 - fVar11 * fVar12) * _DAT_0040aedc;
|
|
||||||
param_1[2] = (fVar5 * fVar6 + fVar9 * fVar10) * _DAT_0040aedc;
|
|
||||||
param_1[4] = (fVar11 * fVar12 + fVar1 * fVar2) * _DAT_0040aedc;
|
|
||||||
param_1[5] = ((fVar13 - fVar14) + fVar15) - fVar16;
|
|
||||||
param_1[6] = (fVar3 * fVar4 - fVar7 * fVar8) * _DAT_0040aedc;
|
|
||||||
param_1[8] = (fVar5 * fVar6 - fVar9 * fVar10) * _DAT_0040aedc;
|
|
||||||
param_1[9] = (fVar3 * fVar4 + fVar7 * fVar8) * _DAT_0040aedc;
|
|
||||||
param_1[10] = ((fVar13 - fVar14) - fVar15) + fVar16;
|
|
||||||
return param_1;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1717
-631
File diff suppressed because it is too large
Load Diff
+641
-1405
File diff suppressed because it is too large
Load Diff
+1600
-1058
File diff suppressed because it is too large
Load Diff
+1446
-1272
File diff suppressed because it is too large
Load Diff
+1385
-1204
File diff suppressed because it is too large
Load Diff
+1270
-1443
File diff suppressed because it is too large
Load Diff
+1585
-1870
File diff suppressed because it is too large
Load Diff
+2342
-4161
File diff suppressed because it is too large
Load Diff
+4239
-2305
File diff suppressed because it is too large
Load Diff
+3006
-1927
File diff suppressed because it is too large
Load Diff
+1927
-4586
File diff suppressed because it is too large
Load Diff
+5553
-5835
File diff suppressed because it is too large
Load Diff
+8703
-3999
File diff suppressed because it is too large
Load Diff
+4880
-6204
File diff suppressed because it is too large
Load Diff
+7241
-1515
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,81 @@
|
|||||||
|
# GAP CENSUS -- the export dark-region inventory (#60)
|
||||||
|
|
||||||
|
Generated by `tools/gapcensus.py` (deterministic; re-run after any re-export).
|
||||||
|
Machine-readable twin: `gap_census.tsv`.
|
||||||
|
|
||||||
|
## Headline
|
||||||
|
| Metric | Value |
|
||||||
|
|---|---|
|
||||||
|
| Code section (.text) | `0x401000`-`0x4e0000` (892 KB) |
|
||||||
|
| Indexed functions (functions_index.tsv) | 6267, covering 778 KB (87.3%) |
|
||||||
|
| **RAW DARK (no indexed function at all)** | **428 regions >= 16B, 105 KB (11.9%) -- 90 KB REAL CODE after padding** |
|
||||||
|
| Exported pseudocode functions (part_*.c) | 6267 |
|
||||||
|
| **Indexed but NOT exported** (Ghidra knew it; no pseudocode) | **0 functions, 0 KB** |
|
||||||
|
| Function starts discovered inside dark regions | 332 (call-graph + data-pointer evidence) |
|
||||||
|
|
||||||
|
Reading the tables: `visited` = the address is already cited somewhere in
|
||||||
|
game/ context/ docs/ (a prior dig reached it); everything else is UNCHARTED.
|
||||||
|
`tu` = nearest file-tagged export neighbors (`before|after` when they disagree).
|
||||||
|
|
||||||
|
## Dark code by suspected TU family
|
||||||
|
|
||||||
|
| Family | Regions | Real code bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| munga/ (engine core -- source in repo) | 150 | 28540 |
|
||||||
|
| bt/ (GAME -- reconstruction target) | 87 | 23954 |
|
||||||
|
| BOUNDARY/mixed | 59 | 15581 |
|
||||||
|
| bt_l4/ (GAME video/glue -- reconstruction target) | 80 | 14916 |
|
||||||
|
| munga_l4/ (engine L4 -- source in repo) | 52 | 9529 |
|
||||||
|
|
||||||
|
## Top 40 dark regions by REAL CODE bytes (padding excluded)
|
||||||
|
|
||||||
|
| # | Range | Code bytes | Fn starts | Call-ins | Visited | Suspected TU |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| 1 | `0x412e41`-`0x4146dc` | 5936 | 6 | 0 | - | munga/boxsolid.cpp|munga/iterator.cpp |
|
||||||
|
| 2 | `0x4a9b5a`-`0x4ab188` | 4957 | 1 | 0 | 69: `4a9b5a` `4a9b5c` `4a9b6c` `4a9bff`... | bt/mech2.cpp|bt/heat.cpp |
|
||||||
|
| 3 | `0x486467`-`0x487788` | 4265 | 0 | 0 | - | munga_l4/l4splr.cpp|bt/btmssn.cpp |
|
||||||
|
| 4 | `0x49ffc8`-`0x4a1232` | 4116 | 5 | 2 | 89: `49ffc8` `49ffcc` `49ffde` `49ffff`... | bt/btmssn.cpp|bt/mech.cpp |
|
||||||
|
| 5 | `0x4cdce5`-`0x4ceecc` | 3831 | 1 | 0 | 18: `4cdcf0` `4cdd75` `4cdd9d` `4cddc1`... | bt_l4/btl4vid.cpp |
|
||||||
|
| 6 | `0x4a68ce`-`0x4a71f4` | 1822 | 2 | 0 | 44: `4a6928` `4a6989` `4a69aa` `4a69d6`... | bt/mech2.cpp |
|
||||||
|
| 7 | `0x4596ab`-`0x459eb8` | 1795 | 0 | 0 | 1: `4599ae` | munga_l4/l4vidrnd.cpp|munga_l4/l4video.cpp |
|
||||||
|
| 8 | `0x480a2a`-`0x48116d` | 1521 | 0 | 0 | - | munga_l4/l4splr.cpp|bt/btmssn.cpp |
|
||||||
|
| 9 | `0x42f3fe`-`0x42f9bc` | 1341 | 1 | 0 | - | munga/player.cpp|munga/explode.cpp |
|
||||||
|
| 10 | `0x40c82a`-`0x40cdb4` | 1322 | 0 | 0 | - | munga/scalar.cpp|munga/boxsolid.cpp |
|
||||||
|
| 11 | `0x464bf6`-`0x4651ac` | 1312 | 2 | 0 | - | munga_l4/l4audio.cpp|munga_l4/l4audlvl.cpp |
|
||||||
|
| 12 | `0x40b68c`-`0x40bbe8` | 1293 | 0 | 0 | - | munga/scalar.cpp|munga/boxsolid.cpp |
|
||||||
|
| 13 | `0x481f77`-`0x48255c` | 1267 | 0 | 0 | - | munga_l4/l4splr.cpp|bt/btmssn.cpp |
|
||||||
|
| 14 | `0x47ae1b`-`0x47b2ec` | 1060 | 0 | 0 | - | munga_l4/l4ctrl.cpp|munga_l4/l4app.cpp |
|
||||||
|
| 15 | `0x4dd80a`-`0x4ddc7c` | 1030 | 0 | 0 | - | bt_l4/btl4app.cpp|? |
|
||||||
|
| 16 | `0x405b1e`-`0x405f44` | 972 | 0 | 0 | - | munga/filestrm.hpp|munga/scalar.cpp |
|
||||||
|
| 17 | `0x427fd0`-`0x4283b8` | 931 | 0 | 0 | - | munga/network.cpp|munga/watcher.cpp |
|
||||||
|
| 18 | `0x4cba73`-`0x4cbea0` | 904 | 1 | 0 | 1: `4cbaf0` | bt_l4/btl4gaug.cpp|bt_l4/btl4vid.cpp |
|
||||||
|
| 19 | `0x4be3cf`-`0x4be7c4` | 897 | 1 | 0 | 1: `4be474` | bt/mechweap.hpp|bt/btplayer.cpp |
|
||||||
|
| 20 | `0x4b297b`-`0x4b2d8c` | 894 | 1 | 1 | 30: `4b2980` `4b298c` `4b299e` `4b29bd`... | bt/powersub.cpp|bt/mechweap.cpp |
|
||||||
|
| 21 | `0x4da08f`-`0x4da44c` | 866 | 4 | 0 | - | bt_l4/btl4app.cpp|? |
|
||||||
|
| 22 | `0x4147f9`-`0x414b60` | 789 | 4 | 0 | - | munga/boxsolid.cpp|munga/iterator.cpp |
|
||||||
|
| 23 | `0x4d41ae`-`0x4d45b0` | 765 | 0 | 0 | - | bt_l4/btl4app.cpp|? |
|
||||||
|
| 24 | `0x412940`-`0x412c78` | 760 | 4 | 0 | - | munga/boxsolid.cpp|munga/iterator.cpp |
|
||||||
|
| 25 | `0x4af691`-`0x4af9cf` | 707 | 0 | 0 | - | bt/heat.cpp|bt/mechmppr.cpp |
|
||||||
|
| 26 | `0x4bc0fd`-`0x4bc3fc` | 693 | 1 | 0 | 6: `4bc100` `4bc104` `4bc136` `4bc194`... | bt/mechweap.hpp|bt/btplayer.cpp |
|
||||||
|
| 27 | `0x4829ab`-`0x482c9f` | 687 | 0 | 0 | - | munga_l4/l4splr.cpp|bt/btmssn.cpp |
|
||||||
|
| 28 | `0x4bdd7b`-`0x4be078` | 673 | 4 | 0 | - | bt/mechweap.hpp|bt/btplayer.cpp |
|
||||||
|
| 29 | `0x4105ff`-`0x4108b4` | 635 | 4 | 0 | - | munga/boxsolid.cpp|munga/iterator.cpp |
|
||||||
|
| 30 | `0x487a27`-`0x487ca0` | 632 | 0 | 0 | - | munga_l4/l4splr.cpp|bt/btmssn.cpp |
|
||||||
|
| 31 | `0x4221b9`-`0x42249c` | 631 | 2 | 0 | - | munga/network.cpp|munga/watcher.cpp |
|
||||||
|
| 32 | `0x4c08fc`-`0x4c0bc8` | 613 | 1 | 0 | - | bt/btplayer.cpp|bt_l4/btl4rdr.cpp |
|
||||||
|
| 33 | `0x4bbcfd`-`0x4bbfcc` | 607 | 2 | 0 | 41: `4bbd04` `4bbd12` `4bbd1b` `4bbd28`... | bt/mechweap.hpp|bt/btplayer.cpp |
|
||||||
|
| 34 | `0x4c05be`-`0x4c083c` | 552 | 1 | 0 | 22: `4c05c0` `4c05c4` `4c05df` `4c0611`... | bt/btplayer.cpp|bt_l4/btl4rdr.cpp |
|
||||||
|
| 35 | `0x4bfca1`-`0x4bfee0` | 535 | 1 | 0 | 1: `4bfcac` | bt/mechweap.hpp|bt/btplayer.cpp |
|
||||||
|
| 36 | `0x483b9b`-`0x483df0` | 520 | 0 | 0 | - | munga_l4/l4splr.cpp|bt/btmssn.cpp |
|
||||||
|
| 37 | `0x4b8837`-`0x4b8a8c` | 509 | 3 | 0 | 6: `4b8837` `4b8a48` `4b8a50` `4b8a5b`... | bt/powersub.cpp|bt/mechweap.cpp |
|
||||||
|
| 38 | `0x4c11b3`-`0x4c1408` | 499 | 4 | 0 | 3: `4c11b3` `4c1230` `4c13d4` | bt/btplayer.cpp|bt_l4/btl4rdr.cpp |
|
||||||
|
| 39 | `0x47b8e2`-`0x47bb20` | 488 | 1 | 0 | - | munga_l4/l4app.cpp|munga_l4/l4splr.cpp |
|
||||||
|
| 40 | `0x4d95b6`-`0x4d9800` | 487 | 0 | 0 | - | bt_l4/btl4app.cpp|? |
|
||||||
|
|
||||||
|
## Indexed-but-unexported functions (first 60 by address)
|
||||||
|
|
||||||
|
These have index rows (address + size) but no pseudocode in `all/part_*.c` --
|
||||||
|
decompile them individually (raw disasm or a targeted re-export) when a dig arrives.
|
||||||
|
|
||||||
|
| Addr | Size | Index name | Cited in repo |
|
||||||
|
|---|---|---|---|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user