The 2026-07-20 audit listed 8 panel buttons dispatching a streamed message with
no reconstructed handler. Only ONE was still missing: generator on/off,
ToggleSeekVoltage, EjectAmmo, ToggleCooling and BalanceCoolant had all landed
between 07-20 and 07-25 while pod-hardware.md and open-questions.md still called
them dead. docs/INPUT_PATH_AUDIT.md had already flagged the census as "stale in
both directions" and was right -- swept both files, and recorded the rule: check
the code, not the census.
DuckRequest @0049fa00 (id 0x1a, RIO 0x13 -- the manual gives crouch a whole
section). The binary's entire body:
if (0 < *(int *)(param_2 + 0xc)) { *(undefined4 *)(param_1 + 0x398) = 1; }
Press-only; sets duckState (mech+0x398, attribute 0x37). A one-shot REQUEST
flag, not a posture toggle -- the handler never clears it and the only other
writer in the whole binary is the mech reset (part_012.c:9439, the same reset
that zeroes incomingLock, which is how that region was already mapped).
WHY THE FLAG HAS NO READER, AND WHY THAT IS CORRECT. duckState has ZERO readers
anywhere in the decomp, because it is published as an ATTRIBUTE and consumed
through DATABINDING: content/GAUGE/L4GAUGE.CFG runs a 3-frame bduck.pcc
oneOfSeveralPixInt bound to DuckState -- "crouch mode: button 4" on the map's
legend column. Verified live by capturing the Secondary/Radar window before and
after a 0x13 press: the crouch icon goes grey -> orange. So the handler is
COMPLETE. A crouch pose invented here would be a stand-in for data we have not
found (no SQUAT clip name survives in the decomp or in content/, only
DuckServo01.wav in AUDIO1.RES).
Bonus: those gauge widgets sit at offsets 537/430/322/215/108 -- a 107 pitch,
independently corroborating the map legend grid measured from pixels.
Still open from that census: MechRIOMapper's own Keypress @004d2514 (id 0x19).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
932 lines
80 KiB
Markdown
932 lines
80 KiB
Markdown
---
|
||
id: open-questions
|
||
title: "Open Questions — deferred systems, unknowns, get-from-Nick"
|
||
status: living
|
||
source_sections: "CLAUDE.md §9; docs/HARD_PROBLEMS.md; per-subsystem deferral notes"
|
||
related_topics: [source-completeness, subsystems, multiplayer, combat-damage, locomotion]
|
||
---
|
||
|
||
# Open Questions
|
||
|
||
Known unknowns + deliberately-deferred systems. For each: what's known, what's missing, what
|
||
would resolve it, status. Deferred ≠ broken — most are marked-and-guarded stand-ins with the
|
||
authentic path scoped.
|
||
|
||
## From Nick (external — gating for some paths)
|
||
- ~~**[GATING] The BT game SOURCE CODE**~~ **CLOSED 2026-07-19: per the user, Nick holds NO
|
||
assets beyond what has already been provided.** Route A (recompile original) is permanently
|
||
off; the decomp reconstruction ([[source-completeness]]) is THE route — behavioral in
|
||
`game/reconstructed/`, literal in `TeslaRel410/restoration/source410/`. Asset-type asks below
|
||
are likewise closed; knowledge/lore asks (throttle detents, pod procedures) remain valid.
|
||
- ~~Confirmation the PodPC image is the complete content master~~ **CLOSED 2026-07-19** (no
|
||
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
|
||
Win10+wrapper pod config. Status: OPEN.
|
||
- **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
|
||
([[locomotion]]) only makes sense if the hardware discouraged parking the lever mid-band —
|
||
"5 speeds" pod lore suggests physical notches. Status: OPEN.
|
||
- ~~Which mechs used rear fire~~ **ANSWERED from data (2026-07-16, task #68)**: the Blackhawk's
|
||
ERMLaser_2/3 (back gun ports) are the only rear weapons in the roster; see [[combat-damage]].
|
||
Remaining: bind the side/down look views to keys (only 'V' look-back is bound); pod look-button
|
||
mappings for Phase 8.
|
||
- ~~The `cameras\` directory (per-map camera-network files)~~ **RESOLVED from our own data
|
||
(2026-07-18, same day)**: the `cameras\<mapname>` FILE path (CAMMGR.cpp:390-415, RP-style
|
||
loose `.CAM` notation — present for RP maps in the nick-games 410SRC archive) is only the DEV
|
||
OVERRIDE; the shipped binary falls back to **BTL4.RES resource TYPE 27 (0x1b) named after the
|
||
map** (`FUN_0042be3c` probe → `FUN_0042c078(res, mapname, 0x1b)`; binary records
|
||
`{count; per-cam {size; type 1|2; pos+quat+...}}`, parsers @0042ad54/@0042b9f8) — and our
|
||
BTL4.RES has ALL 8 maps' networks (arena1 = 62 cameras; cavern/dbase/grass ~57KB each).
|
||
[T1] **CAMERA SEAT LIVE (2026-07-18, same day) [T2]**: the type-27 loader ALREADY existed in
|
||
the engine (`CreateStreamedCameraInstances`, CAMMGR.cpp:530 — the file probe falls back to it;
|
||
62 arena1 cameras load, `[cam]` diag under BT_CAM_LOG). The REAL missing piece was
|
||
**`Mech::PlayerLinkMessageHandler` (@0049f624)** — the BT override the reconstruction never
|
||
filled: base resolves mech→player; BT adds the REVERSE link `player->playerVehicle = this` on
|
||
EVERY node (replicants included — how the director/scoreboard find a remote player's mech),
|
||
clears `NonScoringPlayerFlag` (0x4000 = bit 14: a pilot with a vehicle SCORES/ranks — this
|
||
also makes cross-node RANKINGS work), and on the master seeds the heat bank's
|
||
`ambientTemperature` from the mission `temperature=` (mech+0x7dc bank +0x1d4 ← BTMission+0xf4
|
||
— the heat family's "frozen 300" note stands CORRECTED). Verified live: mech+camera 2-seat
|
||
relay session — replicated player links (`bmp=1+veh`), director locks the goal (30s timer),
|
||
camera ship tracks the moving mech (`sees=1`) and CUTS between authored cameras
|
||
(screenshots). Egg: pilot page `hostType=1` + `vehicle=camera`. **RANKING WINDOW LIVE (same day)**: the stubbed
|
||
1995 overlay reimplemented as screen-space quads in `CameraShipHUDRenderable::Render` — the
|
||
followed player's callsign banner (bottom center, the Execute index is ALREADY 0-based) + a
|
||
rank-ordered row per scoring player ([ordinal][callsign], right of center), visibility = the
|
||
Director's flash logic (10s on/15s off, solid final 30s), live `playerRank` pointers re-sort
|
||
rows as scores change. **Each 128×32 ordinal bitmap packs TWO ordinals side by side**
|
||
("1st|2nd", "3rd|4th" — why 4 bitmaps serve 8 players): texture rank/2, u-half by parity.
|
||
BT_SHOT now captures AFTER the 2D pass (it used to dump pre-HUD — overlays were on screen
|
||
but invisible to screenshots). Screenshot-verified: "1st MAVERICK" + banner over live
|
||
coverage. Remaining: operator-app camera-seat row helper (hostType not in the roster UI),
|
||
shot-composition polish. Status: broadcast display COMPLETE.
|
||
|
||
## 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].**
|
||
The per-zone EFFECT DISPATCHER (+helpers @4d0c14 StopAll / @4d0c5c) is reconstructed
|
||
(btl4vid.cpp:889) + WIRED: `MechDamageZone` change → `BTStartZoneEffect` →
|
||
RendererManager::StartEntityEffect → this virtual → the band effect ATTACHED to the struck
|
||
zone's segment (smoke/fire follows the mech); the Destroyed descriptor fires on kill
|
||
(mechdmg.cpp:1120). Band smoke/fire (psfx ≥1000) render through the live PFX layer
|
||
(BTStartPfxAttached); the 3-15 specialfx band + scaling booms/flames render through the
|
||
reconstructed L4VIDEO specialfx engine + `ScalingExplosionRenderable` (see next bullet).
|
||
**LIVENESS-VERIFIED (2026-07-14):** `BT_CRIT_PROBE=8` on solo DEV.EGG (hammers a local zone) drove
|
||
the full chain — `[zonefx] entity 1:321 seg 12 psfx 12/13 at(527,5.8,-669)`: TakeDamage → the
|
||
mechdmg damage-effect loop → BTStartZoneEffect → StartEntityEffectImplementation resolves the
|
||
struck segment's world pos → BTStartPfxAttached. (The 2-node harness `scratchpad/fx_liveness.sh`
|
||
did NOT engage — nodes spawned but never targeted/drove into combat, `mechPicks=0`; an MP-harness
|
||
spawn/connect issue, orthogonal to FX. `BT_CRIT_PROBE` is the reliable solo FX-liveness path.)
|
||
- **✅ The impact/destruction FX visual chain — RECONSTRUCTED (Fire VISUALS wave 48c9c84 +
|
||
Impact-FX FORENSICS wave 065c114, 2026-07-12/13) [T2].** The i860-firmware-decoded specialfx
|
||
engine (L4VIDEO.cpp: `specialfxN` descriptor pages, per-round detonations, authored firesmoke
|
||
sheet, vertex-alpha effect cards), the case-4 wreck dressing, and `ScalingExplosionRenderable`
|
||
(L4VIDRND.cpp:3413 — real ctor/Execute, the scaling-boom motion) are all live. The audit's six
|
||
renderable-body addresses (@4540ac/45447c/455eb8/456cf0/4589e0/458e5c) map to this reconstructed
|
||
set; a byte-level coverage cross-check of each vs the raw bodies is a minor open audit, NOT a
|
||
dead chain. (NOTE: the `//STUBBED: DPL RB 1/14/07` markers scattered in L4VIDRND are benign 2007
|
||
empty ctor/dtor notes on UNRELATED renderables — InnerProjectile/DPLObjectWrapper/ChildLight —
|
||
not the explosion FX; do not mistake them for a gap.)
|
||
- **FUN_00472480** (273 ln) — unnamed analog→discrete stepper Execute in the gauge band
|
||
(hysteresis + key-repeat), no caller found; the one hole in "gauge system complete".
|
||
Settle via vtable lookup (plausibly a config/roster scroller).
|
||
- **FUN_00454a70** (193 ln) — the PNAME1-8/PLACE1-8.bgf MP name-billboard loader (the known
|
||
"MP target identification" gap's exact function). ✅ **DECODED 2026-07-24 (not yet ported)
|
||
[T1/T2]** — it is the **MP SCOREBOARD/leaderboard**, built by the L4VIDEO video-resource
|
||
factory for **resource type 0x46** (part_008.c:2902-2911, gated `res+0x28 & 0xc == 0`, alloc
|
||
0xbc). All 16 plates are ONE flat textured quad each (4 verts / 1 PCONN quad / no LOD chunk /
|
||
no normals): PNAME = 1.0×0.25 m, PLACE = 0.5×0.25 m, both planar Z=0, UNIT=metre, authored
|
||
30-Nov-1995. **PNAME1-8 are literally the words "PLAYER 1".."PLAYER 8"**; **PLACE1-8 are the
|
||
rank ordinals "1ST".."8TH"** — a row reads `[PLACE][PNAME]` because the PLACE node is
|
||
translated x=−0.75 and spans ±0.25, so it ABUTS the PNAME plate's x∈[−0.5,+0.5] exactly.
|
||
Update fn **FUN_004553ac** re-parents each player's PNAME plate into row node `pnameGeo[rank]`
|
||
(rank from the per-player attr at `player+0x1cc`, players indexed by `player+0x1e0`), so the
|
||
rows SORT live. Row pitch = **0.24** (double @0x004551b8), rows DCS scale 0.12 @ (0.22,0.16,−0.5),
|
||
root DCS scale (0.2,0.2,1.0) @ (0,−0.15,−0.5). Materials/atlas map + the runtime-composite
|
||
mechanism: [[bgf-format]] §"The PNAME/PLACE name-plate atlas". The separate single-PNAME
|
||
consumer is `BTReticleRenderable` @004cc40c (no PLACE) — the in-cockpit designator label,
|
||
now **RECONSTRUCTED (2026-07-24, rig-verified)**: see [[gauges-hud]] §Lock ring.
|
||
The port's ranking display already ships the **2D** equivalent (egg ordinal+callsign bitmaps,
|
||
`CameraShipHUDRenderable::Render`), so this 3D chain is redundant for the scoreboard.
|
||
NOTE on the "PLAYER n" art: those baked words are the SHIPPED FALLBACK. The pod overwrites the
|
||
atlas texels at runtime with the egg's 1bpp callsign rasters — the original source survives
|
||
commented out at `engine/MUNGA_L4/L4VIDEO.cpp:5682-5710` (`FlushBitSliceTexture` →
|
||
`dpl_TexmapTexels2D(…,128,64,4)` + the warning "textures for player names not defined"). So an
|
||
authentic plate reads the player's CALLSIGN; "PLAYER n" only appears when the egg carries no
|
||
name bitmap. [T1]
|
||
- ✅ **Explosion::SplashDamage @0042fad0 — RECONSTRUCTED (task #62, 2026-07-13) [T1/T2].** ONE call
|
||
site, **Missile::Perform (FUN_004bef78, part_013.c:10097)**, in the collision branch (fires on ANY
|
||
missile impact — mover OR world), gated on `missile+0x360`, radius = `missile+0x364`. **Radius source
|
||
corrected:** it is the ROUND's OWN GameModel (type-0xf) `+0x50`, seeded from the launcher's linked
|
||
**AmmoBin `ammoModelFile @0x1e8`** (part_013.c:8778 → missile ctor SearchList @10184) — NOT the
|
||
launcher's `ExplosionModelFile`. The MissileThruster parser `FUN_004bf8ec` writes `"SplashRadius"` to
|
||
+0x50 (size-0x54 record). Burst falloff `baseBurst / dist^exp` floored at 1 (arcade 1.25 / WinTesla
|
||
`EXPLODE.cpp:209` 1.2f). Only missiles splash (AC tracer is not a Missile). The engine `EXPLODE.cpp`
|
||
diff is DONE — the T0 source confirmed the model; the only 1995-vs-WinTesla drift is the exponent.
|
||
Port: `BTResolveSplashRadius`/`BTApplySplashDamage` (mech4.cpp), see [[combat-damage]]. **Remaining
|
||
T3:** the per-player enable sub-gate `missile+0x360 = BTPlayer+0x264` (its writers read as a per-frame
|
||
toggle, not a config flag; the `showDamageInflicted@0x264` label is a guess) — port treats authored
|
||
`SplashRadius>0` as the enable. The mech's `DeathSplashDamage`/`DeathSplashRadius` (mech+0x520/0x524)
|
||
stay **VESTIGIAL** (written, never read; no mech-death splash mechanic — death is visual only).
|
||
- **The binary's own VelociRender BGF/BMF/BSL/VTX loader (~4,550 ln, dead code)** — a free
|
||
ground-truth cross-check for [[bgf-format]]/[[asset-formats]] claims.
|
||
Full ranked audit: the workflow output (2026-07-13); top-10 ranking cross-checked against this
|
||
register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTORY in the KB
|
||
(task #8 "landed" vs the kShotDamage=12 bring-up residue) — audit before reworking.
|
||
|
||
## Deferred subsystems / feeds (authentic path scoped, marked in code)
|
||
- **Revolving-door Phase 2 (true mid-mission drop-in) — PLANNED, gated on a spike**
|
||
(`docs/REVOLVING_DOOR_PLAN.md`, 2026-07-24). Open questions before any engine work:
|
||
(a) can an `Entity::MakeMessage` be REGENERATED post-birth (serializing current state), or
|
||
does the make path assume creation-time state? (b) what does the replicant-creation path do
|
||
with an already-known entity ID (idempotent, or double-create — needs a guard)? (c) where
|
||
exactly is the all-roster-hosts-connected launch wait, and can absent seats stay `Opening`
|
||
without stalling? Go/no-go = the 2-node late-attach spike in the plan doc. Phase 1 (round
|
||
carousel) is console-only and has no engine unknowns. [[multiplayer]]
|
||
- **Glass panel dead-button backlog (input-coverage audit 2026-07-20) — ✅ CLOSED 2026-07-26.**
|
||
The 8 buttons that dispatched a streamed message with NO reconstructed handler are all wired
|
||
now; this entry stays as the index of where each one landed:
|
||
0x13 → Mech `DuckRequest` @0049fa00 (**CROUCH — wired 2026-07-26**, the last one; see below),
|
||
0x28 → Mech `BalanceCoolant` @0049f728 (2026-07-21, issue #20),
|
||
0x1A-0x1D → Generator A-D `ToggleGeneratorOnOff` @004b1ed0 (`powersub.cpp`),
|
||
0x12 → ThermalSight / 0x14 → Searchlight `ToggleLamp` (both 2026-07-25, #61),
|
||
`ToggleCooling` @004ad6f8 msg 3 (2026-07-20, `heatfamily_reslice.cpp`),
|
||
`ToggleSeekVoltage` @004ba478 msg 0xb energy (`emitter.cpp`) / @004b8a48 msg 9 Myomers,
|
||
`EjectAmmo` @004bb9b8 msg 0xb ammo (`projweap.cpp`).
|
||
Still open from that census: **MechRIOMapper's own Keypress @004d2514 (id 0x19)**.
|
||
Tables in [[decomp-reference]] §2026-07-20; census in `docs/GLASS_COCKPIT.md`.
|
||
⚠ 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,
|
||
not the census, before reconstructing anything here.
|
||
- **`DuckState` has no CODE consumer, and that is authentic [T1].** `Mech::DuckRequest` sets
|
||
`duckState`(attr 0x37, binary `mech+0x398`) to 1 and nothing else: the flag has exactly two
|
||
writers in the whole binary (that handler and the mech reset) and ZERO readers. Its consumer is
|
||
a DATABINDING — `content/GAUGE/L4GAUGE.CFG` drives a 3-frame `bduck.pcc` widget off `DuckState`
|
||
on the map's legend column, verified live (the crouch icon lights grey→orange on a press). So
|
||
the button is COMPLETE as a request flag + indicator. What is NOT known: whether the 1995 game
|
||
ever consumed it for posture/collision (no SQUAT clip name survives in the decomp or in
|
||
`content/`, only `DuckServo01.wav` in AUDIO1.RES). Do not invent a crouch pose to "finish" it.
|
||
- **Searchlight-driven fog swap — STILL DEFERRED, but the "ORIGINAL 1995 LATENT BUG" premise is
|
||
❌ RETRACTED (2026-07-25, #61).** The arcade swaps fog between `fog=` (lights on) and
|
||
`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
|
||
compared Searchlight's Performance (@004b841c, reads `requestedOn`@0x1E0) against **ThermalSight's**
|
||
`ToggleLamp` (@004b860c → `0x1DC`), i.e. two different classes, and so invented a missing
|
||
"0x1DC→0x1E0 bridge". Searchlight's real handler is **@004b838c** (table @0x51117C) and toggles
|
||
`0x1E0`, the field its own Performance reads. [T1 attribution: `section_dump.txt:69661-69711`,
|
||
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
|
||
carries no novice gate. All T1.]
|
||
Searchlight's handler set is now 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
|
||
original" is void — a working fog swap is now plain FAITHFUL reconstruction. Remaining work is a
|
||
single item: construct `PullFogRenderable` at btl4vid.cpp `MakeMechRenderables` reticle-build/inside
|
||
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 --
|
||
FILED AS GITEA #65
|
||
(2026-07-25, found via #46).** Ten classes declare it (ammobin/heat×2/hud/mechsub/myomers/
|
||
powersub×3/projweap) as reconstructions of vtable-slot overrides, none `virtual` — a
|
||
base-typed `sub->HandleMessage(n)` statically binds to the base and SKIPS the override
|
||
(bit the `BT_BAYTEST` hook; fixed there with a typed AmmoBin call). No live base-typed
|
||
caller exists in the port today, so nothing is currently broken — but any future
|
||
reconstruction that dispatches subsystem messages through a base pointer (the binary's crit
|
||
path may) must first make the family virtual in one sweep (signatures must agree; some
|
||
declare `Logical`, some `void`+int variants — audit before flipping).
|
||
- **✅ #47 ENG-button attention FLASH BUILT same day (2026-07-25, evening).** All three pieces
|
||
landed: the REAL `BTL4GaugeAlarmManager` bodies (btl4galm.cpp, from @004cc294/@004cc2fc + the
|
||
lamp tables @0051cf1c..0x51d084), the sender (= `MechTech::TechnicalAssistance` @004ad33c —
|
||
the "gauge watcher" guess was wrong; MechTech's 7-condition edge-scan builds
|
||
`Start/StopEntityAlarmMessage` via @00436688/@004366b8 and broadcasts @004364e4; port shape =
|
||
direct `Start/StopEntityAlarmImplementation` calls on the gauge renderer), and the lamps (the
|
||
L4LampManager + streamed-mapping lamps already existed; the manager is now constructed +
|
||
assigned in btl4grnd). Verified live end-to-end (`scratchpad/lampflash.py`): bay fire →
|
||
`[techstat] LRM15_1 condition 4 SET (alarmModel 83)` → gotoEngineering lamp 0xD flashes 0x37 +
|
||
engEject 0xB → detonation clears → the bin reports Destroyed. FOUR load-bearing fixes en
|
||
route: `HeatableSubsystem`'s Derivation chained Subsystem directly (skipping MechSubsystem —
|
||
the whole tree failed the monitor filter); `MechSubsystem::GetStatusFlags` made virtual (binary
|
||
slot 12; static binding hid the weapon bits); `ProjectileWeapon::GetStatusFlags` raw-disasm'd
|
||
from the @004bbf88 export gap (jam→0x20, bin cookOffArmed→0x10); and the Mech::Reset sweep's
|
||
blanket `(MechSubsystem*)` cast filtered (it RespawnRepair'd MechTech through the wrong layout —
|
||
benign only while the monitor chain head was NULL; #47 populating it exposed a load crash).
|
||
Remaining risks: the wooHoo latch fields are still untested; FindLamp needs exact modeMask
|
||
equality so unflashed lamps on rare pages would betray a mapping-stream mask mismatch.
|
||
- **DEFERRED de-shadow: `MechSubsystem::damageZone` re-declares the PUBLIC engine
|
||
`Subsystem::damageZone` (SUBSYSTM.h:159) — gotcha #1, FILED AS GITEA #64
|
||
(2026-07-25, found via #47).** All recon
|
||
code coherently uses the derived member; the ENGINE base member stays NULL forever, and engine
|
||
base paths that use it (`Subsystem::TakeDamage` derefs it unguarded!) would AV if ever reached.
|
||
mechtech's monitor filter hit this (naive `sub->damageZone` resolved to the engine one → 0
|
||
monitors). De-shadowing = make the recon write the ENGINE member and delete the re-declaration
|
||
— a layout-neutral but call-site-wide sweep; do it deliberately, with the ReconDamageZone-proxy
|
||
offset-0-vptr alias (#46's crash) retired in the same pass.
|
||
- **(superseded) the original #47 remainder note follows for the record:** machinery fully mapped,
|
||
3 pieces unbuilt (2026-07-25).** The authored data SHIPS: `BTL4.RES` carries one
|
||
`GaugeAlarmStream` (type 31). The full chain + addresses: [[decomp-reference]] §GaugeAlarm.
|
||
To make it live: (1) reconstruct the REAL `BTL4GaugeAlarmManager` override bodies from
|
||
@004cc148..@004cc2fc (btl4galm.cpp is best-effort scaffolding and its provenance note —
|
||
"no override body exists in the image" — is WRONG; that code region IS the override,
|
||
including the ≥0x80 subsystem-relative eng-button lamp mapping); (2) register the
|
||
gauge-side StateIndicator watcher that sends `StartEntityAlarm` (id 7) on subsystem-alarm
|
||
transitions (the AUDWTHR parallel on the gaugeWatcherSocket); (3) build the LampManager
|
||
lamps for the aux/eng bezel buttons (LAMP.cpp/L4LAMP.cpp are T0-compiled; the glass panels
|
||
+ pod RIO both consume `RIO::SetLamp`, so the flash lands on-screen AND on the pod).
|
||
Decode the type-31 stream format first — it names which conditions flash which lamps.
|
||
- **PlayerStatus (the mission-review panels) reads the compiled player at RAW BINARY OFFSETS --
|
||
FILED AS GITEA #63 (2026-07-25, found via #48) [T1 code-read].** btl4gau3.cpp `PlayerStatus::Execute`:
|
||
`player+0x1FC` (vehicle), `+0x1C8` (score, drawn as a numeric), `+0x1C4` (alive/dead -- a
|
||
state flip draws a 160x240 filled box) -- all 1995 offsets, garbage on our compiled layout
|
||
(the databinding trap). DORMANT in-game (the review context no longer survives the viewpoint
|
||
swap -- the #48 teardown fix) but LIVE the moment the mission-review screen actually runs at
|
||
round end. Bridge all three through btplayer.cpp accessors before enabling the review screen.
|
||
- **`PoweredSubsystem` AutoConnect is DEAD, and vtable slot +0x40 is misattributed --
|
||
FILED AS GITEA #62 (root cause of the #21 field report)
|
||
(2026-07-25, found while auditing #47's GetStatusFlags change) [T1 decomp].** The binary
|
||
(@004b0bd0 tail) calls slot +0x40 with TWO shapes: `(this, 0)` in the outer gate (require
|
||
`== 0`) and `(this, candidateGenerator)` inside the roster walk (require `!= 0`). That is a
|
||
ONE-ARG voltage query -- "am I unpowered?" / "would THIS source power me?" -- i.e. the
|
||
`HasVoltage(source)` family, **not** `GetStatusFlags()`. powersub.cpp:358/372 model both as
|
||
the no-arg `GetStatusFlags()`, so the outer gate demands "no flags at all" while the inner
|
||
demands "some flag", and nothing between them can change the value -> **the loop body can
|
||
never execute and AutoConnect never attaches anything.** Pre-existing (both before and after
|
||
the #47 damage-tier fix); deliberately NOT fixed on release eve because it is a live
|
||
power-bus behaviour change that deserves its own playtest cycle. Fix = give slot +0x40 its
|
||
real signature (`Logical HasVoltage(Subsystem *source = 0)`), restore the two call shapes,
|
||
and verify a mode-Auto subsystem re-attaches after its generator drops.
|
||
- **Factory capability-roster loops 2-4 are STILL DEAD (task #57 discovery).** mech.cpp's
|
||
post-roster loops add to `heatableSubsystems`(0x51155c)/`weaponRoster`(0x511830)/
|
||
`damageableSubsystems`(0x50e4fc) through the local `SubProxy` stub whose `IsDerivedFrom`
|
||
RETURNS 0 — so those three mech-level rosters stay EMPTY (whatever consumes them sees
|
||
nothing; weapons demonstrably work through other paths, so audit consumers before assuming
|
||
impact). Loop 1 (the watcher CONNECT, 0x50e604) was reconstructed for real in task #57 —
|
||
use the same bridge pattern (family-side derivation test + typed body) for the other three. [T2]
|
||
- **Initial-snapshot torso record carries no twist extras**: the replicant's blind +0x10 read
|
||
can latch 0xCDCDCDCD ONCE at spawn until the first real twist record; clamps contain it
|
||
(invisible on fixed-torso mechs). Binary-authentic blind read — fix only if a visibly wrong
|
||
spawn pose ever surfaces. See [[multiplayer]]. [T2]
|
||
- **✅ 0xBD3 SubsystemMessageManager — UNTANGLED + LIVE (task #7, 2026-07-11) [T1/T2].**
|
||
Both halves landed: (1) the factory case builds the REAL messmgr (ctor @0049bca4, 0x130,
|
||
static reconstruction messmgr.cpp) cached at `mech+0x434` — the binary-wide census found
|
||
exactly ONE reader (@0x4b984b in `MechWeapon::SendDamageMessage` @004b9728, which was ALSO
|
||
mislabeled, as "DrawWeaponPip"); every mapper consumer re-pointed to roster slot 0
|
||
(`MappingMapper()`; `SetMappingSubsystem` @0049fe40 touches only slot 0); non-viewpoint mechs
|
||
carry a slot-0 demand LATCH ([T3] accommodation for the port's wider drive/gait reachability).
|
||
(2) The authentic CONSOLIDATED damage delivery is live: the beam path submits TakeDamage into
|
||
`AddDamageMessage` → per-frame `ConsolidateAndSendDamage` (@0049b784) builds ONE
|
||
`Entity::TakeDamageStreamMessage` (id 0x13, wire-verified 0x34+4+N×12) dispatched at the
|
||
victim, whose T0 handler (ENTITY.cpp:817) re-splits it; replicant victims reroute cross-pod.
|
||
Explosion bundling (weapon+0x3E4 per record, unique-queued, Explosion::Make at the impact)
|
||
implemented; TWO latent chain-purge bugs fixed (records re-applied every tick). RESIDUE
|
||
([T3], next task): the weapon-side submission with per-weapon `damageData` +
|
||
`inflictingSubsystemID` awaits the DAMAGE-ECONOMY reconciliation (authored 0.25-scale
|
||
amounts vs the bring-up kShotDamage=12); the 0.1s explosion stagger + the terrain-hit
|
||
derivation check are noted in messmgr.cpp.
|
||
⚠ The earlier "0xBD3 gates the valve/Myomers" claim remains WRONG (those read the owning
|
||
BTPlayer at mech+0x190 — see the next entry); the old wiring notes are superseded by this
|
||
landing.
|
||
- **✅ `mech+0x190` IDENTIFIED (2026-07): it is the owning `BTPlayer`** (`Mech::GetPlayerLink()`,
|
||
`MECH_OWNING_PLAYER`; ENTITY.h:430). Set by `FUN_0049f624` (the mech↔player bind: `mech+0x190 = player`
|
||
AND `player->playerVehicle(+0x1fc) = mech`), which resolves the player from the mission player registry
|
||
(`app+0x2c+0x54`, `FUN_0041fd18`) by the pilot key.
|
||
**✅ FLAG SEMANTICS PINNED (2026-07-18) [T1]: the `player+0x25c..0x274` block = the EXPERIENCE
|
||
LEVEL (egg `experience` novice/standard/veteran/expert), written by the BTPlayer ctor @4c0bc8
|
||
from `btMission(+0x1f8)->experienceLevel(+0xe4)` — full mapping, consumers, and the 4.0-manual
|
||
behavior in [[experience-levels]] (+ [[decomp-reference]] §3 flag table).** Resolved en route: the
|
||
old "resolve first" items — `FUN_004ad7d4` (labeled both `HeatModelActive` and `OwnerAdvancedDamage`)
|
||
is ONE body reading `player+0x260` = the heat-model master switch (veteran+expert); `FUN_004ac9c8`
|
||
reads `player+0x274` = the novice lockout.
|
||
**✅ THE WIRING LANDED (issue #2, 2026-07-20) [T2]:** the ctor now seeds from
|
||
`BTMission::ExperienceLevel()`/`AdvancedDamageOn()` via the inherited `Player::playerMission`
|
||
(the binary's +0x1f8; the old `btMission` phantom member removed), the members are renamed to
|
||
the true semantics (`simLive`/`heatModelOn`/`experienceLevel`/…), and all five gate stubs +
|
||
the FUN_004ac9c8 family (`MechSubsystem::IsDamaged`, `BT_IsBusLive`, powersub call sites) route
|
||
through the three complete-type bridges (`BTPlayerExperienceSimLive`/`…HeatModelOn`/
|
||
`BTPlayerRoleLocksAdvanced`). Every shipped egg says `experience=expert`, so default behavior is
|
||
unchanged. Detail: [[experience-levels]] §Port state.
|
||
- **✅ Authentic target acquisition RECONSTRUCTED (tasks #36/#39, 2026-07-08)** — the `Reticle`
|
||
pick-ray chain is LIVE (see [[combat-damage]] Targeting for the full port map): the crosshair =
|
||
**torso boresight** (NO free-aim mouse — the pod stick twisted the torso; you steer to aim) →
|
||
pick ray → per-frame lock (boresight ON the mech = locked; off = none) → aimed zone damage +
|
||
designator hotbox; `BT_FIRE_ARC` is opt-in-only now. Firing needs a target but NOT a manual
|
||
lock/pinpoint — the target is AUTO-acquired (task #40, binary-verified: FireWeapon doubly-gated
|
||
on mech+0x388, but 0x388 has 11 reads / 0 direct stores across CODE → set indirectly). Residue:
|
||
(a) the binary's own per-frame reticle→mech copy + the AUTO-TARGET SELECTOR are still in an
|
||
un-exported gap (a message/selector; our port auto-targets the single living enemy — for MP,
|
||
reconstruct the real multi-target selection: nearest/most-aligned/cycle); (b) the eyepoint now
|
||
rides the parent DCS (task #55 authentic eye), so torso pose reaches the view; on a
|
||
TWIST-CAPABLE mech (selectable via `BT_FORCE_MODEL`) the crosshair should deflect with the
|
||
torso twist (`BTTwistToReticleX` is wired for it) AND the torso should visibly lead the legs —
|
||
verify the reticle deflection on a twist mech (the BLH is fixed-torso, boresight always
|
||
centred); (c) pre-burial, the pick still
|
||
tests the sinking wreck's collision box at its parked position (pre-existing wreck-targeting
|
||
behavior).
|
||
- **✅ WORLD STRUCTURES (garages/walls) now targetable (task #50, 2026-07-15)** — the world-pick's
|
||
non-mech tier previously only sampled the visual heightfield (`BTGroundRayHit`), which on arena1
|
||
is a single flat `'sky'` ground mesh → shots passed THROUGH the garages and only mechs moved the
|
||
range axis (user-reported regression). Resolved by ray-testing the **zone's static collision
|
||
solid tree** (the geometry that already blocks the walk) via the authentic engine query
|
||
`Mover::FindBoxedSolidHitBy`'s static-world tail — factored into `Mover::FindStaticSolidHitBy` +
|
||
`Mech::WorldStructurePick`. See [[combat-damage]] "WORLD STRUCTURES ARE TARGETABLE". This also
|
||
closes the old "0x388 writer is un-exported / unknown world-pick mechanism" residue: the
|
||
authentic non-mech world pick is the static-solid-tree ray query (`BoxedSolidTree::FindBoundingBoxHitBy`),
|
||
the same one `FindBoxedSolidHitBy` uses for mover-vs-world collision. Interactive aim is
|
||
user-verified (headless has no `BTGetAimRay`); the `BT_WSWEEP` ray-fan is the headless proof.
|
||
- **Cockpit HUD leftovers (tasks #35-#38 residue, 2026-07-08)** — the reticle + 7 weapon pips +
|
||
ALL the instrument dynamics are LIVE (the Execute @004cdcf0 gap was RECOVERED via capstone
|
||
disasm, task #37; the authentic Lock producer + the simple-X mode landed in #38 — see
|
||
[[gauges-hud]] for the full instrument map). Remaining: (a) the **`*_cop` canopy shell** —
|
||
SOLVED for all 8 (task #55): authentic eye + the PUNCH stencil-cut kit + double-sided cop
|
||
geometry + unlit constant frame colour (`BT_COP_FRAME`) — see [[cockpit-view]] Rendering/FINAL;
|
||
shows by DEFAULT now (`BT_HIDE_COCKPIT=1` hides). NOTE the old claim here ("canopy windows are
|
||
punch texels") was WRONG — the shell has no texture; the openings are geometry. ✅ RESOLVED
|
||
(2026-07-11): PUNCH = a 3-chunk STENCIL-CUT kit (mask/hull/twin, i860-firmware-decoded)
|
||
executed as a D24S8 stencil cut; visible canopy = hull-minus-apertures ([[cockpit-view]]
|
||
§FINAL). All 8 mechs render connected dark frames with clear viewports (the intermediate
|
||
"frameless bhk1/loki/vulture/avatar" reading was wrong); thor matches the pod footage exactly.
|
||
Eye placement verified exact per-mech incl. Thor's +1.13 offset cockpit ([[cockpit-view]]). (b) ~~the **3D marker
|
||
chain** ([0x31c]/[0x320] dpl objects + the PNAME1-8.bgf player-name meshes over the locked
|
||
target — matters for MP)~~ **DONE 2026-07-24: the target NAME PLATE is reconstructed +
|
||
rig-verified (the target's callsign under the crosshair, lock-gated) — [[gauges-hud]] §Lock
|
||
ring.** (c) the compass rotation SIGN is derived-not-verified [T3] (turn
|
||
left → the stem should swing right; flip the sin sign in Draw if footage disagrees).
|
||
- **✅ StatusMessagePool / MessageBoard — LIVE (2026-07-12) [T1/T2].** Decoded: FUN_0042e580 =
|
||
the engine `Player::AddStatusMessage` (chain add at +0x1e4); FUN_0042e5a0 = `StatusMessageUpdate`
|
||
(+0x1dc = the displayed message; +0x14 displayTime countdown, expiry-deleted) — ALL already in
|
||
the engine [T0 PLAYER.cpp]. The ONE binary producer = BTPlayer::ScoreMessageHandler's KILL
|
||
branch (@004c02e4 type-2): pool-alloc @00512f6c + ctor(victim's player, strip 0, 6.0s) +
|
||
AddStatusMessage. Port: engine `new Player__StatusMessage` + Register_Object (the engine
|
||
reclaims with delete — the binary pool is an allocation detail); consumer =
|
||
BTResolveMessageBoard reads the mission player's statusMessagePointer + resolves the name via
|
||
Mission::GetSmallNameBitmap; the board's SetSource binder (no recovered caller) is replaced by
|
||
a lazy viewpoint-entity bind in Execute. Ticker pops on BOTH nodes (each node's death
|
||
transition posts its local KillScore).
|
||
- **Gyroscope (0xBC4) — RE-ENABLED LIVE (task #56, 2026-07-10).** The NaN revert's root causes
|
||
are fixed byte-exact (ctor field map @004b3778 — springConstant@0x1E8/dampingConstant@0x1F4
|
||
were mislabelled, the 0x254-0x2B3 block was missing, clamps/accumulators uninitialised; the
|
||
integrators' state-minus-target/componentwise-damping-overwrite/X-Z-crossing semantics; the
|
||
writers' one-node-two-channels form; dispatch from the MECH performance tail, not the gyro
|
||
Performance). Layout locked (sizeof==0x3D0); runtime clean (joints type 5, finite state, no
|
||
NaN). Empirical: spring targets are SYMMETRIC → eye equilibrium (0,0,0) → the gyro is a
|
||
±0.1-0.15u BOUNCE mechanism, NOT a steady offset (the earlier hypothesis is disproven).
|
||
**FAN-OUT LANDED (2026-07-11):** FUN_004b2980 re-disassembled from raw bytes and reconstructed
|
||
(`Gyroscope::ApplyDamageResponse`) + wired at the take-damage hub, the crushable crunch and the
|
||
firing recoil — the hit-BOUNCE is live and verified (damped eye oscillation, no NaN).
|
||
REMAINING (task #56 tail): (a) the alternate-gait engage jolt + engaged-gait rumble
|
||
(@4aa158-4aa365 — [T3] gate naming, mutates the #49/#50-stabilized gait machine; byte recipe in
|
||
the wf_6880e605 synthesis); (b) the mech+0x3F0 overspeed sway model (swayBias fed 0); (c) the
|
||
per-frame `EyepointRotation.pitch = torso pitch` writer (FUN_004b66b4) + mapper glance-look
|
||
states (FUN_004afd10 — yaw/pitch from mech+0x564..0x570); (d) senders filling
|
||
`Damage::damageForce` for directional bounce (random-fallback is binary-legal meanwhile).
|
||
Also: `deathAnimationLatched`/`legResetLatch` were never ctor-initialised (0xCDCDCDCD gate bug,
|
||
fixed in mech.cpp ctor). See [[cockpit-view]].
|
||
- **MechControlsMapper look/eyepoint commit** — reconstructed but its offsets collide with declared
|
||
members; arbitrate before enabling (no port consumer yet). (The fire-trigger-through-mapper is NO
|
||
LONGER the bring-up: the `gBT*Trigger` globals are retired / never-read — the authentic LBE4
|
||
buttonGroup path replaced them, task #5 `8ed6184`.)
|
||
- **✅ Weapon groups — the AUTHENTIC defaults + fire path LIVE (task #5, 2026-07-11) [T1/T2].**
|
||
The system decoded end-to-end: a fire "channel" = a physical `buttonGroup` slot in the global
|
||
`LBE4ControlsManager` (fire buttons 0x40 Trigger / 0x45 Pinky / 0x46 ThumbLow / 0x47 ThumbHigh);
|
||
binding = a permanent mode-0x10000 DIRECT mapping button → the weapon's **TriggerState** attr
|
||
(id **0x13**, = `fireImpulse@0x31C`; the binary MechWeapon table's ONLY attribute @0x511890);
|
||
the weapon self-edge-detects (CheckFireEdge @4b9608). **The default grouping is NOT messages —
|
||
it is the per-mech type-6 controls-map resource in BTL4.RES** ("Thrustmaster"/"L4" variants;
|
||
e.g. madcat: Trigger={4 weapons}, ThumbHigh={2}), installed at `MakeViewpointEntity` by the
|
||
ENGINE's `CreateStreamedMappings` (T0, L4CTRL.cpp) — which the port already called; it needed
|
||
only the TriggerState attribute published (id PINNED to 0x13 — our attr-id chain is 6 short of
|
||
the binary's; full binary attr-table dump in the task #5 scan) + an input feed. Keyboard now
|
||
pushes press/release edges into the buttonGroups (SPACE→Trigger, '2'→ThumbLow, '3'/CTRL→
|
||
ThumbHigh; BT_AUTOFIRE pulses the Trigger); the gBT*Trigger bypasses + pulse hack are retired.
|
||
VERIFIED 2-node at the BINDING level (⚠ honest correction, task #8: those kills still flowed
|
||
through the retired mech4 bring-up block -- the emitter DISCHARGE chain was latched shut by
|
||
the NaN edge bug below until 2026-07-11; the full weapon-fire chain is now verified solo).
|
||
- **✅ THE CONFIG-MODE SESSION — RECONSTRUCTED (task #6, 2026-07-11) [T1 anchors].** The
|
||
pod's in-cockpit weapon-REGROUPING UI, fully decoded from the binary + T0 engine:
|
||
* **The flow**: the pilot HOLDS a weapon's configure button (a per-weapon type-6
|
||
EventMapping, id 9, on the MFD-quadrant aux panels; e.g. BTL4.RES @0xAA3DC) →
|
||
MechWeapon handler id 9 "ConfigureMappables" @004b9550 (table @0x511860; the old
|
||
"Myomers fns" label swept) → mapper(slot 0)->EnterConfiguration @004d1840 (vtbl+0x38):
|
||
StartMappableButtonsConfigure (T0 CONTROLS.cpp:291 -- latches the active weapon for the
|
||
gauge, flips the GLOBAL mode NonMapping 0x10000 → Mapping 0x8000, so every normal fire
|
||
mapping goes dormant), re-arms the held button (+0x8000, id 9), temp-maps the four fire
|
||
buttons 0x40/0x45/0x46/0x47 → id 10. Tapping a fire button → handler id 10
|
||
"ChooseButton" @004b95b8 → RIO AddOrErase(direct) @004d262c: buttonGroup[v-1].AddOrErase
|
||
(0x10000, &fireImpulse) — a TOGGLE; the permanent 0x10000 instances ARE the commit.
|
||
Release → ExitConfiguration @004d18dc: StopMappableButtonsConfigure + strip the 0x8000
|
||
layer. Only the RIO (pod) mapper implements AddOrErase; L4/Thrustmaster carry RET
|
||
no-ops @004d195c/@004d1964 (a joystick cockpit cannot regroup).
|
||
* **Vtable truth (swept)**: base @0050F45C +0x38 Enter(Fail @004b0280) / +0x3C Exit(Fail
|
||
@004b029c) / +0x40 AddOrErase-event(Fail @004b02b8) / +0x44 AddOrErase-direct(Fail
|
||
@004b02d4) / +0x48,+0x4C Notify pair. There is NO "secondary vtable @0050f498" and NO
|
||
"CreateTemporaryEventMappings" virtual (RP-name drift). @004afbc4 (the mapper's shared
|
||
aux/zoom handler) is a pure Fail thunk ("Unhandled button mapping!", line 0x7a), not an
|
||
AddOrErase router.
|
||
* **Port landed**: MechWeapon handlers 9/10 + the GetMessageHandlers() accessor chain
|
||
(MechWeapon → Emitter/ProjectileWeapon → MissileLauncher, + GAUSS/PPC.CPP; dispatch
|
||
resolves through the CONCRETE class's SharedData — the empty per-class sets silently
|
||
swallowed the messages); the 6-arg Enter/1-arg Exit reshape; the L4 Enter/Exit full
|
||
bodies; MechSubsystem +0xE8/+0xEC corrected to controlDestination/controlMessageID (the
|
||
old hostEntity/subsystemId2 labels were wrong; MechWeapon ctor defaults the destination
|
||
to &fireImpulse per @004b99a8); dev harness = HOLD 'G' (BT_CONFIG_SLOT selects the
|
||
weapon; BT_CONFIG_TEST scripts a headless session).
|
||
* **ConfigMapGauge finding [T1] — CORRECTED 2026-07-21**: the old "authentically DORMANT /
|
||
no SetColor caller" claim was WRONG. @004c6ee0 is the virtual GaugeBase::LinkToEntity
|
||
override (vtbl @0051a1b8 slot 9 +0x24; @0x94 = linkedEntity, the Execute gate), and the
|
||
engine broadcasts LinkToEntity(viewpointEntity) at viewpoint bind (APP.cpp:1277 →
|
||
GAUGREND.cpp:3011) — the gauge IS live in the shipped game. The state loop
|
||
(@004c6f1c: 4 {y,button} pairs from DAT_00518eb4 = {13,Pinky}{37,ThumbLow}{61,Trigger}
|
||
{85,ThumbHigh}, x=0xc; buttonGroup[btn].GetMapState(controlDestination, subsys,
|
||
controlMessageID, 0x10000) → cm_off/cm_other/cm_only/cm_both) now runs on the authentic
|
||
LinkToEntity path; the BT_CONFIGMAP dev enable was deleted.
|
||
* **Bonus [T1]**: the binary MechWeapon ATTRIBUTE table @0x511890 has ELEVEN entries
|
||
(0x12 PercentDone .. 0x1C WeaponState) — future gauge-wave feeds; the "TriggerState is
|
||
the only MechWeapon attribute" claim was wrong (fixed in mechweap.hpp).
|
||
* Tails: weapon+0x110 (write 0/-1 on session open/close; port name vitalSubsystemIndex)
|
||
has NO confirmed reader yet; @004d1acc (+0x48 L4 Notify override) is unexported —
|
||
re-export from Ghidra.
|
||
* **Task #12 (2026-07-11) [T2]: the PoweredSubsystem POWER-ROUTING handlers landed** --
|
||
ids 4-8 registered (table @0x50F4EC): SelectGeneratorA-D = FindGeneratorByNumber
|
||
(@004b0b18 ROSTER walk matching generatorNumber@0x1E0) -> AttachToVoltageSource ->
|
||
modeAlarm=Connected; ToggleGeneratorMode (@004b0abc) = <2 -> AutoConnect, ==2 ->
|
||
Detach + ManualConnect. Weapons inherit via the MechWeapon handler chain (the old
|
||
qualified GetMessageHandlers() calls silently resolved to the engine Receiver ROOT
|
||
set -- same trap as the task #6 weapon handlers). The AutoConnect hunt's
|
||
GetSegment() scan corrected to the subsystem ROSTER (the +0x128 gotcha). Desktop:
|
||
F5-F8 assign the selected weapon (BT_CONFIG_SLOT) to Generator A-D, F9 toggles
|
||
Manual/Auto; BT_GENSEL_TEST scripts a headless re-tap. Note: Myomers/Sensor still
|
||
chain the root set (their binary tables unverified) -- extend if aux maps prove
|
||
they receive these ids. TWO MORE TRAPS found landing this: (a) MESSAGE_ENTRY
|
||
tables must be function-local statics INSIDE the accessor (cross-TU static-init
|
||
order emptied the table -- see [[reconstruction-gotchas]] #9 last bullet); (b)
|
||
**FUN_004ac9c8 is NOT IsDamaged** -- raw body: `owner(+0xD0) -> mech+0x190 player
|
||
-> +0x274 == 0` = the NOVICE-experience lockout for advanced cockpit systems
|
||
(2026-07-18 correction: +0x274 = the egg experience level, NOT a role class --
|
||
see [[experience-levels]]; port bridge BTPlayerRoleLocksAdvanced, btplayer.cpp;
|
||
NULL player = unlocked [T3]; seeded from the egg experience since issue #2).
|
||
✅ AUDIT TAIL CLOSED (issue #2, 2026-07-20): every powersub.cpp site annotated
|
||
FUN_004ac9c8 (PoweredSubsystem::HandleMessage @4b0efc, ForceShortRecovery
|
||
@4b11bc, Generator::HandleMessage @4b21d0, PowerWatcher::HandleMessage @4b179c)
|
||
plus MechSubsystem::IsDamaged and BT_IsBusLive now call the bridge; the
|
||
state-based HeatSink::IsDamaged shadow remains only for the gauge tint.
|
||
- **Myomers authentic coupling** — the structural un-stub is INERT (mover feed + heat-gen no-op).
|
||
The heat-model gate (`OwnerAdvancedDamage`/`FUN_004ad7d4` → the owning **BTPlayer**
|
||
`mech+0x190`+0x260 = veteran/expert experience — see [[experience-levels]]) is WIRED since
|
||
issue #2, but the sim stays arithmetically inert (the Owner* motion accessors return neutral
|
||
0s). Real coupling still needs those accessors + `MoverAttach` routing into the LIVE
|
||
JointedMover (must be reconciled with the gait cutover first).
|
||
- **SeekVoltageGraph** — 4 Seek* attrs unpublished (a cluster-child, not config-called; non-blocking).
|
||
|
||
- **✅ DAMAGE ECONOMY — AUTHENTIC (task #8, 2026-07-11) [T1/T2].** The whole economy closes:
|
||
`_DAT_004bafbc` is an x87 float80 = **1e-7** (the port's kDamageScale=1.0 was the mystery) --
|
||
it cancels the ctor's ×1e7 energy bookkeeping, so **damagePortion = authored DamageAmount ×
|
||
(charge/seekV[rec])²** (port: the closed form at fire time; the bring-up charge cycle keeps
|
||
the degenerate EC=1 electrical model). Authored values (BTL4.RES type-0x11, +0x19C; scanner
|
||
scratchpad/scan_weapsub.py): madcat AC=25/LRM15 salvo=50/ERLL=6/ERSL=2; bhk1 PPC=12/SRM6=35/
|
||
ERML=3.5. Zone model already byte-exact (Δlevel = amount/armorPoints, legs ×0.5; armor dump
|
||
in the task #8 synthesis) → the pod's 4-8-hit heavy-weapon pacing. The weapon-side
|
||
SendDamageMessage (@004b9728) is LIVE from Emitter::FireWeapon (damageData filled incl.
|
||
**damageForce = target−muzzle → the gyro's directional hit-bounce feed**); the mech4
|
||
bring-up damage block + flat kShotDamage are retired to diag hooks **(CORRECTED, task #60:
|
||
`kShotDamage=12` was only HALF-retired — still LIVE at the kill-score `mech4.cpp:1551` until
|
||
task #60 replaced it with `lastInflictingDamage`, the real killing-blow magnitude; only the
|
||
cross-pod force hook `mech4.cpp:3255` was env-gated. See the task #60 reconciliation below).**
|
||
**ROOT-CAUSE find: the
|
||
CheckFireEdge NaN latch** -- TriggerState carries ControlsButton INTS (release −65 = a
|
||
negative NaN); the binary's x87 unordered-compare treated it as "released" but an
|
||
IEEE-correct float compare latched the detector shut after the first release (why the
|
||
emitters never discharged in-game until now); fixed with bit-pattern sign compares
|
||
(@004b9608). Residues [T3]: heat stays on the bring-up scale (authentic = heatCost×1e7
|
||
energy units, the missile path already feeds it -- the HEAT-CALIBRATION audit);
|
||
LODReuseHysteresis corrected 0.82→0.33 (a double); the MP beam-kill live-verify awaits a
|
||
clear-sightline spawn (mechanism identical to solo; force-dmg cross-pod cycles verified).
|
||
|
||
- **✅ DAMAGE-ECONOMY RECONCILIATION (task #60, 2026-07-13) [T1/T2].** Full 5-path audit vs the
|
||
decomp (5-finder + adversarial-verify workflow). **Clean as-is:** energy beam (`emitter.cpp`,
|
||
closed form byte-exact; `_DAT_004bafbc` dumped = x87 1e-7), autocannon (`projweap.cpp`, full
|
||
authored DamageAmount from resource +0x19C; the 0.0625 at :667 is the shooter's own gyro recoil,
|
||
not the round), zone-armor BASE model (`damageLevel += amount·damageScale[type]`, engine
|
||
DAMAGE.cpp:379, legs ×0.5, 1.0=destroyed). **3 stand-ins FIXED:** (A) `mechdmg.cpp:451`
|
||
`mech->stance` (phantom, perma-0 → the leg-shot-out fall/death branch was DEAD) → `MovementMode()`
|
||
(mech+0x40, @part_012.c:6910); (B) `mechdmg.cpp:458` `IsAirborne()` (always-0 stub) → `IsDisabled()`
|
||
(@0049fb54) — the task-#52 wreck-graphic corruption fixed AT SOURCE (was only masked by the
|
||
IsMechDestroyed latch); (C) `mech4.cpp:1551` flat `kShotDamage=12` kill-score → `lastInflictingDamage`
|
||
(real killing-blow, latched in TakeDamageMessageHandler; the score handler @0x4c02e4 derives the
|
||
whole kill award from it, so a flat 12 scored every kill identically). Phantom `int stance` slot
|
||
reused for `lastInflictingDamage`. **KB corrected:** the "damageScale[5] EVEN=1/armorPoints"
|
||
prose is a mild inaccuracy — the array is 5 cells indexed by damageType directly
|
||
(Collision/Ballistic/Explosive/Laser/Energy); the per-type value IS ≈1/armorPoints for the
|
||
destructive types (net model unchanged). See [[combat-damage]].
|
||
- **DEFERRED (task #60-D): the missile CLUSTER model.** The port fires N flying rounds each
|
||
carrying `authored/N` damage (net salvo total = authored, so per-armor damage is correct), but
|
||
the binary (@004bcc60) fires ONE missile whose `burstCount` is a RANDOM cluster-hit roll
|
||
(`min(rand(0..N-1)+N/4, N)`, FUN_004bef78) applied to ONE cylinder-resolved zone. The port thus
|
||
loses (i) cluster-hit VARIANCE (always full N) and (ii) single-zone CONCENTRATION (N rounds
|
||
scatter across zones). Net-neutral on total armor, so LOW priority. **OPEN semantic gating the
|
||
severity:** does `burstCount` multiply ARMOR damage or only the gyro kick? DAMAGE.h:49-54 [T0]
|
||
says "times to apply the damage" (→ apply-count → port total authentic); settle by reading the
|
||
consumer chain FUN_004bef78 → FUN_004be078 → EXPLODE.cpp:209-210 (does it loop burstCount over
|
||
DamageZone::TakeDamage or apply once?). Fix only if made byte-faithful (roll clusterHits, thread
|
||
`burst` through BTPushProjectile, one damage-bearing round + rest visual-only).
|
||
- Also open (bring-up, not stand-ins): `scoreAward=0` (btplayer.cpp:1526 — the authentic kill
|
||
bonus lives in the un-exported master-perf writer 0x4a9770-0x4ab188, T4); the exact authentic
|
||
kill `damageAmount` from that same writer (msg+0x24) is unrecovered — the killing-blow magnitude
|
||
is the faithful stand-in until it is.
|
||
|
||
- **HEAT (task #9, 2026-07-11): authentic 1e7-unit heat LIVE; conduction ROUTING defect
|
||
remains [T2/T3].** Landed + byte-anchored: emitters emit `heatCostToFire x 1e7 x
|
||
(charge/seekV)^2` (closed form; PPC = 1.1e8 -> +632 K on its 174000-mass sink); the missing
|
||
projectile/missile heat adds (raw pre-scaled resources); the bank's AMBIENT RADIATOR @4ae73c
|
||
(the system's only heat exit: relax toward 300 K, conductance x 0.1 x HeatSinkCount
|
||
[_DAT_004ae974 float80 = 0.1 -- the audit's "1.7375" was a misread]); the link-attach guard
|
||
corrected (skip = the 0xBBE bank, NOT Condenser -- the inversion had closed the system);
|
||
three coolant epsilon corrections (0.0025/0.003/1e-4 for the old single 1e-4) +
|
||
CoolantCapacityScale = 0.05 float80 (both prior readings wrong). Verified: heat flows,
|
||
exits (~2/3 of a 1e10 spam run drained), weapons overheat under max-rate autofire exactly
|
||
as the authored-constant simulation predicts (thermal spam is unsustainable BY DESIGN; the
|
||
authentic overheat cutout is the FailureHeat CHARGE-HOLD -- `ResetFiringState` + currentLevel=0
|
||
until it cools, @004baa88 -- NOT a range collapse. ⚠ CORRECTION (`aab7a8a`, task #50):
|
||
effectiveRange = (1 - HOST-ZONE damage) x weaponRange reads `Subsystem::damageZone->damageLevel`
|
||
(weapon+0xE0), NOT heatLoad; the old "heatLoad -> effectiveRange 0" mechanism here was the same
|
||
@0xE0-DamageZone-vs-heat misattribution fixed in heat.cpp:803).
|
||
- **Task #10 -- the "scrambled routing" RESOLVED (2026-07-11) [T2].** The routing was NEVER
|
||
scrambled: a `[heat-link]` attach log proved every subsystem links its authored sink
|
||
exactly (bhk1: PPC_1->C4, PPC_2->C6, ERM_1/2/3->C1/C6/C4, SRM6s->C2, generators->C1/C2/C3/C5,
|
||
condensers+reservoir->bank@slot2). The "pools in Condenser1" observation was the
|
||
DIAGNOSTIC-SAMPLER ALIASING trap (one shared static 1-Hz timer, pendingHeat-gated); the
|
||
census log is now per-instance. The REAL defect was the fire RATE: emitter.cpp's local
|
||
`FUN_00417ab4` stub returned NULL, so although the PoweredSubsystem ctor attaches the
|
||
authored generator to `voltageSource@0x1D0`, the Emitter TU could never see it -- the whole
|
||
electrical model was inert and the E7 force-charge recharged every emitter in ONE frame
|
||
(~0.3 s cycle, 1501 fires/90 s, ~1.7e9 heat/s input). Fixed authentically [T1, disasm +
|
||
byte-verified constants]:
|
||
* Emitter ctor @004bb120: seekVoltage[i] = authored fraction x generator ratedVoltage
|
||
(authored 10000); EC = energyTotal/(seekV[rec]^2 x 0.5); **voltageScale@0x310 =
|
||
(RechargeRate / -ln(1 - 0.0001 x seekV)) / EC** -- the exponential charge reaches
|
||
seekV[rec] in EXACTLY the authored RechargeRate on a cold generator (PPC 5 s, ERLLaser
|
||
4 s, SRM6 3 s, ERMLaser 2 s, LRM15 6 s). The gate reads the OWNER's simulationFlags
|
||
(the usual ctor-gate gotcha; the old this-flags read never armed).
|
||
* `PoweredSubsystem::ChargeTimeScale()` (@004b0d50, was a =1.0 stub): voltageScale x
|
||
(1 + thermalResistivityCoefficient x max(0, srcT - srcT0)) -- a HOT generator charges
|
||
slower ("voltageScale is never read back" was wrong -- this reads it every tick).
|
||
* TrackSeekVoltage @004ba838: charging I^2R (`seekRate^2 x dtScale x dt`) lands in the
|
||
GENERATOR's pendingHeat@0x1c8 (~3.5e8 per full PPC charge -> generators self-heat ->
|
||
conduct to their condensers -> slow further charging). This closes the heat/firepower
|
||
feedback economy.
|
||
* **FailureHeat consumers found [T1, disasm]:** `this+0x184` (the weapon's own heatAlarm
|
||
status level) == 2 gates BOTH families -- @004baa88 (emitter: ResetFiringState +
|
||
currentLevel=0 until it cools) and @004bbd36 (ballistic: recoil pinned to rechargeRate +
|
||
alarm state 7). Emitter::GetFaultState() now returns heatAlarm.GetLevel() (was a 0 stub).
|
||
* ProjectileWeaponSimulation @004bbd04 opens with `call 0x4b0bd0` (disasm-verified) -- the
|
||
launchers now run the powered/heat step (their task-#9 firing heat previously accumulated
|
||
in pendingHeat forever; SRM6s now census at ~950 under sustained fire).
|
||
Verified live (120 s max-rate autofire): PPC ~470-500 (was 55,000), bank plateaus ~600 and
|
||
sheds, generators 1100-1400, ERMLaser self-regulates at the 2000 failure threshold
|
||
(shutdown -> cool -> resume). Remaining tails: the HeatWatcher/AmmoBin cook-off
|
||
watchedLink; the mech-level heatAlarm@0x450 producer; the cockpit power-routing message
|
||
handlers (@004b099c..@004b0abc: assign weapon to Generator 1-4 + auto/manual toggle via
|
||
modeAlarm@0x2CC -- wired to buttons, not yet reachable).
|
||
|
||
- **Task #11 -- the live-play regressions (2026-07-11) [T2].** User-reported after the
|
||
electrical model landed. THREE distinct causes, all resolved:
|
||
(1) *Phantom fire on the enemy*: the mech4 bring-up shot block painted an explosion at the
|
||
victim on its OWN 0.3s cadence whenever fire was held -- desynced once real recharges landed.
|
||
RETIRED (the authentic impact visual flows from each real discharge via the messmgr's
|
||
SubmitExplosion). (2) *Weapons bricking one by one* ("fire ~1s then cuts out"): the
|
||
FRAME-PACING TRAP -- see [[reconstruction-gotchas]] §12: the Loading->Loaded snap window
|
||
(+-0.01 around seekV) assumes the pod's locked 60 fps; a dt spike jumps it, the overshoot
|
||
clamp zeroes rechargeLevel, and the weapon sticks in Loading at level ~10000 forever.
|
||
Fixed with pod-frame (1/60s) sub-stepping of the binary's own Loading tick (also fixes the
|
||
I^2R integral overfeeding generator heat on big steps). (3) *Slow kills / short fire
|
||
windows*: AUTHENTIC -- beams last the authored DischargeTime, recharges take the authored
|
||
2-5s stretched by hot generators (ChargeTimeScale), and max-rate autofire equilibrates
|
||
generators in the degradation band (1100-1500 K; the FailureHeat breaker never trips in
|
||
solo max-abuse -- duty-cycle measured). The pilot's authentic counters are the systems
|
||
under reconstruction: config-mode regrouping (task #6), generator reassignment (ids 4-8),
|
||
coolant valves.
|
||
|
||
- **✅ AUTOCANNON JAM / coolant-priority — FAITHFUL + CONFIRMED against QA ground-truth, SOLVED
|
||
purely from the decomp (2026-07-14) [T1/T2].** An original QA dev reported: "all AFCs jam on their
|
||
3rd shot if coolant priority isn't boosted; boosted → almost never jam." VERDICT: the whole chain
|
||
is reconstructed end-to-end, FAITHFUL, and reproduces the reported behavior — no bug, no stand-in.
|
||
**DO NOT "fix" it by driving a weapon's `coolantFlowScale` from the valve; that field being a
|
||
constant 1.0 for weapons is AUTHENTIC** (the ONLY writer of any HeatSink `+0x15C` from a valve value
|
||
is `RecomputeCondenserValves`/FUN_0049f788, which iterates ONLY the condenser chain `mech+0x7cc`).
|
||
Mechanism: a weapon dumps `heatCostToFire` (AFC100=6.5e7 raw) into its OWN linked condenser (AFC100→
|
||
Condenser4); the condenser sheds to the shared bank at a rate scaled by its valve SHARE
|
||
(`coolantFlowScale = valveState_i / Σ valveState`, MoveValve msg id 4, detents 1/5/50/0). The
|
||
weapon's OWN thermal conductance (35500) caps how fast it sheds into its condenser, so coolant
|
||
priority shifts the weapon's equilibrium only ~100 K — but that is EXACTLY enough to straddle the
|
||
`degradationTemperature`(1000) jam line at combat fire rates. Cross `1000` → `heatAlarm`≥1 → the
|
||
jam roll arms (`p=clamp(0.41·T/failT, 0.05, 1)`); on a hit → sticky `JammedState`(5), clears only on
|
||
ResetToInitialState. (Distinct: `failureTemperature`(2000) → self-clearing overheat lockout,
|
||
recoil-pinned + weaponAlarm 7.) **ALL constants VERIFIED AUTHENTIC vs the raw BTL4.RES bytes**
|
||
(`scratchpad/afc_dump.py`): AFC100 startT=77, degradT=1000, failT=2000, thermalConductance=35500,
|
||
thermalMass=94700, heatCostToFire(+0x1A4)=6.5e7, recharge(+0x190)=8, damage=25, minJamChance=0.05 —
|
||
no mis-scaling. **THE ANSWER IS FIRE-RATE-DEPENDENT** (measured, BT_JAM_LOG/BT_VALVE_BOOST/BT_AF_PERIOD,
|
||
madcat AFC100 → Condenser4): at MAX spam (8s recharge) the AFC overheats regardless (~1460 boosted
|
||
~1360 — both jam); at a SLOW cadence (16s) it plateaus ~770 and never jams; at the ~11s COMBAT cadence
|
||
it is DECISIVE — **not boosted → 1058 K (jams), boosted (Condenser4 valve→50, others→1) → 953 K (never
|
||
jams)**. So the QA is right and the reconstruction is faithful; the earlier "boost is weak" read was
|
||
an artifact of testing at max spam (above the window). Diag probes committed: `BT_JAM_LOG`
|
||
(projweap.cpp), `BT_VALVE`/`BT_VALVE_BOOST=<condenserNum>` (heatfamily_reslice.cpp), `BT_AF_PERIOD=<sec>`
|
||
(mech4.cpp). See [[combat-damage]], [[subsystems]] heat.
|
||
|
||
- **Subsystem-panel online/offline gate — fields unidentified [T4] (2026-07-12).** The binary's
|
||
SubsystemCluster draw-state reads `*(subsystem+0x40)==1` and `*(subsystem+0x278)!=4`; neither
|
||
1995 field is identified (weapon simulationState is 0/2/3/4 -- +0x40 isn't it). The port pins
|
||
the panel ONLINE (GetDrawState()==1) because the old raw reads were layout NOISE that flapped
|
||
the state and black-filled the panel over the recharge arc (the frozen-PPC-dial bug; the
|
||
WeaponCluster repaint-heal also landed). Identify both fields to restore the authentic
|
||
destroyed-subsystem dark-panel look.
|
||
|
||
## Locomotion / combat polish (non-gating)
|
||
- **✅ 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
|
||
(@part_012.c:10308-10320) was a no-op (`LoadLowDetailBody`/`LoadHighDetailBody` mislabeled the
|
||
FUN_004a80d4/86c8 clip-loader addresses) and `LoadLocomotionClipsExt` was a stub aliased to the
|
||
exterior loader. Reconstructed the real 4-char INTERIOR (`'i'`-suffix) loader (omits `jointhip`,
|
||
shakes `jointshakey`). Because the port never sets the replicant copy bit the ctor gate lands
|
||
everyone on interior, so `Mech::MaintainViewClipSet()` picks the set by VIEWPOINT each frame
|
||
(your mech → interior/level; every other mech → exterior/lean). Both verified live: cockpit
|
||
level, peer replica leans −8/−11°. See [[locomotion]] "The walk lean IS authored".
|
||
- FOLLOW-UP (non-blocking): if the copy bit ever gets set at construction for other reasons,
|
||
the ctor gate already handles it and MaintainViewClipSet becomes a no-op agreement — no
|
||
conflict, but revisit whether the per-frame maintenance is still needed.
|
||
- ✅ **Authentic per-mech TURN-RATE — DONE (task #64b, 2026-07-14) [T1/T2].** The master-perf
|
||
disasm (0x4aa3d3-0x4aa4ff) turn-rate lerp (walkingTurnRate@0x574 → runningTurnRate@0x578 by
|
||
ground speed, run-speed refinement) is wired into the drive, replacing the `kDriveTurnRate`
|
||
bring-up constant; the rates (which the `Wword(0x15d/0x15e)` no-op was discarding) are real
|
||
members now. Also the trn-in-place DISPATCHER (entry/exit) reconstructed from the same disasm,
|
||
replacing the task-#64 `0.25×`/fast-forward stand-ins. See [[locomotion]].
|
||
- Body-callback gimp handlers (states 16-19, targets 0x70b2/0x7161 undecoded → fall back to stand);
|
||
airborne callbacks (`FUN_004a6344`/`FUN_004a7970`).
|
||
- Wall-block-vs-climb tuning. **✅ Collision DAMAGE application — DONE 2026-07-08 [T2].** The two deferred
|
||
dispatches in `Mech::ProcessCollision` (mech4.cpp: mech-vs-mech `:15324-15358` + icon-crunch `:15369-15401`)
|
||
now fire: on a collision with another Mech or a CulturalIcon, an `Entity::TakeDamageMessage{zone==−1}` is
|
||
dispatched to the victim (via `BTDispatchCollisionDamage`, the engine ctor — same as the weapon path), and
|
||
STEP 6's cylinder table resolves the impact point → a zone on the receiver. Terrain (walls/hills) matches
|
||
neither branch → blocks without damage (faithful). Reachability is guaranteed (`Mover::ProcessCollisionList`
|
||
calls the **virtual** `ProcessCollision` → `Mech::ProcessCollision`); built + stable. The live dispatch
|
||
wasn't captured headlessly (the solo auto-walker never rammed a tree/mech), but the path is proven reachable
|
||
and composed of runtime-verified pieces. **Note:** validated STEP 6's height ref along the way —
|
||
`collisionTemplate->maxY ≈ 7.1` (a real mech height), confirming `CylinderReferenceHeight` reads a height,
|
||
not the heat value the `mech+0x2ec` dual-labeling (heat-gauge sink vs groundRef, mech4.cpp:2697) hinted at.
|
||
- **✅ Cylinder hit-location (STEP 6) — DONE + LIVE 2026-07-08 [T2].** Built + runtime-verified: the `dmgtable.cpp`
|
||
classes now have real storage + a working `ResolveHit→zone`, the mech ctor loads the type-0x1d table by the
|
||
DamageZoneStream name (`[cyl] table 'bhk1' layers=7`), and `Mech::TakeDamageMessageHandler` resolves
|
||
unaimed (zone==−1) hits before base-routing. ⚠ The FIRST "verified" claim was PARTIAL: the table was
|
||
cached via `Wword(0x111)` (the ABSORBER — stores nothing) so every unaimed hit silently no-op'd; fixed
|
||
2026-07-08 by promoting the cache to the named member `Mech::damageLookupTable`
|
||
([[reconstruction-gotchas]] §2). Now verified end-to-end (19 zones resolving, `BT_AUTOFIRE` harness).
|
||
Full change list in [[combat-damage]] "STEP 6 COMPLETE". Historical investigation notes retained
|
||
below for provenance.
|
||
**Re-investigated 2026-07 (see [[combat-damage]] for the full algorithm):** the table (height×angle
|
||
grid, resource type 0x1d) IS built (recon `StandingAnimation` @Mech[0x111], but with an EMPTY name → 0 rows,
|
||
because `ResourceFindByName` is a **no-op template stub**). **CORRECTION to the earlier note:** the LOOKUP is
|
||
**exported** [T1] — `FUN_0049eb54` (table height→row) + `FUN_0049e678` (row angle→cell), NOT unexported.
|
||
Only the thin `Mech::TakeDamageMessageHandler` glue (zone==-1 → call the lookup → `damageZones[zone]->TakeDamage`)
|
||
is unexported, and it's fully implied by pieces already reconstructed → no disassembly needed. Remaining:
|
||
6b real `FindByName` (name = the mech's type-0x14 DamageZoneStream name @ResourceDescription+0xc); 6c the 3
|
||
container ctors + the lookup as a `BTCylinderResolveZone` bridge (ground the cell zone-dict stream format vs
|
||
the real .RES bytes); 6d register the handler + verify. Feasible; the only open risk is the cell stream format.
|
||
The earlier-cited `FUN_004a0230`/`FUN_0049ed0c` don't exist.
|
||
- **Death sequence — CORE DONE 2026-07-08 [T2]; effects+anim deferred.** `Mech::UpdateDeathState()`
|
||
(mech4.cpp) reconstructs the death STATE machine (collapse `movementMode` → RP `VTV::DeathShutdown`
|
||
subsystem loop → settle to disabled/frozen; wreck stays, no crash) — runtime-verified (`BT_DEATH_LOG`).
|
||
Full map + citations in [[combat-damage]] "Death SEQUENCE". ✅ (b) `MechDeathHandler` DONE (mechdmg.cpp) +
|
||
✅ the render-side **RemakeEntity** destroyed-mesh swap DONE 2026-07-08 (btl4vid.cpp
|
||
`RemakeEntityRenderables` + `BTRemakeMechModel` bridge; in-place `SetDrawObj` swap — the tree dtor does
|
||
NOT cascade so never rebuild). ✅ (a) **RESOLVED — no collapse animation exists in BT 4.11** [T1]: the
|
||
fall latch (modes 5-8 → table slots 0x1c-0x1f) is a vestige — no loader fills the slots, no fall clip
|
||
ships, and firing it would bind resource 0 (an AUDIO stream) as keyframes. ✅ The authentic death
|
||
VISUAL is the **WRECK-HULK SWAP** (death ModelList `blhdead` → effect 104 → the victim becomes its
|
||
`<mech>dbr.bgf` burning hulk) — RECONSTRUCTED + verified (`SwapToWreck`, btl4vid.cpp; see
|
||
[[combat-damage]] "Death SEQUENCE" for the full chain + proofs).
|
||
✅ The wreck **sinks** (the 1996 quadratic burial, `offsetY = -0.025·t²`, gone ~17s) with the `ldbr`
|
||
debris field; at burial it goes **INERT** (collisionVolumeCount=0 + target lock dropped — no phantom
|
||
blocking/hits). **Remaining**: (c) the whole-mech **DeathSplash** radius damage; wreck mesh FLAMES
|
||
(flamesml/flamebig + sweep flicker, 1996 script case 4); the **full entity teardown at burial**
|
||
(the authentic death-row removal — needs the mech render tree unhooked from the renderer first;
|
||
the inert wreck is the safe stand-in until then).
|
||
Do NOT issue DestroyEntityMessage on death (before the render-tree teardown lands).
|
||
- ✅ **Critical-subsystem plugs BOUND — damage propagates (task #2, 2026-07-11) [T1/T2].**
|
||
The "binding write is elsewhere" reading was WRONG: the binding is IN the zone ctor loop
|
||
(@0049d0e1-0049d10d) — **Ghidra dropped the two argument pushes** ("type propagation not
|
||
settling"), making `Slot::AddImplementation(roster[streamedIndex])` read as a bare no-arg
|
||
call (mislabeled Resolve). The stream's third per-entry dword IS the roster index into
|
||
`subsystemArray` (mech+0x128). Port: the `DZSlot` stand-in (always-null Resolve) replaced
|
||
with the engine `SlotOf<T>` (binary vtables 0050bb84/0050bb7c ARE its instantiations); bind
|
||
gated MasterInstance+DynamicFlag (`simulationFlags@+0x28`); replicant plugs stay unbound
|
||
(master-authoritative). `SendSubsystemDamage` rewritten to the recovered @0049c9a8 logic:
|
||
the unused crit allotment lands in the subsystem's OWN private 0x160 DamageZone (the old
|
||
`SubProxy2` cast to a mech zone was wrong), ≥1.0 → statusAlarm Destroyed + gated PrintState
|
||
+ zone valve, vital → `graphicAlarm 9` (mech kill); `CriticalHit` now calls the real
|
||
`ApplyDamageAndMeasure` (@4ac07c); the artifact `parentArtifactZone.Add` back-pointer bind
|
||
also revived (LOD damage averaging). `videoObjectFlag` was `vitalSubsystem` (+0xE4,
|
||
res+0x48 "VitalSubsystem"). VERIFIED live (`BT_CRIT_PROBE=<zone>` diag): 66 plugs bound at
|
||
ctor per mech, zone destroyed → crits damaged/DESTROYED with authentic accumulation.
|
||
Residue: roster idx 0/1 (mapper installed post-ctor / voltage-bus stub) stay unbound with a
|
||
logged skip — none of the 8 mechs' streams crit-list them in practice.
|
||
- **✅ Two dead Wword comparison branches — DONE (task #1, `c9f0c2a`).** `Mech::ReadUpdateRecord`
|
||
case 3 now reads real named fields off `Mech__StateUpdateRecord` (legState / stability / legResetLatch /
|
||
speedDemand); the always-false Wword/BTVal branches are gone (the old `mech.cpp:1511`/`:1613` line
|
||
numbers no longer point at that code).
|
||
|
||
## Multiplayer (Phase 7 / P6)
|
||
- **Gitea #12 MP incident (2026-07-19) — root causes found; BOTH FIXES LANDED 2026-07-19
|
||
(awaiting the human MP death-and-survive verification).** Findings [T1/T2 — see the #12
|
||
comments + `scratchpad/incident_2157/`]:
|
||
(1) *Gauge-window freeze* = lobby-built gauge tree dangling over a mission-launch mech
|
||
RE-STREAM after a pre-launch host drop/rejoin; SEH guard disables each faulted gauge
|
||
permanently — see [[gauges-hud]] §MP gauge-window FREEZE. ✅ FIX LANDED:
|
||
`BTL4GaugeRenderer::TearDownForViewpointRestream()` (alarms + Remove(0) + entity grids — the
|
||
entity-bound half of the engine `ShutdownImplementation`), called from
|
||
`MakeViewpointEntity` before `ConfigureForModel` on a viewpoint RE-make; the tree rebuilds
|
||
bound to the new mech. Sentinel: `[gauge] viewpoint re-stream: tearing the gauge tree down`.
|
||
(2) *Missiles cycle-but-never-launch*: ✅ FIX LANDED — the port's RivetGun-approximation
|
||
ProjectileWeaponSimulation replaced with the disasm-exact @004bbd04 machine
|
||
([[decomp-reference]] §5; projweap.cpp): fault gate reads `simulationState@0x40==1`; the
|
||
`simulationFlags|=0x1` DelayWatchersFlag poisoning removed everywhere
|
||
([[reconstruction-gotchas]] §20 — emitter SetDirty split into ForceUpdate/ExecuteOnUpdate per
|
||
site); view/target gating + the `bin->FeedAmmo` pull + recoil set moved into the caller's
|
||
Loaded case with the authentic 4-blip stay-Loaded denial; recoil bleeds ONLY in Loading at
|
||
electrical-Ready and clamps at 0; slot-17 @004b9c9c called from Loading/7 (the launcher
|
||
recharge dial now ANIMATES). State 7 kept binary-faithful (re-asserted unconditionally —
|
||
no exit INSIDE the Execute machine; recovery via ResetToInitialState/mission reset, OR — per
|
||
the 2026-07-23 EjectAmmo @004bb9b8 disasm, [[decomp-reference]] §message-tables — an EJECT
|
||
TAP, which overwrites the alarm from outside the machine: bin still holding rounds →
|
||
weaponAlarm 3 + reload restart, so even a FailureHeat 7 recovers while ammo remains; the
|
||
empty-bin gate 2 re-pins 7 only when the bay is actually dry. Same handler: state-5 jams
|
||
clear the same way — the earlier "no in-mission unjam" claim was wrong). Solo-verified
|
||
on 4.11.388+: 54-60 `[projectile] PUSH` + impact smoke; denial window (BT_LOOK_TEST
|
||
look-back) = trig pulses, state STAYS 2, zero launches, resumes on release; disabled-mech
|
||
gate pins enemy launchers at 7+full recoil; STREAK-6 dial sweeps across screenshots; lasers
|
||
fire throughout. Open fidelity question REMAINS: is launcher FailureHeat/jam-rate at
|
||
sustained autofire authentic-intensity? (both SRM6s jam within ~2 min of held autofire via
|
||
the bring-up CheckForJam heat gate; emitters never trip it — task #11 duty-cycle
|
||
measurement.)
|
||
(3) *J/K/L preset hypothesis REFUTED for solo* [T2]: BT_PRESET_TEST cycling on 387 — missiles
|
||
kept launching after presets (36 PUSH before / 24 after), pip count unchanged, gauges live,
|
||
[mode] masks touch ONLY ModeMFD bits 0-14 (`reticleElementMask` is a different object).
|
||
Corroborating: a 2-player LAN session on the same build (no J/K/L) had working missiles +
|
||
normal pips. UNTESTED: J/K/L during a live RELAY MP session.
|
||
(4) *"WAY MORE pips" (BLH, symptom 4)* — UNRESOLVED: the incident registered 10 pips
|
||
(8×3016 @750/500×5/225×2 + 2×3024 @800) which matches NO stock arsenal (solo dumps: bhk1=7,
|
||
madcat=6, thor=7); the reticle built identically TWICE (lobby + launch re-stream). Needs the
|
||
incident egg / a live 3-seat repro to identify the mech that owned those 10 weapons
|
||
(custom operator-egg loadout? viewpoint mis-assignment at build time?).
|
||
- **✅ Console-death peer-replication freeze — NOT REPRODUCIBLE + the one real bug FIXED
|
||
(2026-07-15) [T2].** The 2026-07-14 report (btconsole.py died mid-match → peer↔peer replication
|
||
froze both ways) did NOT reproduce in the current build under EITHER failure mode, tested directly
|
||
(BT_REPL_LOG on a circling peer, affinity-pinned): (1) a clean relay kill — the replicant kept
|
||
circling smoothly through the kill, NO disconnect logged, node survived; (2) a deliberately STUCK
|
||
console (`scratchpad/btconsole_stuck.py` — connects, starts the mission, then stops recv()'ing while
|
||
holding the socket OPEN = the exact "receive pad full → close never seen" mode hypothesized) — the
|
||
replicant kept moving the entire run. So **peer replication is INDEPENDENT of the console**: the
|
||
pods replicate peer-to-peer over the GAME socket; the console is a separate egg/mission/status
|
||
channel. The original freeze was a transient (plausibly the same single-box packet-jitter /
|
||
CPU-contention artifact root-caused 2026-07-15, `49d73dc`) or was fixed by later MP work.
|
||
**The flagged latent bug IS real and is now FIXED:** `HostDisconnectedMessageHandler`'s
|
||
ConsoleHostType branch closed **`gameListenerSocket`** (the GAME listener) on a CONSOLE disconnect —
|
||
a naming bug; the comment + the commented-out `OpenConnection` intended a CONSOLE re-listen, which
|
||
`CreateConsoleHost()` already does. Removed (L4NET.CPP:969). Harmless to established peer sockets
|
||
(why a live match keeps replicating through console loss), but it would have blocked a NEW peer
|
||
from joining after a console cycle. Verified: 2-node still connects (`All connections completed!`)
|
||
+ the peer circles. Diag harness retained: `scratchpad/btconsole_stuck.py`.
|
||
- ✅ Cross-pod COMBAT — DONE (tasks #46/#47: replicant targeting + damage rerouted to the owning
|
||
master, full cross-pod kill verified). ✅ Replicant GAIT animation — DONE (task #50: replicant
|
||
derives speedDemand from the replicated velocity, full stand→walk→run lifecycle verified).
|
||
See [[multiplayer]] — the entries here were stale (swept 2026-07-11).
|
||
- ✅ **Mech-level update records (@0x4a0c2c) — DONE (task #1, 2026-07-11):** all 9 types
|
||
transcribed + 2-node verified; the peer's death SINK/burial, knockdown, heat and state now
|
||
replicate (the observer's replicant runs its own wreck loop off the replicated
|
||
simulationState). Full mechanics in [[multiplayer]] "Mech-level update records". Residue
|
||
[T3]: stream the model's UpdatePositionDiffrence/TurnVelocity/TurnDegree deadband constants
|
||
to the senders (stand-in thresholds meanwhile).
|
||
- The WinSock2 stack (L4NET.CPP) is 2-node verified end-to-end (cross-pod kills, beams,
|
||
replicant gait — tasks #46-#51). Real-IP pod-LAN config VALIDATED 2026-07-15; **D1 relay mode
|
||
+ UDP channel + LAN discovery + relay-assigned seats + operator console shipped 2026-07-18**
|
||
(see [[multiplayer]] D1). Remaining (config, not code): an actual two-physical-machine
|
||
internet session through the relay (ports verified open; `players/join.bat` ready).
|
||
- **✅ Subsystem-record replication IS wired — the AC just wasn't using it (task #61,
|
||
2026-07-13) [T2, CORRECTED].** (An earlier draft of this note WRONGLY claimed "no
|
||
subsystem-record channel" — that was concluded from an AC-only test before the AC's
|
||
ForceUpdate gap was found. The channel exists and works.) MECHANISM: the mech's per-frame
|
||
tick calls `subsystem->PerformAndWatch(till, update_stream)` for every subsystem
|
||
(mech4.cpp:3893) → base `Simulation::WriteSimulationUpdate` walks the subsystem's `updateModel`
|
||
and serializes any set record; `Entity::UpdateMessageHandler` (ENTITY.cpp:387) routes each
|
||
incoming record to `GetSimulation(subsystemID-1)->ReadUpdateRecord` on the peer. A subsystem
|
||
replicates its fire IFF its `FireWeapon` calls **`ForceUpdate()`** (sets the updateModel bit).
|
||
The EMITTER (beam) and MISSILELAUNCHER (salvo mirror, mislanch.cpp:326) both do → enemy lasers
|
||
+ missiles ARE visible on the peer. The AUTOCANNON set only `simulationFlags |= 0x1` (the
|
||
+0x28 instance flag, NOT updateModel) and had no fire record → its shot never serialized → the
|
||
enemy's cannon (tracer + DAFC muzzle flash) was invisible. FIX: added
|
||
`ProjectileWeapon::WriteUpdateRecord/ReadUpdateRecord` (fire counter + aim, the AC twin of the
|
||
salvo mirror) + `ForceUpdate()` in FireWeapon. Verified live: watching node now logs
|
||
REPLICANT AC shots + muzzle flashes. **LESSON (methodology):** the binary-coverage audit is a
|
||
function-PRESENCE census — it can't see "reconstructed but inert" code (a function that exists
|
||
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.
|
||
|
||
## Rendering follow-ups (non-blocking)
|
||
- ~~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
|
||
`lastInflictingID`, now written by `Mech::TakeDamageMessageHandler`). Remaining nuances, all
|
||
cosmetic: (a) `atten/attenv` distance attenuation not honoured; (b) the per-file texture name
|
||
(`btfx:firesmoke1_scr_tex`) — all effects share the radial-masked grit sheet; decoding FIRE.BSL
|
||
would give the authored sheet; (c) beams draw before particles, so a beam through thick smoke
|
||
reads too bright (pass-ordering); (d) `colorWarp/alphaWarp` applied as `t^(1/warp)` [T4 convention];
|
||
(e) emission uses rate×releasePeriod batching [T4 convention vs the exact 1995 emitter].
|
||
- Replacement-LOD selection for NON-additive multi-LOD models (needs the board's hot-spot/
|
||
reference-point semantics; leads: `s_dplobject.lod_ranges[16]/lod_hot_spot`, 0x2047/0x2048).
|
||
- Day/night MATERIAL path priority (BMF/IMG) — infrastructure done, gated `BT_MATPRI` (over-applies
|
||
to shared terrain mats); needs the terrain-material model understood.
|
||
- RGBA4444 alpha (TREE.BSL cutout) decoded but not alpha-tested outside punch batches; VTX/TGA paths.
|
||
|
||
## Content build sub-project (low priority)
|
||
- Lab/other-build maps (`des`/`burnt`/`frstrm`) are source-only (`.map` in CONTENT/BT/MAPS/); would
|
||
need compiling into a RES via the DOS `btl4tool.exe`. The 8 RES maps cover testing.
|
||
|
||
## Key Relationships
|
||
- Feeds from: every subsystem/render topic (their deferral notes collect here).
|
||
- Gating master: [[source-completeness]] (the missing BT source).
|
||
|
||
## Peer coupled-motion architecture — ✅ DONE (task #50, 2026-07-14/15) [T1/T2]
|
||
RESOLVED. (This block was the old COMPLETION PLAN from `ea39af1`; it was left stale after
|
||
the work actually landed — reconciled 2026-07-15.) The coupled single-source gait pipeline
|
||
is reconstructed, DEFAULT ON, and the residual was root-caused. See [[multiplayer]] two
|
||
sections: **"Authentic coupled peer motion — DONE"** (linear position = BODY gait channel
|
||
integrated into `projectedOrigin@0x260`; angular = replicated-velocity slerp; master
|
||
SEND-mirror runs the SAME `IntegrateMotion` predictor fed the last-sent `bodyTargetSpeed`)
|
||
and **"Peer motion: the 'random shakiness' is single-box packet jitter, NOT the game"**.
|
||
- Landed: coupled path promoted to default (`c52a1ad` + `b013742`); peer poses from the
|
||
BODY channel (`96a896a`); turn-step on the body channel (`f094d78`); true-mirror cadence
|
||
driving the clip from actual replicated motion (`23f1532`). Groundwork: exact quaternion
|
||
integration (`a8eb8a4`), incremental peer heading (`d78e77b`), clock guard (`93456be`),
|
||
commanded-speed cadence feed (`6374efc`), Abs-macro fix (`7615ecd`).
|
||
- **The old "needs the MASTER HALF" conclusion was SUPERSEDED:** the leftover accel/decel
|
||
shakiness was ROOT-CAUSED as single-box **packet jitter** (`49d73dc`) — two Debug nodes on
|
||
one box batch TCP delivery, so records arrive bursty (BT_RXJIT burstiness 3-7x) and the peer
|
||
dead-reckons across the gaps then snaps. Pinning each node to a disjoint core set
|
||
(`ProcessorAffinity 0x00F/0x3C0`, baked into `tools/mp_launch.sh`) restored even ~17ms
|
||
delivery and the shakiness vanished (user-confirmed). Real pods are dedicated machines and
|
||
never see it. **DO NOT add an interpolation/jitter buffer** to mask a rig artifact.
|
||
- Minor non-gating follow-ups (tracked in [[multiplayer]]): the peer accel/decel drift
|
||
(flagged ~2.9u, but **MEASURED ~0.64u max** on the affinity rig with the peer on the body
|
||
channel -- `BT_SNAPLOG` + `BT_DRIVE_SWEEP0`, 2026-07-15; sub-unit, effectively resolved,
|
||
no fix warranted); the exact `_DAT_004ab9cc` offset-decay constant; whether
|
||
`IntegrateMotion`'s 2-stage angular integrate is intended.
|
||
- Speed model decomp-settled (`2c6db6a`): analog throttle, '5 speeds' false, `throttleState@0x4a4`
|
||
is actually the fall-surface material (RENAME PENDING).
|
||
|
||
## Missile launcher trips FailureHeat -> permanent NoAmmo brick (REPRODUCED 2026-07-20)
|
||
|
||
Playtest: "lost the ability to fire missiles mid-fight, no pips, sad noise -- did I run out?"
|
||
Added the `[ammo]` fire trace (env `BT_AMMO_LOG`, projweap.cpp) and reproduced it: the SRM6 is
|
||
NOT out of ammo -- after ~5 volleys it trips **gate 1 FailureHeat** (`heatAlarm==2`) and latches
|
||
into the NoAmmo roach-motel (weaponAlarm 7, never released) with **19 rounds still in the bin**:
|
||
|
||
[ammo] SRM6_2 FIRED, rounds left=19
|
||
[ammo] SRM6_2 -> NoAmmo (gate1): destroyed=0 failHeat=1 heatLvl=2 mechDisabled=0
|
||
|
||
This is the heat-economy open question made concrete (gauges-hud.md "degenerate bring-up heat
|
||
scale"; projweap.cpp:730 "whether FailureHeat is reachable at authentic play intensity"): our
|
||
autofire test fires 60/s (extreme), but the user hit it in MANUAL play too -> either the per-shot
|
||
heat / FailureTemperature scaling is too aggressive, or the roach-motel (no recovery when heat
|
||
falls) is the wrong reading. NEXT: audit the SRM6 heatPerShot vs FailureTemperature vs the mech's
|
||
dissipation, and whether the binary's gate-1 FailureHeat is a LATCH or clears when heat drops.
|
||
Trace it live with `BT_AMMO_LOG=1` (+ `BT_PROJ_LOG=1` for per-tick heat/recoil).
|