Merge remote-tracking branch 'origin/master' into glass-cockpit

# Conflicts:
#	context/gauges-hud.md
This commit is contained in:
Cyd
2026-07-19 20:54:32 -05:00
46 changed files with 2418 additions and 431 deletions
+12
View File
@@ -199,3 +199,15 @@ the shell green), `BT_COP_DUMP` (per-batch punch/bbox), `BT_EYE_FWD=<f>` (eye po
- Uses: [[bgf-format]] (PUNCH, vertex tags) · [[rendering]] (ramp model, skeleton branch)
- Feeds: [[gauges-hud]] (the HUD overlays this view) · [[subsystems]] (gyro, task #56)
- Gotchas: [[reconstruction-gotchas]] §14 (LookAt axis guess), §15 (per-patch edge counting)
## Torso-elevation aim -> EYE PITCH (fixed 2026-07-19) [T2, pixel-calibrated]
User+tester report "pitch does not work": the Torso sim integrated R/F (stick-Y)
into `currentElevation` (authored limits/rates) but NOTHING consumed it — the
view and the aim ray (camera basis, `BTGetAimRay`) never pitched. Fix: the
per-frame HUD tick composes `eyepointRotation = EulerAngles(lookPitch + elev,
lookYaw, 0)` (mech4.cpp; look component stored by `BTCommitLookState` in
`gBTLookPitch/Yaw`; elevation via the new `BTGetTorsoElevation` torso.cpp
bridge). `DPLEyeRenderable` reads it every frame -> the view AND the boresight
aim pitch together (crosshair stays screen-centred, authentic). Sign
pixel-calibrated with `BT_FORCE_ELEV=<-1..1>` screenshots: axis +1 (key R) =
aim UP. The torso GEOMETRY does not tilt — only the eye, like the pod.
+18 -1
View File
@@ -131,7 +131,24 @@ now: closest MECH (`PickRayHit`, damage zones + lock) → closest STRUCTURE (`Wo
occludes a mech BEHIND it) → flat ground (`BTGroundRayHit`) → sky (fire-at-nothing). A structure
hit designates `mech+0x388 = gBTTerrainEntity` (the no-damage-zone sentinel) + `0x37c` = the entry
point, so the range caret reads the structure distance and NO lock ring draws (mech4.cpp:4529, the
`des != hotTarget` branch), exactly the reported authentic behavior. ⚠ There is NO ray query
`des != hotTarget` branch), exactly the reported authentic behavior.
**⚠ SUPERSEDED IN PART (2026-07-19, issue #3): the struck ENTITY, not the sentinel, is now
designated.** The binary damages world structures/icons under the boresight [T1]:
`MechWeapon::SendDamageMessage` @004b9728 (part_013.c:6765-6794) gates only on "target NOT derived
from Mech (Derivation@0x50bdb4) OR aimed zone != -1" — i.e. a NON-MECH target is sent the zone=1
TakeDamageMessage UNCONDITIONALLY — and the Missile contact branch @004be078 dispatches at
whatever entity's solid the round struck (owner via the solid's tag pointer) with NO class test.
The port: `Mech::WorldStructurePick` (mech.cpp) now returns the struck solid's owning entity
(`BoxedSolid::GetOwningSimulation` — every static solid carries its Terrain/CulturalIcon/Door
entity, TERRAIN.cpp:107/246, DOOR.cpp:395), the mech4.cpp pick block designates it (sentinel only
as fallback), and the projectile contact path grew the non-mech damage-zoned else-branch
(@004be078 mirror, direct Dispatch). A plain terrain owner IGNORES the damage
(Entity::TakeDamageMessageHandler returns on zone==1/no zones, ENTITY.cpp:885); a CulturalIcon's
own handler maps 1→0 and dies → its FULL res-32 `trkdead` explosion (dtrkboom+dtrkburn) instead
of only the ram crunch — see [[rendering]] §Cultural-icon DESTRUCTION. HUD unchanged: the icon is
never `hotTarget`, so no hotbox/lock ring (the `des != hotTarget` terrain branch). Verified live:
26 laser truck/prop kills (`[cult] TakeDamage type=3``DYING res=32`), ram crunch (res 31)
unregressed. [T2] ⚠ There is NO ray query
against the ground/`containedByNode` `BoundingBoxTree``FindBoundingBoxUnder` is DOWNWARD-only
(gravity/ground-snap: `*height = FindDistanceBelowBounded`), useless for a horizontal boresight;
the static SOLID tree (`BoxedSolidTree::FindBoundingBoxHitBy`) is the only ray-vs-world query.
+6
View File
@@ -283,6 +283,12 @@ default-ON (`'0'` disables).
| `BT_SELF=<[pilots] tag>` | claim a specific roster seat in relay mode; absent/`auto` = the relay assigns one (SEAT_REQUEST) |
| `BT_RELAY_TCP_ONLY=1` | disable the relay UDP channel (all unreliable traffic rides the TCP relay conn) |
| `BT_INPUT_LOG` | binding-engine trace: CONTROLS.MAP load, button/keypad emissions, pad connect, torso twist/elev axis ([[pod-hardware]] §Desktop input remap) |
| `BT_MODE_LOG` | `[mode] display notify` / `[mode] preset (g,i)` — the ModeManager mask after each secondary-view swap (Gitea #6) or upper-MFD preset-page swap (SetPresetMode, Gitea #9) (btl4mppr.cpp) |
| `BT_VIEWCYCLE_TEST=<frame>` / `BT_MODECYCLE_TEST=<frame>` | scripted verify (mech4.cpp): pulse a secondary-schematic cycle / a control-mode cycle at the frame and every 300 after — pixel-verifiable with `BT_DEV_GAUGES_DOCK`+`BT_SHOT` |
| `BT_PRESET_TEST=<frame>` | scripted verify (mech4.cpp, Gitea #9): from the frame, pulse an upper-MFD preset-page cycle on all three MFDs every 120 frames (Quad → populated Eng pages → Quad) — pixel-verifiable with `BT_DEV_GAUGES_DOCK`+`BT_SHOT` |
| `BT_PRESET_HOLD=<n>` | (with BT_PRESET_TEST, Gitea #11) stop after n pulses so a page is HELD — steady-state pixel verification; the 120-frame cycle phase-locks with BT_SHOT's 90-frame cadence, so cycling shots periodically capture the 1-frame page-switch transition |
| `BT_SEEK_LOG` | `[seek] BecameActive/replot` — SeekVoltageGraph activation sentinel + clear/replot events with the response-at-12kV value (Gitea #11, btl4gau2.cpp) |
| `BT_CTRLMAP_LOG` | streamed `.CTL` record audit (L4CTRL.cpp CreateStreamedMappings): Direct records with the resolved member offset + EVENT records (button→subsystem message; found the pod's secondary-panel Cycle buttons 0x15/0x18) |
| `BT_CAM_LOG` | camera-seat/broadcast trace: streamed camera network count, director pick + Players-group census, ship follow state, PlayerLink dispatch/receive, ranking-window render ([[multiplayer]] camera seat) |
| `BT_SPEC_LOG` | manual spec-audit dump at subsystem ctor: torso speed/limits, heat-sink count, reservoir capacity, per-subsystem coolant loop ([[pod-hardware]] §Spec cross-check) |
+142 -12
View File
@@ -7,8 +7,10 @@ related_topics: [reconstruction-gotchas, decomp-reference, subsystems, pod-hardw
key_terms: [gauge, methodDescription, attribute-pointer, GaugeRenderer, MFD, PCC]
open_questions:
- "MessageBoard LIVE 2026-07-12 (kill ticker); remaining: only strip 0 (kill) is produced -- survey the other btsmsgs.pcx strips for authentic producers"
- "HUD binary attr table @005110c0 offsets conflict with the port HUD layout on 3 slots (0x1D8/0x1EC/0x1F8) -- re-dump before publishing"
- "Secondary-view cycling (Damage/Critical/Heat) unreachable from the desktop keyboard: the mapper's 0x13d/0x13e cases are dead DOS F3/F4 codes (VK_F3/F4 = 0x72/0x73 collide with 'r'/'s') -- wire a free key to CycleControlMode"
- "HUD attr table RESOLVED 2026-07-19 (Gitea #10 audit): re-dumped @5110b8 -- the table starts one record earlier (id 3 FlickerRate@0x1D8), every hud.hpp offset was shifted one slot; hud.hpp/CLASSMAP corrected, no behavioral impact (the reticle feeds via port globals). See GAUGE_COMPOSITE.md AUDIT finding B"
- "SeekVoltageGraph RECONSTRUCTED 2026-07-19 (Gitea #11, was #10 finding A): full widget landed (see §SeekVoltageGraph below) -- ghosts gone steady-state (BT_PRESET_HOLD verification); remaining polish: a 1-frame transition artifact when a BT_SHOT lands on the exact page-switch frame (label BecameActive vs the graph's next rated Execute -- same lag class as the binary; self-heals next frame)"
- "Secondary-view cycling RESOLVED 2026-07-19 (Gitea #6): the selector is the DISPLAY mode (CycleDisplayMode -> vtbl+0x4C override @4d1ae4), NOT CycleControlMode; desktop 'N' / pad RightThumb wired; pixel-verified dama->crit->heat"
- "Upper-MFD PRESET pages RESOLVED 2026-07-19 (Gitea #9): SetPresetMode table @0051dbf0 re-decoded (little-endian -> ModeMFD bits 0-14), per-MFD pod button banks identified from the .CTL dump, desktop J/K/L cycle wired; remaining: the always-active 0x28-0x2F / 0x1A-0x1D per-subsystem msg-4 records (subs 3-13 -- likely the condenser-valve/coolant buttons) unidentified"
- "MP DEATHS resolved 2026-07-12 (observed-death tally + display clamp); remaining: verify multi-death tallies stay in sync across a long session (GAUGE_COMPOSITE.md)"
---
@@ -18,6 +20,11 @@ The pod's cockpit instruments: the secondary MFD (radar/heat/comm), the 5 mono M
engineering screens. Full map + per-widget history in **`docs/GAUGE_COMPOSITE.md`**. **The gauge
system is COMPLETE** at the registration+binding level: all 50 config attribute bindings resolve
(0 NULL) and every config gauge primitive is registered + built (0 parse-skips). [T2]
**AUDIT 2026-07-19 (Gitea #10):** the full per-widget verdict table (45 rows: 34 CORRECT / 1
WRONG-filed / 6 AUTH-STATIC / 6 DEFERRED-FEED) is in `docs/GAUGE_COMPOSITE.md` §"AUDIT
2026-07-19" — the one WRONG was the missing SeekVoltageGraph reconstruction (the emitter/myomer
eng pages' POWER curve + the authentic top-box eraser; caused the stale-ammo "SYSTEM 10 PPC"
ghost). **RESOLVED (Gitea #11): full reconstruction landed — §SeekVoltageGraph below.** [T2]
## Architecture (three layers)
1. **The config** `content/GAUGE/l4gauge.cfg` — a text file the engine `GaugeInterpreter` parses
@@ -70,7 +77,7 @@ over one shared `SVGA16` pixelBuffer; `SVGA16::DrawDevSurface`). On the POD they
intact). The `overlay` port (SectorDisplay lives there) shares the `sec` physical surface via a
different bit-plane (0x00C0). [T2]
## The secondary screen's THREE views (Damage / Critical / Heat) — mode-gated [T0/T1]
## The secondary screen's THREE views (Damage / Critical / Heat) — mode-gated [T0/T1/T2]
The `sec` port stacks three mode-gated mech-schematic layers at offset (50,0) over the
always-on radar/heading/speed/messageBoard (`Secondary1`): **Damage** (`ModeSecondaryDamage`,
`<mech>dama.pcc` + `colorMapArmor`/`colorMapperMultiArmor` — 4 silhouettes front/left/right/back,
@@ -80,13 +87,82 @@ pixel-plane ids 60-63, per-DAMAGE-ZONE `dz_*` tint through the adpal→adpal2 ra
NonMapping=0x10000, Intercom=0x20000, **SecondaryDamage=0x40000, SecondaryCritical=0x80000,
SecondaryHeat=0x100000** (bits 18-20). `ModeInitial` includes **SecondaryDamage** → the ARMOR
view is the default-on layer (our port creates `BTL4ModeManager(ModeInitial)`, btl4app.cpp:303).
`L4MechControlsMapper::SetControlMode` @004d1ae4 is the SELECTOR — its mask table
{0x40000,0x80000,0x100000} clears bits 18-20 and sets one — despite the "control mode" name it
switches the secondary VIEW. ⚠ Desktop gap: the Keypress cases `0x13d`/`0x13e`
(CycleControlMode/CycleDisplayMode) are the DOS Tesla extended F3/F4 codes and never fire under
the WinTesla VK map (VK_F3=0x72 collides with 'r', VK_F4=0x73 with 's' — the same collision
class as the documented 'p'/VK_F1 drop), so the desktop is PINNED on the Damage view. The
schematic shows the pilot's OWN mech only — there is no target-damage readout in the cockpit.
The schematic shows the pilot's OWN mech only — there is no target-damage readout in the cockpit.
**The selector is the DISPLAY mode, not the control mode (Gitea #6, RESOLVED 2026-07-19) [T1→T2].**
The L4 vtable @0051e440 pins the slots: **+0x4C = @004d1ae4** — dispatched by
`CycleDisplayModeMessageHandler` (FUN_004afcac) with the new `displayMode` (0/1/2) — is the
`NotifyOfDisplayModeChange` override that clears bits 18-20 and sets the mask from the table
@0051dbe4 {0x40000,0x80000,0x100000}. (The old "SetControlMode @004d1ae4 switches the secondary
VIEW" claim was the mislabel that kept the port's copy a never-called non-virtual.) **+0x48 =
@004d1acc** — dispatched by `CycleControlModeMessageHandler` (FUN_004afbe0) — just forwards to
the base RET no-op @004b048c: a BAS/MID/ADV control-mode change NEVER touches the secondary
view (empirically confirmed: M cycles the CONTROL MODE gauge lamp, mask bits 18-20 unchanged,
schematic stays on ARMOR DAMAGE). **Authentic pod inputs** (streamed type-6 `.CTL`
EventMappings, dumped live via `BT_CTRLMAP_LOG`): secondary-panel button **0x15 → msg 0x15
CycleDisplayMode** (the manual-p13 "'Mech status Info center", bottom left of the secondary
screen: Armor/Critical/Heat Damage Schematic cycle), button **0x18 → msg 0x14 CycleControlMode**
(the manual-p6 mode button, top right), buttons 0x10/0x11 → ZoomIn/ZoomOut 0x12/0x13 (the map
zoom ± pair). The DOS keyboard fallbacks (Keypress `0x13d`/`0x13e` = extended F3/F4) never fire
under the WinTesla VK map (VK_F3=0x72 collides with 'r', VK_F4=0x73 with 's' — the 'p'/VK_F1
collision class), so the desktop was PINNED on Damage. **Port wiring (mirrors the M/ModeCycle
pattern): 'N' / pad RightThumb → action DisplayCycle → gBTDisplayCycle → `CycleDisplayModeNow()`**
(mechmppr.cpp; the same body the pod button message drives). Pixel-verified live (docked gauges +
BT_SHOT): ARMOR DAMAGE silhouette → CRITICAL DAMAGE subsystem list → HEAT DAMAGE colored list,
mask 0x450421→0x490421→0x510421. Diags: `BT_MODE_LOG`, `BT_VIEWCYCLE_TEST=<frame>`,
`BT_MODECYCLE_TEST=<frame>`.
## The upper-MFD PRESET pages — 3 MFDs × 5 pages (Gitea #9, RESOLVED 2026-07-19) [T0/T1/T2]
The three preset-able MFDs are **Mfd1 (lower left) / Mfd2 (upper center) / Mfd3 (lower right)**,
each a PAIR of bit-planes on one physical monitor: the base **Quad** plane (`Mfd1/2/3`, masks
0x0100/0x0400/0x1000, `btquad.pcx`) and the **engineering-page** plane (`Eng1/2/3`, masks
0x0200/0x0800/0x2000, `bteng.pcx`). Mode bits (BTL4MODE.HPP [T0], bits 0-14):
`ModeMFD{1,2,3}{Quad,Eng1-4}` = `1<<(group*5+item)`**fully disjoint** from Mapping/NonMapping
(15/16), Intercom (17) and the #6 Secondary* trio (18-20); `ModeInitial` puts all three MFDs on
Quad. **What the pages show:** Quad = up to four `vehicleSubSystems` cluster mini-panels (the
quadrants, geometry table @0x51bf34); Eng item i = the FULL-SCREEN engineering detail of the
subsystem streamed onto aux screen `group*4+i` (`sub+0x1dc`; `prepEngr` screens 1-12: "SYSTEM NN"
+ per-class label cells + the cluster's eng child — GENERATOR SELECT A-D, POWER graph, COOLING
loop, DAMAGE (MJ), ammo count …). Unpopulated screens are authored-empty per mech (Blackhawk:
9 of 12 — scr 1/2/4=PPC/Streak6/ERMed, 5-8=Sensors/Myomers/ERMed/ERMed, 9/10=Streak6/PPC;
screens 3, 11, 12 empty).
**`SetPresetMode(group,item)` @004d1b24** swaps the page: table @0051dbf0 = 15 `{clear,set}`
pairs — set = the page's ModeMFD bit, clear = the group's other four (item 0) or all five
(items 1-4). ⚠ The old reconstruction had transcribed the little-endian set column as
BIG-endian dwords (0x01→0x01000000 …), so a preset press set a garbage high bit — and for
group 1 items 3-4 / group 2 items 0-2 STOMPED the live NonMapping/Intercom/Secondary* bits —
while the page bits never moved (the "presets unwired" defect). Fixed against
section_dump.txt:72901-72908; the "does group 2 duplicate #6's secondary views?" concern is
resolved: **no** — group 2 = MFD3, bits 10-14.
**Authentic dispatch (streamed type-19 "L4" .CTL, 121 records dumped via `BT_CTRLMAP_LOG`):**
every MFD has its own 8-button RIO bank whose meanings are MODE-MASK-gated — **Mfd1 =
buttons 0x08-0x0F (AuxLowerLeft), Mfd2 = 0x20-0x27 (AuxUpperCenter), Mfd3 = 0x00-0x07
(AuxLowerRight)**. On a Quad page the bank's buttons DIRECT-SELECT the populated Eng pages
(mapper EventMappings → `MechRIOMapper` messages `Aux1Eng1-4`=0x4-0x7, `Aux2Eng1-4`=0x9-0xC,
`Aux3Eng1-4`=0xE-0x11 → SetPresetMode; no button is streamed for an empty screen); on an Eng
page one button returns to Quad (`Aux1/2/3Quad` = 0x3/0x8/0xD) and the rest remap to the SHOWN
subsystem (per-subsystem msgs: 0x4-0x7 = SelectGeneratorA-D, 0x8 = ToggleGeneratorMode, 0x9 =
ConfigureMappables, 0x3/0xb = unjam/eject-class functions). The `MechRIOMapper` keyboard cases
in @004d1bf0 mirror it as three key rows: `1-4`=MFD1 Eng1-4/`5`=Quad, `a s d f`/`g`,
`z x c v`/`b` — mostly claimed by the port's WASD bindings, hence dead on desktop.
**Port wiring:** the streamed records install and fire on desktop (btinput passes the LIVE
manager mask on button press, so the NUMPAD profile's 0x20-0x27 keys page MFD2 authentically);
the default WASD profile adds **J/K/L → actions Mfd1/2/3Cycle → gBTPresetCycle →
`CyclePresetModeNow(group)`** (btl4mppr.cpp; a port cycle-key shim — 24 mode-dependent pod
buttons don't fit a keyboard — that visits exactly the pod-reachable set: Quad + populated Eng
pages; the body is the authentic SetPresetMode).
**Dev-composite change:** `BTDrawGaugeSurfaces` (L4VB16.cpp) now draws the Eng1-3 planes into
their sibling's cell and SKIPS any mono plane whose port channel is currently `BlankColor`
honoring the mode-driven `reconfigure` (RemapGraphicsPort) so each dev cell shows the ACTIVE
page, like the pod monitor. This SUPERSEDED the 2026-07-12 "frozen-dial" scaffold
(GAUGREND.cpp force-activated all 15 page bits under BT_DEV_GAUGES — removed; it made every eng
screen paint over the shared Eng plane, pinning it on the highest screen). Pixel-verified live
(BT_PRESET_TEST): all three MFDs page Quad → eng details → back to Quad in lockstep with the
[mode] mask log; N/M un-regressed. Diags: `BT_MODE_LOG` ([mode] preset lines),
`BT_PRESET_TEST=<frame>`, `BT_CTRLMAP_LOG`.
## pilotList (Comm KILLS/DEATHS) row semantics + the 1 [T1/T2]
One ROW PER PILOT in the mission (2-player MP = 2 rows — not duplicate displays). KILLS =
@@ -155,8 +231,60 @@ so this is the NOVICE lockout, not a "ROOKIE role" — see [[experience-levels]]
`BTPlayerRoleLocksAdvanced` bridge; bring-up seeding = 2 ≈ veteran = UNLOCKED, verified live:
press → valveState 1→5 → flow redistribution 1/6 → 5/10). Desktop: 'C' cycles the selected
condenser (BT_VALVE_SLOT).
**✅ The COOLANT FLUSH is LIVE (Gitea #7, 2026-07-19) [T2]:** `InjectCoolant` (id 4, the Reservoir
handler table @0x50e680, handler @4aee70) — HOLD flushes reservoir coolant through the loops
(InjectCoolant @4aefa4, set%-biased via each sink's flowScale); the coolant **vertBar C**
(`Reservoir/CoolantMass` = coolantLevel@0x12C, l4gauge.cfg:4526) drops live as the tank drains
(BLH: 6.0 → 0 in ~0.6 s held ≈ the manual's 3-4 punches); the bluish FLUSH.PFX condensation
cloud (psfx 19) spawns on the ReservoirState 0→1 edge. Desktop: **'H' HELD** (action Flush).
Full chain + the two ctor decode corrections it surfaced: [[subsystems]] §coolant FLUSH.
Diags: `BT_FLUSH_LOG`, `BT_FLUSH_TEST=<frame>`.
The **MessageBoard** feed needs **StatusMessagePool** (a NULL stub) + the per-player status
queue. `SeekVoltageGraph`'s 4 Seek* attrs are a cluster-child (not config-called). [T2]
queue. [T2]
## SeekVoltageGraph — the eng-page POWER graph AND the top-box eraser (Gitea #11, 2026-07-19) [T1→T2]
The emitter/PPC + myomer engineering pages' "POWER" box widget (btl4gau2.cpp/.hpp; ctor
@004c6798, BecameActive @004c6920, Execute @004c6934; a CLUSTER-CHILD — built by
EnergyWeaponCluster @004c93b0 / MyomerCluster @004c8df4, not a config keyword). Fully
reconstructed from the capstone disasm (Ghidra dropped every x87 arg; tools/disas2.py):
- **View/geometry:** localView = the page's top data box (0x97,0x80)-(0x17d,0x13b) — exactly
230×187, the plot scales; view coords are view-relative (GRAPH2D.h `origin =
areaWithinPort.bottomLeft`). Ctor sets **SetOperation(Xor)** — ticks/cursor erase by redraw.
- **The eraser role:** BecameActive poisons the cached sample (previousVoltage@0xAC = 9999);
the next Execute's change-test (sample the response at **12000 V**) then runs the CLEAR
@004c6be4 (SetOperation(Replace), color 0, filled 1000×1000 clipped to the box) before
replotting — THIS is what erases the sibling pages' stale pixels on the shared Eng bit-plane
(the #10 ghosts). Every topBox=0 PrepEngr page owns a graph; the box erase design is coherent.
- **Plot math (recovered):** polyline v = 0..12000 step 1200 (_DAT_004c6bdc/_be0):
x = Round(response(v)·230), y = Round(v·(1/12000)·187) — the 80-bit consts @004c6bd0/@004c6d74
are EXACTLY 1/12000. Ticks (@004c6c6c): per gear i in [*min..*max], the current gear draws a
full L (axis→point→axis), others 10-px axis stubs; XOR pair moves the highlight. Cursor
(@004c6c30, emitter pages only): 8×8 XOR square at the LIVE voltage (the ctor's
"OutputVoltage" attr pointer). Destroyed subsystem (simulationState==1 via the
BTSubsystemDestroyed bridge, powersub.cpp): centred edestryd.pcc once; revive calls own
BecameActive (vtbl+0xC, slot 3 of PTR_0051a1fc — verified by vtable dump).
- **The sampler == subsystem vtbl+0x3C (slot 15), reached via the BTSeekVoltageSample dispatch
bridge (emitter.cpp → myomers.cpp; databinding rule):** Emitter @004bb42c =
`sqrt(SeekPower(v)/2.0e8)` with SeekPower @004bb3f4 = `damageFraction·v²·0.5·energyCoefficient`
(a LINE in v — the emitter graph is authentically straight); Myomers @004b8f94 =
`sqrt(AvailableOutput(v)·3.6/350)` (steep near-vertical curve at BLH values).
**FUN_004dd138 = sqrt** (part_015.c:4026) — the old "fabs/fp-magnitude" reading was wrong;
Myomers' old best-effort name "GetSpeedReading" renamed SeekVoltageResponse.
- **The attributes:** Myomers already published all 4 Seek* (@0x320-0x330). Emitter's AUTHENTIC
table was recovered (binary @0x511dd4, ids 0x1D-0x25) and published (emitter.cpp):
LaserOn@0x418, LaserScale@0x42C, LaserRotation@0x41C, Current/Recommended/Min/Max
SeekVoltageIndex@0x3F0-0x3FC, SeekVoltage@0x400, **OutputVoltage@0x414 = currentLevel (RAW
volts — the cursor feed)**. ⚠ The MechWeapon 0x1D "OutputVoltage" PORT ALIAS (→ rechargeLevel
0..1) was RETIRED: the binary MechWeapon table ends at 0x1C, and `AttributeIndexSet::Find`
walks lowest-id-first, so the alias SHADOWED the authentic Emitter row. Emitter renames:
@0x3F8 minSeekVoltageIndex / @0x3FC maxSeekVoltageIndex (were seekStepCounter/seekVoltageCount
guesses; the attr table proves the identities). PPC chains Emitter::GetAttributeIndex().
- **Verified live (BLH, autofire):** page-cycling + BT_PRESET_HOLD steady-state shots — both #10
repro pairs ghost-free (SYSTEM 09 Streak → SYSTEM 10 PPC held clean; SYSTEM 02 → SYSTEM 04
clean; SYSTEM 05 → 06 Myomers clean); curves draw with moving cursors (charge cycling);
exactly ONE replot per activation ([seek] log). Diags: `BT_SEEK_LOG` ([seek]
BecameActive/replot), `BT_PRESET_HOLD=<n>` (freeze the #9 preset cycler after n pulses —
steady-state pixel verification; the 120-frame cycle phase-locks with BT_SHOT's 90).
## Cockpit HUD reticle (main screen, inside view) — LIVE
`BTReticleRenderable` (0x358 bytes, ctor @004cc40c) draws over the finished 3D frame in cockpit
@@ -232,7 +360,9 @@ and every instrument is now live [T2]:**
[T4]). The old "translates to the torso boresight" wording here was the falsified
body-mounted-view model — see [[combat-damage]] Targeting for the full re-correction.
This recovery also CONFIRMS the HUD attr-table ids 4/5/6/8/0xA/0xB/0xC/0xD name↔use pairings
(hud.hpp had flagged them uncertain). Deferred: PNAME1-8.bgf 3D marker chain. (The canopy shell
(hud.hpp had flagged them uncertain; the OFFSETS were re-based 2026-07-19 — the table @5110b8
starts with id 3 FlickerRate@0x1D8, so Rotation=0x1DC…CompassHeading=0x214 Scalar; hud.hpp/
CLASSMAP corrected, names/ids unchanged). Deferred: PNAME1-8.bgf 3D marker chain. (The canopy shell
is now authentic and shows by default — see [[cockpit-view]]; `BT_HIDE_COCKPIT=1` hides it.)
Player CALLSIGN labels (kill/damage feed, radar/target tags, score display) are 1bpp name
+12 -2
View File
@@ -27,8 +27,8 @@ the `MechControlsMapper` interprets them ([[locomotion]]). [T2]
### The button space + lamps [T0, L4CTRL.h enum]
buttonGroup addresses 0x00-0x47: `AuxLowerRight/Left 1-8` (0x00-0x0F), `Secondary1-12` (0x10-0x1B),
`AuxUpperCenter/Left/Right 1-8` (0x20-0x37 — the preset/eng "display eng data" banks; **0x30-0x37 =
the target HOTBOX**, pilot select), icom/door 0x39-0x3C, `Panic` 0x3D (the config-mode lamp),
`AuxUpperCenter/Left/Right 1-8` (0x20-0x37; **0x30-0x37 = the target HOTBOX**, pilot select),
icom/door 0x39-0x3C, `Panic` 0x3D (the config-mode lamp),
`Throttle1` 0x3F (throttle-head = REVERSE THRUST), joystick cluster 0x40-0x47 (Main trigger 0x40,
hat 0x41-0x44, Pinky/ThumbLow/ThumbHigh 0x45-0x47 — the four MAPPABLE fire buttons). "Lamp buttons"
are literal: physical illuminated pushbuttons in panels around the screens; the RIO `LampRequest`
@@ -37,6 +37,16 @@ per-lamp names). A lit button = active. Keypads are NOT buttons: `keyboardGroup[
KeyboardExternal]` carry key VALUES ('0'-'9','A'-'F', L4CTRL.cpp:2526). RIO event convention:
press = `buttonGroup[a].Update(a+1, modeMask)` (mask saved), release = `Update(-a-1, savedMask)`
(L4CTRL.cpp:2470-2520).
**Bank→MFD assignment (Gitea #9, from the streamed "L4" .CTL EventMapping dump [T1]):** each
preset-able MFD owns the 8-button bank AROUND it, mode-mask-gated per page — **Mfd1 (lower
left) = 0x08-0x0F (AuxLowerLeft), Mfd2 (upper center) = 0x20-0x27 (AuxUpperCenter), Mfd3
(lower right) = 0x00-0x07 (AuxLowerRight)**: Quad page → the outer buttons direct-select the
populated Eng pages; Eng page → one button back to Quad, the rest drive the shown subsystem
(generator select A-D, gen mode, weapon config/unjam). Always-active: **0x28-0x2F
(AuxUpperLeft) → per-subsystem msg 0x4 to roster subs 3-9** and secondary-panel 0x1A-0x1D →
subs 10-13 msg 0x4 (identities unverified — likely the condenser-valve / coolant buttons
[T4]); 0x10/0x11 = map ZoomIn/Out, 0x15 = CycleDisplayMode, 0x18 = CycleControlMode (#6).
Full decode: [[gauges-hud]] §preset pages.
### Desktop input remap — CONTROLS.MAP + XInput (2026-07-18) [T2 live]
`game/reconstructed/btinput.cpp` + `content/CONTROLS.MAP` (WASD-classic default, compiled-in twin;
+72
View File
@@ -243,6 +243,78 @@ the user plays the DEBUG build, but the 10fps was NOT the debug build (RelWithDe
split draw-vs-present + check the DEVICE flags before blaming the compiler. Diagnostics kept:
`[spike]/[loadobj]/[rstat]`. [T2]
## Cultural-icon (map prop) DESTRUCTION — revived 2026-07-19 (Gitea issue #3) [T2]
Trucks/props/buildings are **CulturalIcon/Landmark** entities (`engine/MUNGA/cultural.h`, map-
streamed via INTEREST.cpp; ARENA1 = 124 icons, each with a damage zone, `ExplosionModelFile`,
`removeOnDeath`). The SIM was always complete [T0]: ram/weapon damage → `CulturalIcon::
TakeDamageMessageHandler` → zone `damageLevel ≥ 1`**Explosion entity** (Crunch variant for
collision) + `WaitingToBurn` (+`timeDelay`) → `BurningState` + `UpdateCollisionVolumes` (boxes
DELETED on `removeOnDeath`, else sunk by `destroyedYTranslation`). The mech-side crunch dispatch
(`mech4.cpp ProcessCollision``BTDispatchCollisionDamage`, ×0.001 ram economy) reaches icons
(≈19 walking-speed bumps kill the spawn-yard truck). What was BROKEN (the "no state change"
report): the 2007 port fully stubbed **`StateInstanceSwitchRenderable`** ("STUBBED: DPL RB
1/14/07") — the intact→rubble visual swap NEVER fired. Revived D3D9-native: the switch now
controls the draw component via the **`SetDrawObj` in-place drawable swap** (the mech
RemakeEntity mechanism); `[video] Object` hides at BurningState, `Rubble` shows; watcher =
`StateIndicator::AddVideoWatcher` (SetState DOES fire video watchers). ⚠ REQUIRED gotcha:
cultural objects must stay **OUT of static-mesh consolidation** (`RecurseStaticObject` — same
exclusion class as banded LODs/shadows), else the merged mesh draws the intact model forever
regardless of DrawObj. **RUBBLE PLACEMENT FIX (2026-07-19, pixel-verified via BT_SHOT):** the
rubble originally never appeared because additional cultural objects were built as
DPLStaticChildRenderable/DCSInstanceRenderable at TOP level — those read the bare matrix stack
(identity), so the rubble drew at the WORLD ORIGIN. Every cultural video object is now a
`RootRenderable` (places from `myEntity->localToWorld`). Screenshot timeline confirms: intact
MECHMOVR truck → ram death → MECHMD wreck debris scattered at the truck's spot.
**(a) SCORCH QUAD FIXED (2026-07-19, pixel-verified, awaiting human confirm):** the scorch base
(MECHMD patch 0, 4-vtx no-normal quad, `basev:bvx9_mtl` → bexp9_tex = BEXP.BSL **RGBA4444
slice 8**, ramp `cdusty`) drew as a hard dark SQUARE for TWO stacked reasons: (1) the blanket
"truecolor slice (channel ≥ 6) never ramps" gate blocked its cdusty ramp — but only 4 shipped
textures use truecolor slices, and bexp9/bdet9 are GRAYSCALE in RGB (100% / 98.8% r==g==b
texels; only bdam8 is truly coloured) — `bgfload rampableSlice()` now probes the decoded slice
(a truecolor slice ramps iff effectively gray, ≥95%; bdam8's colour stays protected); (2) the
RGBA4444 **authored alpha** (a binary 0/240 splat CUTOUT mask, 78.5% transparent) was never
alpha-tested — new `BgfDrawBatch.texAlpha` (channel ≥ 8) routes through the PUNCH alpha-test
draw states WITHOUT the black-texel keying (which would hole the near-black charred centre).
Side benefit: tree9 (GRASS tree/leaf cards) + bdet9 (trans-rail lattice) get their authored
cutouts too. Post-death shot: irregular char splat, dark brown (71,44,34) lifting to
near-terrain tan (115,100,91 vs terrain 131,119,108), no rectangle; pre-death frames
unregressed (vehicles stay lit/diffuse — the `hasNormals` gate is untouched). [T2]
**(b)+(c) CLOSED (2026-07-19, pixel-verified, awaiting human confirm).** The icon death
EXPLOSION packages are authored per KILL TYPE [T1, BTL4.RES byte-dump]: the CRUNCH
res 31 = `stephit` (ONE video object, effect **1008** = ddam5 damage smoke) — the ram/step
squash is SMALL **BY DESIGN**; the full res 32 = `trkdead` = **1015 dtrkboom.pfx** (fiery
omni burst: vel 150, rad 3.5 exp 10, orange 2.0/0.7/0.2→1.0/0.3/0, ~1.5s) + **1016
dtrkburn.pfx** (the BURNING-WRECK FIRE: 30 rising fire-card particles over a 5s emission,
4±1s each ≈ a ~9s fire at the wreck). The whole icon family shares the pattern:
bigdead=[1017 dbigboom,1016], meddead=[1018,1016], msldead/twrdead=[1017,1016],
smldead=[1020,1022,1021]. **Item (c) verdict: the burning fire is the dtrkburn ONE-SHOT
authored INTO the death package** — there is NO looping BurningState fire: the
damage-zone effect watcher ctor @0042a984 has exactly ONE call site binary-wide (the
Mech ctor, part_012.c:10405), so icon ExplosionTables are inert, and CulturalIcon has no
Performance (AlwaysExecute is replication flush). Documented as authentic. **Item (b):
the res-32 path is reached by WEAPON kills, which the binary DOES dispatch [T1]:**
`MechWeapon::SendDamageMessage` @004b9728 (part_013.c:6765) gates only "target NOT
derived from Mech@0x50bdb4 OR aimed zone set" — a non-Mech target under the boresight is
damaged UNCONDITIONALLY (zone 1; the icon handler maps 1→0) — and the Missile contact
branch @004be078 dispatches a zone=1 TakeDamageMessage at the struck solid's OWNER with
no class test at all. The port never delivered this because the structure pick designated
the `gBTTerrainEntity` SENTINEL; now `Mech::WorldStructurePick` returns the struck
solid's owning entity (`BoxedSolid::GetOwningSimulation` — every static solid is built
with its Terrain/CulturalIcon/Door entity as owner, TERRAIN.cpp:107/246) and mech4.cpp
designates IT (mech.cpp `WorldStructurePick`, mech4.cpp pick block + projectile contact
else-branch). Truck armor is `WeaponDamagePoints=1` (TRK.DMG) — one laser kills it.
Verified live: 26 laser kills `[cult] TakeDamage type=3``DYING … res=32`
`DPLIndependantEffect 1015+1016` at the icon origin → BT_SHOT frames show the orange
dtrkboom fireball, dtrkburn flames on the fresh wreck, and the rubble aftermath; ram
harness re-run still fires res 31 (crunch unregressed). All 24 psfx slots (incl. 15/16)
load from the BTDPL.INI pages. Diag: `BT_FIRE_AT_ICON` (designate nearest ahead icon),
`BT_FX_TEST="1015,1016"`. LESSON: `[cultvis]` log lines verify
the STATE MACHINE, not pixels — only a screen check (BT_SHOT / human) verifies rendering.
Diags: `BT_CULT_LOG` (census/damage/death + `[cultvis]` + `[cultobj]` per-object file/type/op
flags). MP: replicants transition via `ReadUpdateRecord` → same SetState watcher path (untested
cross-pod).
## Key Relationships
- Geometry/LOD: [[bgf-format]]. Base: [[wintesla-port]] (L4D3D). Shadow/visual-conform: [[locomotion]].
- Renders on: [[pod-hardware]] (main 3D view).
- Prop destruction feeds: [[combat-damage]] (crunch dispatch) · uses the psfx layer.
+31
View File
@@ -89,6 +89,37 @@ its electrical state. The full chain, byte-verified [T1]:
- **STILL DEAD:** factory loops 2-4 (heatable/weapon/damageable capability rosters) go through the
`SubProxy` stub whose `IsDerivedFrom` returns 0 — they add NOTHING. See [[open-questions]].
## The coolant FLUSH (Gitea #7, 2026-07-19) — Reservoir InjectCoolant end-to-end [T2 live-verified]
The manual-p24 coolant button (coolant MFD top-right; punch or HOLD): message id **4 "InjectCoolant"**
on the Reservoir (handler table @**0x50e680**, one entry → @4aee70 — same per-receiver id space as the
Condenser's MoveValve @0x50E52C). Handler @4aee70: novice-lockout gate (FUN_004ac9c8), press
(data≥1) → `reservoirAlarm.SetLevel(1)` **gated on coolantLevel@0x12C > 0** (+300 decimal = 0x12C =
coolantLevel — the old "currentTemperature" reading was a misdecode), release → level 0; ForceUpdate.
While level==1 the CoolantSimulation Performance (@4aef78) runs **InjectCoolant @4aefa4** each frame:
work list = condenser chain @+0x7cc + weapon chain @+0x7bc + heatable chain @+0x7ac + linkedSinks
master (port walks the roster with the same membership tests — chains are SubProxy-dead; condensers/
weapons/bank get the binary's duplicate-visit weighting; watcher-branch members are filtered to
HeatSink [T2 guarded]); per sink with flowScale@0x15C≠0: move `squirtMass@0x224 × flowScale × dt`
clamped to [-resLevel, 0] (reservoir drains → the coolant vertBar C `Reservoir/CoolantMass` drops),
sink level clamped [0, capacity], and `pendingHeat@0x1C8 += -(sinkE×|Δ|/sinkLvl resE×|Δ|/resLvl)`
capped at `sinkMass@0x154 × res->startingTemperature@0x13C`, clamped ≤0 (only cools) — the heat
relief rides the existing heat model. **Two ctor corrections surfaced:** (1) the master gate @4af408
(and @4aeb40 HeatWatcher) reads `*(param_2+0x28)` = the **owner MECH's simulationFlags**, not the
resource's subsystemFlags (the resource read left the Reservoir Performance unregistered → no drain);
(2) `(float10)*(int*)(link+0x1d0)` is a **FILD of the bank's integer HeatSinkCount** — capacity =
0.05 (`_DAT_004af518`) × heatSinkCount × streamed CoolantCapacity (BLH: 0.05×6×20 = 6), not a float
reinterpret (which gave ~1e-44 → empty tank). Visual: the binary's mode-1 coolant-effect renderable
(FUN_00456a68, built for classID 0xBC0 on the "ReservoirState" attr, part_014.c:5439; tick
@part_007.c:8780) spawns **psfx 19 = FLUSH.PFX** ("Coolant flush", BTDPL.INI:1018, bluish smoke) on
the state's 0→1 edge — port: `BTSpawnFlushCloud` (mech4.cpp) on the same alarm edge, attached emitter
at torso height. Audio (CoolantDump layers) rides the ReservoirState watchers. Desktop input: **'H'
HELD = Flush** (CONTROLS.MAP action; press+release dispatch). Diags: `BT_FLUSH_LOG` ([flush-tx]
press/release, [flush] level drain, cloud spawn), `BT_FLUSH_TEST=<frame>` scripted ~2 s hold.
Verified live (FOGDAY): level 6→0 over ~0.6 s held (≈3-4 short punches to empty — matches the
manual), bluish cloud screenshot-confirmed (scratchpad/flush_cloud.png). Replicant note: the state
receive @4aeef8 sets the alarm from the state stream — the port's reservoir state replication is not
wired, so remote clouds are deferred (solo/master path complete).
## The four systemic checks (every subsystem)
See [[reconstruction-gotchas]]: (1) shadowing (re-declared engine-base fields), (2) the `Wword` trap,
(3) message-handler chaining, (4) entity validity. Plus resource-struct layout (must mirror the class