From 46446f38702e937d5cfbeba04bd8822744bb75e3 Mon Sep 17 00:00:00 2001 From: arcattack Date: Fri, 17 Jul 2026 07:32:20 -0500 Subject: [PATCH 1/6] KB: pod-hardware readiness gaps -> open-questions (Phase 8 assessment) What's carried + plausibly functional vs what will need real work on modern hardware: RIO serial chain / RGB-splitter channel packing / multi-output selection / plasma driver all EXIST but are pod-untested; expected work = multi-head D3D9 fullscreen modernization, the folded-away rear sound card (4-speaker split), RIO protocol timing, and the undocumented pod bring-up. Co-Authored-By: Claude Opus 4.8 (1M context) --- context/open-questions.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/context/open-questions.md b/context/open-questions.md index 7d8ee17..e3cbad5 100644 --- a/context/open-questions.md +++ b/context/open-questions.md @@ -29,6 +29,32 @@ authentic path scoped. Remaining: bind the side/down look views to keys (only 'V' look-back is bound); pod look-button mappings for Phase 8. +## Pod-hardware readiness gaps (Phase 8 assessment, 2026-07-17) [[pod-hardware]] +Everything below EXISTS in the tree but has NEVER run against real pod hardware. Mechanism +status vs the pod's 7-display / RGB-splitter / dual-sound-card rig: +- **CARRIED + plausibly functional (untested on hardware):** + - RIO serial controls: real Win32 COM (`L4SERIAL` CreateFileA/SetCommState), the full chain + RIO AnalogReply → Ranger → ControlsManager → streamed `.CTL` mappings → mapper traced [T1]; + keyboard bridge disable = `BT_KEY_BRIDGE=0`. Pod look/config buttons are `.CTL` data-driven. + - RGB-splitter channel packing: `L4GraphicsPort::ChannelEnableID` (Red/Green/Blue/All + + transparent-zero variants) — each gauge draws into ONE color channel of the shared + 640x480x16 surface, feeding one monochrome CRT per channel through the splitter. The gauge + CONTENT is complete (same `L4GaugeRenderer` verified daily in the `BT_DEV_GAUGES` window). + - Multi-output selection: `SVGA16::BuildWindows` (a fullscreen D3D9 device per gauge/MFD + surface), adapter env `PRIMGAUGE`/`SECGAUGE`/`MFDGAUGE`/`MFDGAUGE2`/`SPANDISABLE`, MFD span + = width x2 across two displays; `content/SETENV.BAT` = the authentic pod env preset. + - The `L4PLASMA.cpp` serial plasma-display driver (`L4PLASMA=com2`). +- **EXPECTED TO NEED REAL WORK on modern hardware:** + 1. **Multi-head display management**: the original used TWO video cards; simultaneous + exclusive-fullscreen D3D9 devices on multiple heads of one modern GPU is fragile — likely + needs a borderless-window-per-head modernization of `BuildWindows`. + 2. **The REAR sound card is folded away**: the pod ran `AWE_FRONT`/`AWE_REAR` as two physical + cards for the 4-speaker front/rear split; our OpenAL opens ONE device (front/rear collapses + to stereo). Feasible fix: OpenAL Soft quad output (surround config) or a second device. + 3. Serial protocol timing/handshake vs a real RIO board (and the intercom path) — unverified. + 4. `-platform pod` deliberately does NOT auto-enable `L4GAUGE` (each surface needs its own + device); the pod bring-up procedure itself is undocumented → the Nick items above. + ## BINARY-COVERAGE AUDIT discoveries (2026-07-13, 6-agent decomp census) [T1 verified-uncited] New unaccounted functionality no prior list knew (addresses verified absent from game/+context/+docs): - **✅ `BTL4VideoRenderer::StartEntityEffectImplementation` @004d097c — DONE (2026-07-13) [T2].** From dda517b65c4186dc3077bd79446cd9f9272fd7ed Mon Sep 17 00:00:00 2001 From: arcattack Date: Fri, 17 Jul 2026 09:32:53 -0500 Subject: [PATCH 2/6] KB: mech paint audit -- per-pilot color/badge/patch substitution authored but unwired (the 'missing stripes/coloration' report) Co-Authored-By: Claude Fable 5 --- context/open-questions.md | 4 ++++ context/rendering.md | 30 ++++++++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/context/open-questions.md b/context/open-questions.md index e3cbad5..f027f97 100644 --- a/context/open-questions.md +++ b/context/open-questions.md @@ -644,6 +644,10 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR LIVENESS audit (does a live call path reach each reconstructed fn?) would catch this class. ## Rendering follow-ups (non-blocking) +- **Per-pilot mech PAINT (color/badge/patch) — art ships, port never engages it** (2026-07-17 audit; + full mechanics + fix path in [[rendering]] §Per-pilot mech PAINT). Wire egg color/badge/patch into + `SetupMaterialSubstitutionList`, apply `substituteMaterial` in `bgfload resolve()`, per-variant + mesh cache. This is the "missing warning stripes / coloration" report. - **.PFX effect-layer polish (deferred 2026-07-08; the layer itself is LIVE + verified).** The reconstructed BT particle layer (L4VIDEO.cpp, see its banner) renders the authentic .PFX content (fire + smoke, premultiplied blend, impact-frame oriented — weapon hits AND damage bands via diff --git a/context/rendering.md b/context/rendering.md index ecd0976..ed857ae 100644 --- a/context/rendering.md +++ b/context/rendering.md @@ -105,8 +105,34 @@ models is deferred (needs the board's hot-spot semantics). [T2] `.BSL` is a bit-sliced container (6 grayscale sub-images per file); the BMF `0x18 BITSLICE` tag picks the slice. Reading it as RGB overlaid 2-3 gray sheets = the "rainbow/graffiti" bug. Fixed: `image.cpp decodeBSL` slice-decodes; `bgfload` threads the 0x18 channel; a cross-library RAMP registry -resolves the mech-skin `softer` ramp (defined in OTHER libs). Every mech renders authentic gray-metal -paint now. See [[asset-formats]]. [T2] +resolves the mech-skin `softer` ramp (defined in OTHER libs). Every mech renders the authentic BASE +gray-metal skin now. See [[asset-formats]]. [T2] + +## Per-pilot mech PAINT (color/badge/patch substitution) — NOT WIRED (2026-07 audit) +The "missing warning stripes / coloration" report is the per-pilot paint layer, fully authored but +never engaged by the port. Mechanics (all [T1] from the BMF/BGF/vehicletable content + FUN_004d0cc0): +- Mech BGFs reference the BASE lib only (`blhskin:gen2_dz_rfoot_mtl` …). The binary rewrites lib + prefixes at load via the `vehicletable` resource `[substitute]` templates: `skin:` → `_%color%%serno%:` + (`blhskin:` → `blh_wht0:`) and `skin:lgo` → `<<` (emblem pages → + `EMBLEM0.BMF` `lgoN__dz_*` materials). Pilot egg values (color/badge/patch) map through the + `[color]/[badge]/[patch]` sections to the codes. +- The COLOR libs (e.g. `BLH_WHT0.BMF`) are byte-identical to the base lib EXCEPT they add tags + `0x23`(diffuse RGB) + `0x24`(ambient RGB) = the pilot color to the ~35 paintable `gen*/mech*_dz_*` + materials (base leaves them untinted). 7 colors: wht(.99,.99,.99) red(.90,.11,.10) grn(.63,.83,.52) + gry(.65,.60,.65) brn(.60,.44,.28) tan(.65,.55,.43) blk(0,0,0). All 8 sernos are byte-identical + (verified: 0 of 193 groups differ) — `%serno%` was future variety, never used. `SKIN.BMF` + (bare) == `SKIN0.BMF`. +- WARNING STRIPES are grayscale hazard-stripe art in the shared `GEN.BSL` pages (slice 1 = `gen2_tex`, + top-left), shown through the paintable-zone materials and tinted by the pilot color. `EMBLEM0.BMF` + carries `lgoN__dz_ld/rdleg` materials (8 patch tints incl. yellow .89,.73,0) over + `LGO.BSL` slices 0-5 (slice 5 = VGL winged logo). +- PORT GAP: `SetupMaterialSubstitutionList` (btl4vid.cpp:2094) feeds NULL egg names (Mech's + resourceNameA/B/C not yet safely readable), and `substituteMaterial` (L4VIDEO.cpp:2565) is consumed + only by the legacy .X loader (L4D3D.cpp:151) — NOT by `bgfload`'s `MaterialResolver::resolve`. So + every mech renders base untinted paint + the hardwired `lgo6` (VGL) emblem. Verified live 2026-07-17: + MP MadCat renders white/gray, no tint/emblem/stripes. FIX PATH: (a) wire real egg color/badge/patch + into the substitution list; (b) apply the substitution in `resolve()` (BGF path); (c) per-variant + mesh caching (BgfData is cached per filename — two pilots' colors must not share a baked mesh). [T2] ## The OLD-STYLE SPECIALFX layer (the weapon-impact action) — RECONSTRUCTED 2026-07-12 (firmware-decoded) The per-weapon impact visuals are fully AUTHORED in `content/BTDPL.INI` `[effects_to_load]`: From e0474ff92ad5b3966e183adae524f507234f355b Mon Sep 17 00:00:00 2001 From: arcattack Date: Fri, 17 Jul 2026 09:50:23 -0500 Subject: [PATCH 3/6] Per-pilot mech paint: wire the color/badge/patch substitution end-to-end - Mech::resourceNameA/B/C -> real CString members (the binary's 16-byte CStringRepresentation; deep-copy bind from the MakeMessage = FUN_00402a98, implicit member dtors) + paint-name accessors - SetupMaterialSubstitutionList reads the real egg names ([paint] log); TearDown clears the callback first (FUN_004d11e8) - dpl_SetMaterialNameCallback is real now (L4VIDEO registry); bgfload MaterialResolver::resolve() applies it to every material name -- the port analogue of the dpl board rewriting names at load - MP_BHMC.EGG: color=Red -> Crimson (vehicletable has no Red; binary Fail()ed) Verified live 2-node MP: crimson MadCat with hip hazard stripes + yellow VGL leg emblems; white Blackhawk + emblems; replicants painted on both nodes. Co-Authored-By: Claude Fable 5 --- content/MP_BHMC.EGG | 244 +++++++++++++++++++++++++++++++++ context/open-questions.md | 8 +- context/rendering.md | 23 +++- engine/MUNGA_L4/L4VIDEO.cpp | 33 +++++ engine/MUNGA_L4/L4VIDEO.h | 7 + engine/MUNGA_L4/bgfload.cpp | 11 +- game/reconstructed/btl4vid.cpp | 42 +++--- game/reconstructed/btstubs.cpp | 16 +-- game/reconstructed/mech.cpp | 25 ++-- game/reconstructed/mech.hpp | 26 +++- 10 files changed, 382 insertions(+), 53 deletions(-) create mode 100644 content/MP_BHMC.EGG diff --git a/content/MP_BHMC.EGG b/content/MP_BHMC.EGG new file mode 100644 index 0000000..ff91cb1 --- /dev/null +++ b/content/MP_BHMC.EGG @@ -0,0 +1,244 @@ +[mission] +adventure=BattleTech +map=cavern +scenario=freeforall +time=night +weather=clear +temperature=27 +length=600 +[ordinals] +bitmap=Ordinal::BitMap::1 +bitmap=Ordinal::BitMap::2 +bitmap=Ordinal::BitMap::3 +bitmap=Ordinal::BitMap::4 +[Ordinal::BitMap::1] +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=000038000003C000001FF00000000F00 +bitmap=0000F8000003C000003FF80000000F00 +bitmap=0001F8000003C00000707C0000000F00 +bitmap=0001F8000003C00000603C0000000F00 +bitmap=00007801FC0FF00000003C3DF807FF00 +bitmap=00007803FE0FF00000003C3FFC0FFF00 +bitmap=00007803C703C00000003C3E3C1F0F00 +bitmap=000078078303C0000000783C1E1E0F00 +bitmap=000078078003C0000000783C1E1E0F00 +bitmap=00007807C003C0000000F03C1E1E0F00 +bitmap=00007807F003C0000001E03C1E1E0F00 +bitmap=00007803FC03C0000003C03C1E1E0F00 +bitmap=00007801FE03C0000007803C1E1E0F00 +bitmap=000078007F03C000000F003C1E1E0F00 +bitmap=000078001F03C000001E003C1E1E0F00 +bitmap=000078000F03C000003C003C1E1E0F00 +bitmap=000078060F03C0000078003C1E1E0F00 +bitmap=000078071E03E0000078003C1E1F1F00 +bitmap=00007803FE01F000007FFC3C1E0FFF00 +bitmap=00007801FC00F000007FFC3C1E07EF00 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +x=128 +y=32 +width=8 +[Ordinal::BitMap::2] +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=001FFF0000003C0000003C03C0780000 +bitmap=001FFF0000003C0000007C03C0780000 +bitmap=00000E0000003C000000FC03C0780000 +bitmap=00003C0000003C000001BC03C0780000 +bitmap=0000700F3E1FFC000003BC0FF07BF000 +bitmap=0001E00F7E3FFC0000073C0FF07FF800 +bitmap=0003800FFE7C3C0000063C03C07C7800 +bitmap=0007F80FFE783C00000C3C03C0783C00 +bitmap=0007FE0F80783C0000183C03C0783C00 +bitmap=00001E0F00783C0000383C03C0783C00 +bitmap=00000F0F00783C0000703C03C0783C00 +bitmap=00000F0F00783C00007FFF03C0783C00 +bitmap=00000F0F00783C00007FFF03C0783C00 +bitmap=00000F0F00783C0000003C03C0783C00 +bitmap=00000F0F00783C0000003C03C0783C00 +bitmap=00000F0F00783C0000003C03C0783C00 +bitmap=00180F0F00783C0000003C03C0783C00 +bitmap=001C1F0F007C7C0000003C03E0783C00 +bitmap=000FFE0F003FFC0000003C01F0783C00 +bitmap=0007FC0F001FBC0000003C00F0783C00 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +x=128 +y=32 +width=8 +[Ordinal::BitMap::3] +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=001FFF03C07800000000FC03C0780000 +bitmap=001FFF03C07800000003FC03C0780000 +bitmap=001E0003C07800000007C003C0780000 +bitmap=001E0003C0780000000F0003C0780000 +bitmap=001E000FF07BF000000F000FF07BF000 +bitmap=001E000FF07FF800001E000FF07FF800 +bitmap=001E0003C07C7800001E0003C07C7800 +bitmap=001FFC03C0783C00001EFC03C0783C00 +bitmap=001FFE03C0783C00001FFE03C0783C00 +bitmap=00001F03C0783C00001F1F03C0783C00 +bitmap=00000F03C0783C00001E0F03C0783C00 +bitmap=00000F03C0783C00001E0F03C0783C00 +bitmap=00000F03C0783C00001E0F03C0783C00 +bitmap=00000F03C0783C00001E0F03C0783C00 +bitmap=00000F03C0783C00001E0F03C0783C00 +bitmap=00000F03C0783C00001E0F03C0783C00 +bitmap=00180F03C0783C00001E0F03C0783C00 +bitmap=001C1F03E0783C00001F1F03E0783C00 +bitmap=000FFE01F0783C00000FFE01F0783C00 +bitmap=0007FC00F0783C000007FC00F0783C00 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +x=128 +y=32 +width=8 +[Ordinal::BitMap::4] +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=001FFF03C07800000007FC03C0780000 +bitmap=001FFF03C0780000000FFE03C0780000 +bitmap=00000F03C0780000001F1F03C0780000 +bitmap=00000F03C0780000001E0F03C0780000 +bitmap=00000F0FF07BF000001E0F0FF07BF000 +bitmap=00001F0FF07FF800001E0F0FF07FF800 +bitmap=00001E03C07C7800001E0F03C07C7800 +bitmap=00003E03C0783C00001E0F03C0783C00 +bitmap=00003C03C0783C00000F1E03C0783C00 +bitmap=00003C03C0783C000007FC03C0783C00 +bitmap=00007803C0783C000007FC03C0783C00 +bitmap=00007803C0783C00000F1E03C0783C00 +bitmap=00007803C0783C00001E0F03C0783C00 +bitmap=0000F003C0783C00001E0F03C0783C00 +bitmap=0000F003C0783C00001E0F03C0783C00 +bitmap=0000F003C0783C00001E0F03C0783C00 +bitmap=0000F003C0783C00001E0F03C0783C00 +bitmap=0000F003E0783C00000F1E03E0783C00 +bitmap=0000F001F0783C00000FFE01F0783C00 +bitmap=0000F000F0783C000007FC00F0783C00 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +x=128 +y=32 +width=8 +[pilots] +pilot=127.0.0.1:1502 +pilot=127.0.0.1:1602 +[127.0.0.1:1502] +hostType=0 +advancedDamage=1 +loadzones=1 +name=Aeolus +bitmapindex=1 +experience=expert +badge=VGL +patch=Yellow +role=Role::Default +dropzone=one +vehicle=bhk1 +vehicleValue=1000 +color=White +[127.0.0.1:1602] +hostType=0 +advancedDamage=1 +loadzones=1 +name=Boreas +bitmapindex=2 +experience=expert +badge=VGL +patch=Yellow +role=Role::Default +dropzone=one +vehicle=madcat +vehicleValue=1000 +color=Crimson +[largebitmap] +bitmap=BitMap::Large::Aeolus +[BitMap::Large::Aeolus] +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=000003FE0000000001E0000000000000 +bitmap=000007FF0000000001E0000000000000 +bitmap=00000F8F8000000001E0000000000000 +bitmap=00000F078000000001E0000000000000 +bitmap=00000F0781FF00FF81E0F0781FC00000 +bitmap=00000F0783FF81FFC1E0F0783FE00000 +bitmap=00000F0787C7C3E3E1E0F0783C700000 +bitmap=00000F078783C3C1E1E0F07878300000 +bitmap=00000F078783C3C1E1E0F07878000000 +bitmap=00000FFF8783C3C1E1E0F0787C000000 +bitmap=00000FFF8783C3C1E1E0F0787F000000 +bitmap=00000F0787FFC3C1E1E0F0783FC00000 +bitmap=00000F0787FFC3C1E1E0F0781FE00000 +bitmap=00000F07878003C1E1E0F07807F00000 +bitmap=00000F07878003C1E1E0F07801F00000 +bitmap=00000F07878003C1E1E0F07800F00000 +bitmap=00000F078780C3C1E1E0F07860F00000 +bitmap=00000F0787C1C3E3E1E078F871E00000 +bitmap=00000F0783FF81FFC1E07FF83FE00000 +bitmap=00000F0781FF00FF81E03F781FC00000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +x=128 +y=32 +width=8 +[smallbitmap] +bitmap=BitMap::Small::Aeolus +[BitMap::Small::Aeolus] +bitmap=00000000000000000000000000000000 +bitmap=0000000000000000003E00000C000000 +bitmap=006300000C00000000631F0F8CC63C00 +bitmap=00633198CCC66200007F3198CCC66000 +bitmap=00633F98CCC6780000633018CCC61E00 +bitmap=00633018CCC6060000633098CCC64600 +bitmap=00631F0F8C7E3C000000000000000000 +bitmap=00000000000000000000000000000000 +x=64 +y=16 +width=4 +[Role::Default] +model=dfltrole +[Role::NoReturn] +model=noretun diff --git a/context/open-questions.md b/context/open-questions.md index f027f97..57f6955 100644 --- a/context/open-questions.md +++ b/context/open-questions.md @@ -644,10 +644,10 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR LIVENESS audit (does a live call path reach each reconstructed fn?) would catch this class. ## Rendering follow-ups (non-blocking) -- **Per-pilot mech PAINT (color/badge/patch) — art ships, port never engages it** (2026-07-17 audit; - full mechanics + fix path in [[rendering]] §Per-pilot mech PAINT). Wire egg color/badge/patch into - `SetupMaterialSubstitutionList`, apply `substituteMaterial` in `bgfload resolve()`, per-variant - mesh cache. This is the "missing warning stripes / coloration" report. +- ~~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 + tables in [[rendering]] §Per-pilot mech PAINT. Residual nit: eggs saying `color=Red` mean + **Crimson** (table has no Red; the binary Fail()ed on it, the port warns + drops). - **.PFX effect-layer polish (deferred 2026-07-08; the layer itself is LIVE + verified).** The reconstructed BT particle layer (L4VIDEO.cpp, see its banner) renders the authentic .PFX content (fire + smoke, premultiplied blend, impact-frame oriented — weapon hits AND damage bands via diff --git a/context/rendering.md b/context/rendering.md index ed857ae..b5852b3 100644 --- a/context/rendering.md +++ b/context/rendering.md @@ -126,13 +126,22 @@ never engaged by the port. Mechanics (all [T1] from the BMF/BGF/vehicletable con top-left), shown through the paintable-zone materials and tinted by the pilot color. `EMBLEM0.BMF` carries `lgoN__dz_ld/rdleg` materials (8 patch tints incl. yellow .89,.73,0) over `LGO.BSL` slices 0-5 (slice 5 = VGL winged logo). -- PORT GAP: `SetupMaterialSubstitutionList` (btl4vid.cpp:2094) feeds NULL egg names (Mech's - resourceNameA/B/C not yet safely readable), and `substituteMaterial` (L4VIDEO.cpp:2565) is consumed - only by the legacy .X loader (L4D3D.cpp:151) — NOT by `bgfload`'s `MaterialResolver::resolve`. So - every mech renders base untinted paint + the hardwired `lgo6` (VGL) emblem. Verified live 2026-07-17: - MP MadCat renders white/gray, no tint/emblem/stripes. FIX PATH: (a) wire real egg color/badge/patch - into the substitution list; (b) apply the substitution in `resolve()` (BGF path); (c) per-variant - mesh caching (BgfData is cached per filename — two pilots' colors must not share a baked mesh). [T2] +- **WIRED 2026-07-17 (same-day fix), verified live**: MP MadCat renders crimson with hip hazard + stripes + yellow VGL leg emblems; Blackhawk white + emblems; replicants painted on both nodes. + The reconstruction: (a) `Mech::resourceNameA/B/C` are real `CString` members (the binary's exact + 16-byte `CStringRepresentation` {size,len,text@+8,refs@+0xc}; ctor = part_012.c:9841-71, deep-copy + bind from the MakeMessage = `CString::operator=(const char*)` == FUN_00402a98 @10427-41, implicit + member dtors = 11388-403) with paint-name accessors; (b) `SetupMaterialSubstitutionList` reads them + (real egg names; `[paint]` log line); (c) `dpl_SetMaterialNameCallback` is REAL now + (L4VIDEO.cpp registry, cleared first in TearDown as FUN_004d11e8 does) and + `bgfload MaterialResolver::resolve()` runs every name through `dpl_ApplyMaterialNameCallback`; + no mesh-cache issue (LoadObject/LoadBgfFile don't cache — every load resolves fresh). [T2] +- DEVIATION (documented, btl4vid.cpp): on a vehicletable miss the binary Fail()ed ("Exiting" + @BTL4VID.CPP:0xbeb); the port warns + drops that placeholder (RP analogue). Real case: dev eggs + ship `color=Red` but the `[color]` table only has **Crimson** — MP_BHMC.EGG fixed to Crimson; + LAST.EGG pilot-2 still says Red (unused slot). The 7 valid egg colors: + Black/Brown/Crimson/Green/Grey/Tan/White; badges Davion/Kurita/Liao/Marik/None/Steiner/VGL; + patches Black/Blue/Green/Grey/Red/Violet/White/Yellow. [T1] ## The OLD-STYLE SPECIALFX layer (the weapon-impact action) — RECONSTRUCTED 2026-07-12 (firmware-decoded) The per-weapon impact visuals are fully AUTHORED in `content/BTDPL.INI` `[effects_to_load]`: diff --git a/engine/MUNGA_L4/L4VIDEO.cpp b/engine/MUNGA_L4/L4VIDEO.cpp index bbc9ef3..af2b08a 100644 --- a/engine/MUNGA_L4/L4VIDEO.cpp +++ b/engine/MUNGA_L4/L4VIDEO.cpp @@ -2631,6 +2631,39 @@ char* return source; } +//############################################################################# +// dpl material-name callback registry (FUN_0049664c / DAT_00527f24). +//----------------------------------------------------------------------------- +// The 1995 dpl board applied the installed callback to every material name as +// a model loaded (dpl_LoadObject). The port analogue: the BGF loader +// (bgfload.cpp MaterialResolver::resolve) runs every "lib:material" name +// through dpl_ApplyMaterialNameCallback before resolving it, so the per-pilot +// paint substitutions (skin: -> _:, skin:lgo -> emblem:) +// installed by SetupMaterialSubstitutionList take effect on the mech model +// loads that happen between the Setup/TearDown bracket. +//############################################################################# +static char *(*gMaterialNameCallback)(char *source) = NULL; + +void dpl_SetMaterialNameCallback(char *(*callback)(char *source)) +{ + gMaterialNameCallback = callback; +} + +// C++-friendly wrapper honouring the callback's legacy protocol (the callback +// may delete[] the input and return a new[] buffer). Returns the (possibly +// substituted) name. +std::string dpl_ApplyMaterialNameCallback(const std::string &name) +{ + if (gMaterialNameCallback == NULL) + return name; + char *buf = new char[name.size() + 1]; + strcpy(buf, name.c_str()); + buf = gMaterialNameCallback(buf); + std::string result(buf); + delete [] buf; + return result; +} + vector DPLRenderer::MonitorsCreateAll(int &monitorCount) { monitorCount = gD3D->GetAdapterCount(); diff --git a/engine/MUNGA_L4/L4VIDEO.h b/engine/MUNGA_L4/L4VIDEO.h index 6d3d455..f15c101 100644 --- a/engine/MUNGA_L4/L4VIDEO.h +++ b/engine/MUNGA_L4/L4VIDEO.h @@ -40,6 +40,13 @@ class NotationFile; char* substituteMaterial(char *source); +// dpl material-name callback registry (FUN_0049664c): install/clear the +// per-load substitution hook; dpl_ApplyMaterialNameCallback runs the installed +// hook over a name (identity when none installed). Consumed by the BGF +// loader's material resolver. +void dpl_SetMaterialNameCallback(char *(*callback)(char *source)); +std::string dpl_ApplyMaterialNameCallback(const std::string &name); + void loadTables(); const char* opMaterialName(const char *fileName, int opId); diff --git a/engine/MUNGA_L4/bgfload.cpp b/engine/MUNGA_L4/bgfload.cpp index 049b8a7..0918420 100644 --- a/engine/MUNGA_L4/bgfload.cpp +++ b/engine/MUNGA_L4/bgfload.cpp @@ -11,6 +11,11 @@ #include #include +// L4VIDEO.cpp: the dpl material-name callback registry. While a mech loads +// (the SetupMaterialSubstitutionList / TearDown bracket) this rewrites +// "blhskin:..." -> "blh_wht0:..." / "emblem0:..." -- the per-pilot paint. +std::string dpl_ApplyMaterialNameCallback(const std::string &name); + namespace { // ---- DIV-BIZ2 tag ids (PFBIZTAG.H), masked form (tagword & 0x2fff) ---- @@ -503,7 +508,11 @@ struct MaterialResolver { return libs.emplace(lib, std::move(table)).first->second; } - MatInfo resolve(const std::string& full) { + MatInfo resolve(const std::string& fullIn) { + // Per-pilot paint: run the name through the installed dpl material-name + // callback first (identity when none) -- the board rewrote names at + // load time, before any material lookup. + std::string full = dpl_ApplyMaterialNameCallback(fullIn); auto cached = resolvedCache.find(full); if (cached != resolvedCache.end()) return cached->second; MatInfo info; diff --git a/game/reconstructed/btl4vid.cpp b/game/reconstructed/btl4vid.cpp index d7a384c..75fe058 100644 --- a/game/reconstructed/btl4vid.cpp +++ b/game/reconstructed/btl4vid.cpp @@ -2128,23 +2128,25 @@ void // // Look up this mech's colour / badge / patch codes from the table, using // the egg-supplied names carried on the entity (badge=resourceNameA @0x844, - // color=resourceNameB @0x848, patch=resourceNameC @0x84c). + // color=resourceNameB @0x848, patch=resourceNameC @0x84c). The binary + // reads the backing text straight off the ref-counted name objects + // (*(char**)(*(int*)(mech+0x848)+8) etc.); the reconstructed Mech now + // carries them as real CString members deep-copied from the MakeMessage, + // exposed via the paint-name accessors. // - // The recovered code read these directly off the BattleTech mech egg - // (Mech::vehicleColor / vehicleBadge / vehiclePatch) and Fail()ed on a miss. - // TODO(bring-up): the reconstructed Mech carries those names as ref-counted - // creation-name objects (resourceNameA/B/C) whose backing string is the - // transient MakeMessage buffer -- not safely readable here yet, and the - // attribute-index path ("VehicleColor"/...) is not wired, so it returns - // garbage that FindNote then deref-crashed on (btl4vid.cpp:808). Follow the - // RP analogue (RPL4VID.cpp:1562) which simply tolerates a missing colour/ - // badge: a NULL egg name or a table miss leaves veh_* == NULL and the - // placeholder substitution below drops to default materials. Re-wire the - // real egg names (a named Mech accessor) once the Mech layout is mapped. + // DEVIATION (documented): on a table miss the binary Fail()ed hard + // ("Exiting" @BTL4VID.CPP:0xbeb/0xbf2/0xbf9). We follow the RP analogue + // (RPL4VID.cpp:1562) and tolerate it -- a NULL/missing name leaves veh_* == + // NULL and that placeholder expands empty (base materials) -- so a sparse + // test egg degrades to the old gray-metal look instead of killing the pod. // - const char *egg_color = NULL; // [0x848] resourceNameB (color) - const char *egg_badge = NULL; // [0x844] resourceNameA (badge) - const char *egg_patch = NULL; // [0x84c] resourceNameC (patch) + Mech *mech = (Mech *)entity; // caller dispatched on MechClassID + const char *egg_color = mech->GetVehicleColorName(); // [0x848] resourceNameB + const char *egg_badge = mech->GetVehicleBadgeName(); // [0x844] resourceNameA + const char *egg_patch = mech->GetVehiclePatchName(); // [0x84c] resourceNameC + if (egg_color && !*egg_color) egg_color = NULL; + if (egg_badge && !*egg_badge) egg_badge = NULL; + if (egg_patch && !*egg_patch) egg_patch = NULL; const char *veh_color = NULL, *veh_badge = NULL, *veh_patch = NULL; if (egg_color && !veh_tbl->GetEntry("color", egg_color, &veh_color)) // @0051d94e @@ -2166,6 +2168,14 @@ void veh_patch = NULL; } + DEBUG_STREAM << "[paint] mech egg color='" << (egg_color ? egg_color : "") + << "' badge='" << (egg_badge ? egg_badge : "") + << "' patch='" << (egg_patch ? egg_patch : "") + << "' -> codes color=" << (veh_color ? veh_color : "-") + << " badge=" << (veh_badge ? veh_badge : "-") + << " patch=" << (veh_patch ? veh_patch : "-") + << " serno=" << gSerno << "\n" << std::flush; + // // Generic substitution list, then expand placeholders per entry. // @@ -2257,6 +2267,7 @@ void void BTL4VideoRenderer::TearDownMaterialSubstitutionList() { + dpl_SetMaterialNameCallback(NULL); // FUN_0049664c(0) -- first, as the binary does if (materialSubstitutionList != NULL) { for (NameList::Entry *entry = materialSubstitutionList->GetFirstEntry(); @@ -2269,7 +2280,6 @@ void delete materialSubstitutionList; materialSubstitutionList = NULL; } - // dpl_SetMaterialNameCallback(NULL); } //===========================================================================// diff --git a/game/reconstructed/btstubs.cpp b/game/reconstructed/btstubs.cpp index f4d9daa..503a025 100644 --- a/game/reconstructed/btstubs.cpp +++ b/game/reconstructed/btstubs.cpp @@ -247,17 +247,11 @@ LWord BT_GetSegmentFlags(Mech *owner) return owner ? owner->simulationFlags : 0; } -//===========================================================================// -// Legacy libDPL material-name callback. -//---------------------------------------------------------------------------// -// The dpl2d_ 2D display-list API (NewDisplayList/Begin/SetColor/Circle/ -// PushMatrix/PopMatrix/MoveTo/End/Compile) was MOVED OUT of this file and -// reimplemented over Direct3D 9 in decomp/reconstructed/dpl2d.cpp (the HUD/ -// reticle 2D layer). Only the 3D material-name-substitution hook -- which is -// part of the world renderer, not the 2D HUD -- remains stubbed here. -// TODO(bring-up): wire the material substitution table (FUN_00459eb8). -//===========================================================================// -void dpl_SetMaterialNameCallback(char *(*)(char *)) {} +// dpl_SetMaterialNameCallback is now REAL (per-pilot mech paint): it lives in +// engine/MUNGA_L4/L4VIDEO.cpp next to substituteMaterial, and the BGF loader +// (bgfload.cpp MaterialResolver::resolve) applies the installed callback to +// every material name -- the port analogue of the dpl board rewriting names +// at dpl_LoadObject time (FUN_0049664c / FUN_00459eb8). //===========================================================================// // BTL4VideoRenderer helper methods whose bodies were not reconstructed. diff --git a/game/reconstructed/mech.cpp b/game/reconstructed/mech.cpp index 6ed9e9c..44770f2 100644 --- a/game/reconstructed/mech.cpp +++ b/game/reconstructed/mech.cpp @@ -888,10 +888,11 @@ Mech::Mech( // // Three ref-counted creation-name holders (badge/color/insignia). + // CString members now: their default ctors run in the member-init phase and + // allocate the empty CStringRepresentation + addref -- exactly the binary's + // new(0x10)/FUN_004023f4/refs++ triple @part_012.c:9841-9871. Nothing to + // do here. // - resourceNameA = NewRefCounted(); // Wword(0x211) - resourceNameB = NewRefCounted(); // Wword(0x212) - resourceNameC = NewRefCounted(); // Wword(0x213) instanceFlags |= 0x10; // this[10] |= 0x10 // Zero the optional-subsystem back-pointers up front (binary @9873-9875). @@ -1710,11 +1711,14 @@ Mech::Mech( } // - // Bind the three creation-name strings from the MakeMessage. + // Bind the three creation-name strings from the MakeMessage. CString:: + // operator=(const char*) releases the empty ctor rep and allocates a fresh + // rep COPYING the message's inline char[20] -- byte-for-byte FUN_00402a98 + // (part_012.c:10427-10441); the names survive the transient message. // - resourceNameA = creation_message->resourceNameA; // param_2+0x7c -> Wword(0x211) - resourceNameB = creation_message->resourceNameB; // param_2+0x90 -> Wword(0x212) - resourceNameC = creation_message->resourceNameC; // param_2+0xa4 -> Wword(0x213) + resourceNameA = creation_message->resourceNameA; // badge param_2+0x7c -> Wword(0x211) + resourceNameB = creation_message->resourceNameB; // color param_2+0x90 -> Wword(0x212) + resourceNameC = creation_message->resourceNameC; // patch param_2+0xa4 -> Wword(0x213) // // Register this Mech in the global "Mechs" directory. @@ -1810,9 +1814,10 @@ Mech::~Mech() delete (MechDeathHandler *)deathHandler; // real class -> plain delete } - ReleaseRefCounted(resourceNameC); // Wword(0x213) - ReleaseRefCounted(resourceNameB); // Wword(0x212) - ReleaseRefCounted(resourceNameA); // Wword(0x211) + // resourceNameC/B/A: the binary's inline refcount-decrement/delete triple + // (part_012.c:11388-11403) is the CString member dtors -- implicit at the + // closing brace now that the members are real CStrings (dtor-epilogue rule: + // do NOT also write them out here). // // ⚠ RECONSTRUCTION RULE (root cause of the P5 death-row crash AND the app-exit diff --git a/game/reconstructed/mech.hpp b/game/reconstructed/mech.hpp index 291393a..3b079cf 100644 --- a/game/reconstructed/mech.hpp +++ b/game/reconstructed/mech.hpp @@ -50,6 +50,7 @@ # include // GaugeAlarm #endif #include // PlatformTool +#include // CString (the vehicle badge/color/patch creation names) #include // AffineMatrix #include // AverageOf #include // Scalar @@ -764,10 +765,27 @@ protected: // eye-slew system (HUD freeAimSlew / gyro eye chain) writes it. EulerAngles eyepointRotation; - // Three ref-counted creation-name objects (badge/color/insignia). - void *resourceNameA; // @0x844 this[0x211] - void *resourceNameB; // @0x848 this[0x212] - void *resourceNameC; // @0x84c this[0x213] + // Three ref-counted creation-name strings (badge/color/insignia). + // CString members: each slot is a CStringRepresentation* -- the exact + // 16-byte object {size,len,text@+8,refs@+0xc} the binary's Mech ctor + // allocates (part_012.c:9841-9871) and the make handler re-binds from + // the MakeMessage via CString::operator=(const char*) == FUN_00402a98 + // (part_012.c:10427-10441). SetupMaterialSubstitutionList reads the + // backing text (*(char**)(this+0x844..84c)+8) for the vehicle-table + // color/badge/patch lookups. + CString resourceNameA; // @0x844 this[0x211] (vehicle badge) + CString resourceNameB; // @0x848 this[0x212] (vehicle color) + CString resourceNameC; // @0x84c this[0x213] (vehicle patch) + + public: + // Paint-name accessors (SetupMaterialSubstitutionList). CString's + // operator char*() returns the rep's stringText -- NULL for a + // default-constructed (never-bound) name; callers must tolerate NULL. + const char *GetVehicleBadgeName() const { return (const char *)resourceNameA; } + const char *GetVehicleColorName() const { return (const char *)resourceNameB; } + const char *GetVehiclePatchName() const { return (const char *)resourceNameC; } + + protected: //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Reconstruction additions -- members & methods whose bodies live in the From 63250aee416730fcedda2aef04805246d1a8b20e Mon Sep 17 00:00:00 2001 From: arcattack Date: Fri, 17 Jul 2026 13:58:32 -0500 Subject: [PATCH 4/6] Fix two Jul-16 regressions: BT_DEV_GAUGES crash + -net dead controls mapper Both surfaced today (dormant ~1 day) the moment the pod launch flags (BT_DEV_GAUGES=1 BT_START_INSIDE=1, tools/mp_launch.sh) were used to drive in multiplayer -- which made the same-day paint change look guilty. Confirmed on the pre-paint build too; the trigger was the Jul-16 audio attribute work. 1) BT_DEV_GAUGES crash (cdb: CoolingLoopConnection::Update, ~15s in as the gauge builds lazily). The dev-gauge cooling-loop lamp reached the cooling master by RAW attribute index -- GetAttributePointer(3) + *(master+0x1d4). The audio commits (cc2b109 ReportLeak etc.) inserted rows into the chained attribute tables, so index 3 shifted onto a scalar, the resolve walked garbage, and the background pass AV'd. Fix: route through a complete-type bridge reading NAMED members (heat.cpp BTCoolingLoopFrame: linkedSinks.Resolve() + Condenser::condenserNumber) -- the databinding rule; never a raw numeric attribute index. Removed the now-orphaned GetAttributePointer-by-index helper. Name-keyed samplers (InputVoltage) were unaffected. 2) -net dead controls mapper (mech wouldn't walk; turning/weapons still worked). The RIO mapper is built from a stack SubsystemResource in btl4app.cpp that set only name/classID/modelSize -- leaving subsystemFlags as stack GARBAGE, which Subsystem::Subsystem copies into simulationFlags. A stray DontExecuteFlag (0x2) froze the mapper (speedDemand stuck at 0). Config-dependent: clean in SP, dirty in -net; the audio commits shifted the stack and flipped the bit. Fix: memset each hand-built control-mapper resource to 0 (flags 0 = AlwaysExecute, the faithful value -- a mapper must tick every frame). Verified with the pod launch flags: SP alive 32s no crash; MP mech walks (speedDemand 61.5 at full throttle, ~430u traveled), both nodes alive, no crash. KB: reconstruction-gotchas.md gains gotcha 18 (uninitialized stack-resource flags + the raw-attribute-index-into-a-growing-table variant) and a verify-under-the-user's-launch-flags note. Co-Authored-By: Claude Opus 4.8 (1M context) --- context/reconstruction-gotchas.md | 35 ++++++++++++++++++++++++++ game/reconstructed/btl4app.cpp | 26 +++++++++++++++++++ game/reconstructed/btl4gau2.cpp | 42 +++++++++++-------------------- game/reconstructed/heat.cpp | 28 +++++++++++++++++++++ 4 files changed, 104 insertions(+), 27 deletions(-) diff --git a/context/reconstruction-gotchas.md b/context/reconstruction-gotchas.md index 100345c..e48ce4c 100644 --- a/context/reconstruction-gotchas.md +++ b/context/reconstruction-gotchas.md @@ -250,6 +250,11 @@ state transitions: charge/seek loops, snap comparisons, timers compared with `== ucrtbased **abort() dialog** ("Debug Error!"), not an AV — `sxe av` won't break there; the box blocks the event loop (a headless node just "stops logging"). cdb: run with a config that does `g` then `kb 40` — the int3 lands ON the aborting thread. [T2] +- **Verify under the USER'S launch flags, not a bare run:** a "30 s regression: stable" check that + omits `BT_DEV_GAUGES=1` / `BT_START_INSIDE=1` (the `tools/mp_launch.sh` set) misses any bug on the + gauge/cockpit paths those flags gate. The task-#7 gauge crash was DORMANT for a day because every + post-commit check ran flagless; it surfaced the instant the pod launch config was used. Reproduce + with the real launcher's env, and drive (`-net` keyboard) — not just spawn-and-look. [T2] --- @@ -345,6 +350,36 @@ hundreds of pixels off-scope (the radar looked simply "empty"; nothing crashed, decompile) before assigning it an engine method — a wrong-but-plausible identity survives every compile and every "it runs" test. +## 18. Uninitialized stack-built resource → garbage `simulationFlags` (the "worked for weeks then broke" trap) + +**Symptom:** a subsystem installed from a HAND-BUILT (stack) `*__SubsystemResource` silently stops +ticking — `IsNonReplicantExecutable()` returns false because a stray `DontExecuteFlag` (0x2) bit is +set. Config-dependent and nondeterministic: it "works for weeks" then an UNRELATED commit that shifts +the stack layout before the install flips the garbage bit. Archetype (task #7, 2026-07-17): the RIO +controls mapper built on the stack in `btl4app.cpp MakeViewpointEntity` — `Subsystem::Subsystem` +copies `model->subsystemFlags` into `simulationFlags`, but the stack struct only set +`subsystemName`/`classID`/`subsystemModelSize`, leaving `subsystemFlags`+`segmentIndex` = stack +garbage. When the garbage carried 0x2 the mapper froze → `speedDemand` stuck at 0 → no forward motion +(MASKED in single-player when the garbage happened to be clean; surfaced in `-net` where the stack +differed; the July-16 audio commits shifted the stack and flipped it). **Cause:** partial init of a +struct the engine reads in full. **Fix:** `memset(&res, 0, sizeof(res))` before filling ANY +stack-built resource — flags 0 = `AlwaysExecute`, the faithful value for a mapper (it MUST tick every +frame). **Tell:** a subsystem present in the roster but absent from the executed set; instrument the +tick's `!IsNonReplicantExecutable()` branch to name the slot + dump `simulationFlags` (bit 0x2 = +DontExecute). **Rule:** zero every hand-built resource struct; never rely on a partially-filled one. +[T2] + +**Related — raw numeric attribute index into a GROWING table (same root as gotcha 11):** the dev +gauges' `CoolingLoopConnection` reached the cooling master with a RAW `GetAttributePointer(3)` + +`*(master+0x1d4)` walk. The July-16 audio work inserted attribute rows (`ReportLeak` etc.), shifting +the chained ids — index 3 landed on a scalar, the resolve walked garbage, and the gauge background +pass AV'd (only with `BT_DEV_GAUGES=1`, the pod launch flag, ~15 s in when the gauge builds lazily — +the cdb stack pinned it to `CoolingLoopConnection::Update`). **Fix:** route through a complete-type +bridge reading NAMED members (`heat.cpp BTCoolingLoopFrame`: `linkedSinks.Resolve()` + +`Condenser::condenserNumber`) — the databinding rule (gotcha 8), never a raw numeric attribute index. +The name-keyed samplers (`PowerSourceConnection`/`GeneratorVoltageConnection`, which look up +`"InputVoltage"` by NAME) were unaffected — name lookup survives table growth. [T2] + --- ## Diagnostic recipe (the standard loop) diff --git a/game/reconstructed/btl4app.cpp b/game/reconstructed/btl4app.cpp index 2f4a5c2..5254d9b 100644 --- a/game/reconstructed/btl4app.cpp +++ b/game/reconstructed/btl4app.cpp @@ -445,6 +445,8 @@ BTL4Application::SharedData //------------------------------------------------------------ CameraControlsMapper::SubsystemResource control_subsystem_resource; + // task #7: zero the stack resource (see the mech mapper note below). + memset(&control_subsystem_resource, 0, sizeof(control_subsystem_resource)); Str_Copy( control_subsystem_resource.subsystemName, @@ -530,6 +532,14 @@ BTL4Application::SharedData // MechControlsMapper::SubsystemResource control_subsystem_resource; + // task #7 ROOT-CAUSE FIX: Subsystem::Subsystem copies + // model->subsystemFlags into simulationFlags -- an + // uninitialized stack resource left GARBAGE there, and a + // stray DontExecuteFlag bit froze the installed controls + // mapper for the whole session (the -net "can't drive" + // freeze; config-dependent because stack contents are). + // Zero the whole struct before filling it. + memset(&control_subsystem_resource, 0, sizeof(control_subsystem_resource)); Str_Copy( control_subsystem_resource.subsystemName, @@ -555,6 +565,14 @@ BTL4Application::SharedData { MechControlsMapper::SubsystemResource control_subsystem_resource; + // task #7 ROOT-CAUSE FIX: Subsystem::Subsystem copies + // model->subsystemFlags into simulationFlags -- an + // uninitialized stack resource left GARBAGE there, and a + // stray DontExecuteFlag bit froze the installed controls + // mapper for the whole session (the -net "can't drive" + // freeze; config-dependent because stack contents are). + // Zero the whole struct before filling it. + memset(&control_subsystem_resource, 0, sizeof(control_subsystem_resource)); Str_Copy( control_subsystem_resource.subsystemName, @@ -589,6 +607,14 @@ BTL4Application::SharedData { MechControlsMapper::SubsystemResource control_subsystem_resource; + // task #7 ROOT-CAUSE FIX: Subsystem::Subsystem copies + // model->subsystemFlags into simulationFlags -- an + // uninitialized stack resource left GARBAGE there, and a + // stray DontExecuteFlag bit froze the installed controls + // mapper for the whole session (the -net "can't drive" + // freeze; config-dependent because stack contents are). + // Zero the whole struct before filling it. + memset(&control_subsystem_resource, 0, sizeof(control_subsystem_resource)); Str_Copy( control_subsystem_resource.subsystemName, diff --git a/game/reconstructed/btl4gau2.cpp b/game/reconstructed/btl4gau2.cpp index ace13f7..d7b8292 100644 --- a/game/reconstructed/btl4gau2.cpp +++ b/game/reconstructed/btl4gau2.cpp @@ -154,22 +154,11 @@ static void *ResolveLink(void *plug) // extern bool BTGetSubsystemAuxScreen(Subsystem *sub, int *screen, int *placement, char *label64); -// -// FUN_0041bf44 -- GetAttributePointer(index): a pointer to the subsystem's Nth -// attribute member (the cooling-loop lamp reaches its cooling-master link at -// attribute slot 3), or 0 if the index is out of range. The engine exposes this -// via Simulation::GetAttributePointer(AttributeID); a miss returns &NullAttribute, -// normalised to 0. -// -static void *FindLinkedSubsystem(void *subsystem, int slot) -{ - if (subsystem == 0) - return 0; - void *p = ((Simulation *)subsystem)->GetAttributePointer((Simulation::AttributeID)slot); - if (p == (void *)&Simulation::NullAttribute) - return 0; - return p; -} +// (FUN_0041bf44 GetAttributePointer-by-index helper removed with the task #7 +// fix: its only caller was CoolingLoopConnection, which now resolves the +// cooling master through the named-member bridge BTCoolingLoopFrame -- reaching +// an attribute by RAW numeric index is exactly what broke when the audio +// attribute rows grew the chained tables.) // // FUN_0041a1a4 IsDerivedFrom(0x50f4bc == PoweredSubsystem's ClassDerivations). @@ -191,10 +180,16 @@ static Logical IsGeneratorDerived(Subsystem *sub) { return BTIsPoweredSubsystem( // // CoolingLoopConnection -- @004c3134 ctor / @004c31a0 sampler (vt 0x518ea8). -// When the source subsystem's coolant loop is active (source+0x134 == 1) it -// follows the linked cooling master (FindLinkedSubsystem(source,3) then Resolve) -// and copies its loop-lamp state (+0x1d4); otherwise 0. +// When the source subsystem's coolant loop is available (coolantAvailable == 1, +// the binary's source+0x134 gate) it follows the linked cooling master (binary +// attr id 3 == "HeatSink" == linkedSinks) and shows its Condenser loop number +// (+0x1d4 == condenserNumber, the image-strip frame); otherwise 0. +// task #7 REGRESSION FIX (2026-07-17): the old raw GetAttributePointer(3) + +// *(+0x1d4) walk AV'd after the audio attribute-table rows shifted the chained +// ids (BT_DEV_GAUGES crash). The sampler now routes through a complete-type +// bridge (heat.cpp) reading NAMED members -- the databinding rule (gotcha 8). // +extern int BTCoolingLoopFrame(void *subsystem); class CoolingLoopConnection : public GaugeConnection { public: @@ -203,14 +198,7 @@ public: source(source), destination(destination) {} void Update() // @004c31a0 { - void *resolved = 0; - if (source != 0 && *(int *)((char *)source + 0x134) == 1) - { - void *linked = FindLinkedSubsystem(source, 3); - if (linked != 0) - resolved = ResolveLink(linked); - } - *destination = (resolved == 0) ? 0 : *(int *)((char *)resolved + 0x1d4); + *destination = BTCoolingLoopFrame(source); } protected: void *source; // @0x10 this[4] diff --git a/game/reconstructed/heat.cpp b/game/reconstructed/heat.cpp index 5bd5c62..dc7e549 100644 --- a/game/reconstructed/heat.cpp +++ b/game/reconstructed/heat.cpp @@ -1123,3 +1123,31 @@ Subsystem *CreateCondenserSubsystem(Mech *owner, int id, void *seg) // CreateHeatSinkBankSubsystem (0xBBE) now lives in heatfamily_reslice.cpp -- it // builds the real AggregateHeatSink (which needs that TU's class definition). + +//===========================================================================// +// Gauge cooling-loop bridge (task #7 regression fix, 2026-07-17). +// +// The dev-gauge CoolingLoopConnection (btl4gau2.cpp @004c31a0) reached the +// cooling master with a RAW attribute index (GetAttributePointer(3)) and then +// raw-read master+0x1d4. The binary heat attribute table (@0x50e438..0x50e4c8) +// shows id 3 == "HeatSink" == the linkedSinks plug, and +0x1d4 on the resolved +// master is Condenser::condenserNumber (the loop number that selects the +// image-strip frame). The numeric-id read crashed when the AUDIO_FIDELITY rows +// (ReportLeak et al., cc2b109) grew the chained tables: index 3 landed on a +// scalar, the resolve walked garbage, and the gauge background pass AV'd +// (BT_DEV_GAUGES=1, the pod launch flag). House rule (gotcha 8): named members +// via a complete-type TU -- never a raw numeric attribute index. +//===========================================================================// +int BTCoolingLoopFrame(void *subsystem_v) +{ + Subsystem *sub = (Subsystem *)subsystem_v; + if (sub == 0 || !sub->IsDerivedFrom(*HeatSink::GetClassDerivations())) + return 0; + HeatSink *sink = (HeatSink *)sub; + if (sink->coolantAvailable != 1) // the binary's src+0x134 gate + return 0; + Subsystem *master = sink->linkedSinks.Resolve(); // attr id 3 "HeatSink" + FUN_00417ab4 + if (master == 0 || !master->IsDerivedFrom(*Condenser::GetClassDerivations())) + return 0; + return ((Condenser *)master)->condenserNumber; // master+0x1d4 (loop/frame number) +} From 8616b62405f5a7aaf6edf60032f5b3a5c8a359f6 Mon Sep 17 00:00:00 2001 From: arcattack Date: Fri, 17 Jul 2026 14:25:17 -0500 Subject: [PATCH 5/6] LAST.EGG: pilot-2 -> crimson MadCat (Red->Crimson: vehicletable has no Red) Co-Authored-By: Claude Opus 4.8 (1M context) --- content/LAST.EGG | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/content/LAST.EGG b/content/LAST.EGG index 72e7c5a..e0961a1 100644 --- a/content/LAST.EGG +++ b/content/LAST.EGG @@ -183,9 +183,9 @@ badge=VGL patch=Yellow role=Role::Default dropzone=one -vehicle=ava1 +vehicle=madcat vehicleValue=1000 -color=Red +color=Crimson [largebitmap] bitmap=BitMap::Large::Aeolus [BitMap::Large::Aeolus] From 1ed8b051602de084ebb48afd544d5ee4d08751d1 Mon Sep 17 00:00:00 2001 From: arcattack Date: Fri, 17 Jul 2026 15:24:52 -0500 Subject: [PATCH 6/6] Radar view-wedge tracks the torso twist (Gitea issue #1) The SECTOR radar's view cone read viewHorizontalRotation, wired by the MapDisplay ctor from the mech's Torso -- but FindSubObject and GetHorizontalRotation were NULL stubs, so the connection was never created and the wedge sat at heading 0 regardless of twist. Reconstructed from the binary: - FindSubObject (FUN_0041f98c): a subsystem-ROSTER walk (count @+0x124, array @+0x128) matching the streamed subsystem name (sub+0xd4) with a tolower-strcmp (FUN_004d4b58) -- the 'Torso' sub-object IS the roster Torso subsystem. - GetHorizontalRotation: torso+0x1D8 == Torso::currentTwist (layout-locked), via the existing task-#56 bridge BTGetTorsoTwistAddr (Radian is layout- identical to Scalar). Verified live (MadCat, Standard mode Q/E): the wedge tracks the twist in lockstep (rot 0 -> -2.21 rad), and the semantic test passes -- body turned away, torso twisted back onto the enemy: reticle green + wedge pointing at the enemy's blip on the body-fixed scope. NB the Blackhawk's torso is FIXED (+/-0.01 deg limits) -- its wedge authentically never moves; test with a twisting mech. Diag env: BT_RADAR_LOG. Co-Authored-By: Claude Opus 4.8 (1M context) --- context/gauges-hud.md | 16 ++++++++++ game/reconstructed/btl4rdr.cpp | 55 +++++++++++++++++++++++++++++----- 2 files changed, 63 insertions(+), 8 deletions(-) diff --git a/context/gauges-hud.md b/context/gauges-hud.md index 2b13a77..a862a46 100644 --- a/context/gauges-hud.md +++ b/context/gauges-hud.md @@ -131,6 +131,22 @@ At the authentic 1e7-unit economy (tasks #9-#10) the Heat MFD is fully dynamic: 77→2000 (the authored failure threshold), condensers/generators run 100-1400 under sustained fire, and the bank plateaus ~600 — temperatures, heatLoad and the heat alarms all animate. [T2] +## Radar view-wedge tracks the torso twist (Gitea issue #1 — FIXED 2026-07-17) [T2] +The SECTOR radar's view cone (`MapDisplay::DrawViewWedge` @004c2484, btl4rdr.cpp) reads +`viewHorizontalRotation@0x31C`, fed by a per-frame `GaugeConnectionDirectOf` the ctor +wires from the mech's Torso. The two helpers were NULL stubs, so the connection was never +created and the wedge sat at heading 0 (the reported "cone does not track the twist"). +Reconstructed from the binary: `FindSubObject` (FUN_0041f98c) = a SUBSYSTEM-ROSTER walk +(count @+0x124 / array @+0x128) matching the streamed name (sub+0xd4) case-insensitively +(FUN_004d4b58 = tolower-strcmp) — the "Torso" sub-object IS the roster Torso subsystem; +`GetHorizontalRotation` = torso+0x1D8 = `Torso::currentTwist` (layout-locked), reached via the +existing task-#56 bridge `BTGetTorsoTwistAddr` (Radian ≡ {Scalar angle}, exact reinterpret). +VERIFIED live (MadCat, Standard mode Q/E): wedge rot tracks twist 0→−2.21 rad in lockstep, and +the SEMANTIC test passes — body turned away, torso twisted back onto the enemy → reticle goes +green AND the wedge points at the enemy's blip while the scope stays body-fixed. ⚠ Test with a +TWISTING mech: the Blackhawk's torso is FIXED (limits ±0.01°) — its wedge authentically never +moves. Diag: `BT_RADAR_LOG` ([radar] ctor probe + 1 Hz [radar-wedge] rot trace). [T2] + ## Remaining = DATA FEEDS, not widgets (deferred systems) **✅ The condenser valve CONTROL is LIVE (task #13, 2026-07-11) [T2]:** `MoveValve` (id 4, the Condenser handler table @0x50E52C — exactly one entry) registered + guarded by the REAL diff --git a/game/reconstructed/btl4rdr.cpp b/game/reconstructed/btl4rdr.cpp index 4d2c360..ac061d9 100644 --- a/game/reconstructed/btl4rdr.cpp +++ b/game/reconstructed/btl4rdr.cpp @@ -201,11 +201,13 @@ static Entity * // // Mech sub-object lookup ("Torso") + its horizontal rotation feed. +// (The "sub-object" IS a roster SUBSYSTEM -- FUN_0041f98c walks the entity's +// subsystem roster by NAME; see the bodies at the end of this file.) // -static Entity * +static Subsystem * FindSubObject(Entity *entity, const char *name); static Radian * - GetHorizontalRotation(Entity *entity); + GetHorizontalRotation(Subsystem *torso); //########################################################################### @@ -471,7 +473,11 @@ MapDisplay::MapDisplay( // if (entity->GetClassID() == 0xBB9) { - Entity *torso = FindSubObject(entity, "Torso"); // FUN_0041f98c + Subsystem *torso = FindSubObject(entity, "Torso"); // FUN_0041f98c (roster walk) + if (getenv("BT_RADAR_LOG")) + DEBUG_STREAM << "[radar] ctor: torso sub " + << (torso ? "FOUND" : "NOT FOUND (roster names?)") + << " on mech " << (void *)entity << "\n" << std::flush; if (torso != NULL) { AddConnection( @@ -791,6 +797,19 @@ void Radian leftEdge = viewHorizontalRotation - viewHalfHorizontalWidth; Radian rightEdge = viewHorizontalRotation + viewHalfHorizontalWidth; + // DIAG (issue #1): 1 Hz wedge-heading trace. + if (getenv("BT_RADAR_LOG")) + { + static unsigned long sLast = 0; + unsigned long now = GetTickCount(); + if (now - sLast >= 1000) + { + sLast = now; + DEBUG_STREAM << "[radar-wedge] rot=" << (Scalar)viewHorizontalRotation + << "\n" << std::flush; + } + } + localView.SetColor(staticColor); // (this+0x48 vtbl+0x18) Radian edges[2]; @@ -1176,20 +1195,40 @@ static Entity * // // FUN_0041f98c -- Entity::FindSubObject(name) (locates "Torso"). +// The binary walks the entity's SUBSYSTEM ROSTER (count @+0x124, array +// @+0x128) and returns the first subsystem whose streamed NAME (sub+0xd4 == +// Subsystem::subsystemName) matches case-insensitively (FUN_004d4b58 is a +// tolower-strcmp). Gitea issue #1 fix (2026-07-17): this and the feed below +// were NULL stubs, so the torso-twist -> radar view-wedge connection was +// never created and the wedge sat at heading 0 forever. // -static Entity * - FindSubObject(Entity * /*entity*/, const char * /*name*/) +static Subsystem * + FindSubObject(Entity *entity, const char *name) { + if (entity == NULL || name == NULL) + return NULL; + for (int i = 0; i < entity->GetSubsystemCount(); ++i) + { + Subsystem *sub = entity->GetSubsystem(i); + if (sub != NULL && sub->GetName() != NULL + && _stricmp(sub->GetName(), name) == 0) + return sub; + } return NULL; } // -// torso+0x1D8 -- the sub-object's horizontal (yaw) rotation feed. +// torso+0x1D8 -- the sub-object's horizontal (yaw) rotation feed: +// Torso::currentTwist (layout-locked @0x1D8, torso.hpp). Reached via the +// torso.cpp complete-type bridge (task #56's BTGetTorsoTwistAddr -- the same +// pointer the gyro's external-pitch link uses). Radian is layout-identical +// to Scalar ({ Scalar angle }, ANGLE.h), so the reinterpret is exact. // static Radian * - GetHorizontalRotation(Entity * /*entity*/) + GetHorizontalRotation(Subsystem *torso) { - return NULL; + extern Scalar *BTGetTorsoTwistAddr(Subsystem *torso); + return (Radian *)BTGetTorsoTwistAddr(torso); }