diff --git a/content/CONTROLS.MAP b/content/CONTROLS.MAP index 31c847a..3dfcd88 100644 --- a/content/CONTROLS.MAP +++ b/content/CONTROLS.MAP @@ -31,8 +31,8 @@ # LeftTrigger RightTrigger (as digital, >= 50% pressed) # LeftStickX LeftStickY RightStickX RightStickY # LeftTrigger RightTrigger -# action ViewToggle LookBehind AllStop ModeCycle Valve ConfigHold -# Generator1-4 Reconnect +# action ViewToggle LookBehind AllStop ModeCycle DisplayCycle Valve Flush +# ConfigHold Generator1-4 Reconnect Mfd1Cycle Mfd2Cycle Mfd3Cycle # pckey sends an authentic 1995 PC-keyboard hotkey (the in-cockpit # dispatcher): '+'/'-' = target zoom, 'w'..'o' = pilot select, # 'a','b','c','d','f','g','s','v','x','z' = display presets, ... @@ -73,7 +73,12 @@ key D4 button 0x45 # Pinky (4th group) # Systems. key G action ConfigHold # hold + fire keys = regroup the weapon key C action Valve # coolant valve cycle +key H action Flush # coolant flush (HOLD -- dumps reservoir coolant) key M action ModeCycle # Basic -> Standard -> Veteran controls +key N action DisplayCycle # secondary schematic: Damage -> Critical -> Heat +key J action Mfd1Cycle # lower-left MFD page: Quad -> Eng detail pages +key K action Mfd2Cycle # upper-center MFD page (engineering) +key L action Mfd3Cycle # lower-right MFD page key V action ViewToggle # cockpit <-> chase camera (dev) key B action LookBehind # HOLD = the pod's rear-view button key F5 action Generator1 @@ -101,4 +106,5 @@ pad B action AllStop pad Y action ViewToggle pad X action LookBehind pad Start action ModeCycle +pad RightThumb action DisplayCycle pad Back action Valve diff --git a/content/CONTROLS_NUMPAD.MAP b/content/CONTROLS_NUMPAD.MAP index 852336a..15d1482 100644 --- a/content/CONTROLS_NUMPAD.MAP +++ b/content/CONTROLS_NUMPAD.MAP @@ -44,6 +44,7 @@ key Right button 0x43 # Hat Right # ---- Systems ------------------------------------------------------------ key G action ConfigHold key Enter action ModeCycle +key N action DisplayCycle # secondary schematic: Damage -> Critical -> Heat key V action ViewToggle key B action LookBehind key F5 action Generator1 @@ -52,6 +53,9 @@ key F7 action Generator3 key F8 action Generator4 key F9 action Reconnect key F10 action Valve +key J action Mfd1Cycle # lower-left MFD page: Quad -> Eng detail pages +key K action Mfd2Cycle # upper-center MFD page (engineering) +key L action Mfd3Cycle # lower-right MFD page # ---- Upper MFD banks: number row = the top aux row, QWERTY row = the row # ---- under it, left to right across the Left / Center / Right panels. @@ -99,4 +103,5 @@ pad B action AllStop pad Y action ViewToggle pad X action LookBehind pad Start action ModeCycle +pad RightThumb action DisplayCycle pad Back action Valve diff --git a/context/cockpit-view.md b/context/cockpit-view.md index 46a79f7..ce84d47 100644 --- a/context/cockpit-view.md +++ b/context/cockpit-view.md @@ -199,3 +199,15 @@ the shell green), `BT_COP_DUMP` (per-batch punch/bbox), `BT_EYE_FWD=` (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. diff --git a/context/combat-damage.md b/context/combat-damage.md index 803ba8f..e7d5a2c 100644 --- a/context/combat-damage.md +++ b/context/combat-damage.md @@ -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. diff --git a/context/decomp-reference.md b/context/decomp-reference.md index 3da8294..8188bf9 100644 --- a/context/decomp-reference.md +++ b/context/decomp-reference.md @@ -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=` / `BT_MODECYCLE_TEST=` | 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=` | 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=` | (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) | diff --git a/context/gauges-hud.md b/context/gauges-hud.md index eee5d7e..f96a0de 100644 --- a/context/gauges-hud.md +++ b/context/gauges-hud.md @@ -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`, `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=`, +`BT_MODECYCLE_TEST=`. + +## 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=`, `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=`. 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=` (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 diff --git a/context/pod-hardware.md b/context/pod-hardware.md index 98117d6..f417963 100644 --- a/context/pod-hardware.md +++ b/context/pod-hardware.md @@ -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; diff --git a/context/rendering.md b/context/rendering.md index b5852b3..7ebd645 100644 --- a/context/rendering.md +++ b/context/rendering.md @@ -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. diff --git a/context/subsystems.md b/context/subsystems.md index ed0abaa..c5b2e8a 100644 --- a/context/subsystems.md +++ b/context/subsystems.md @@ -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=` 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 diff --git a/docs/GAUGE_COMPOSITE.md b/docs/GAUGE_COMPOSITE.md index c88504c..fc7864d 100644 --- a/docs/GAUGE_COMPOSITE.md +++ b/docs/GAUGE_COMPOSITE.md @@ -567,8 +567,9 @@ dev-parse skips — the tool that pinned the last 3 (⚠ the gauge renderer buil long enough for BuildConfigurationFile; a too-early kill shows [gparse]=0 and looks like "not built"). Remaining gauge work is DATA FEEDS, not widgets — [UPDATE 2026-07-12: the 0xBD3 untangle landed (task #7) and the valve CONTROL landed (task #13, live MoveValve via the rookie-role gate); only the message half remains]: the -status-message queue (StatusMessagePool) — deferred systems, cleanly marked. `SeekVoltageGraph::Execute` (@004c6934) -is a cluster-child (not config-called) — its 4 Seek* attributes are the only remaining publish, non-blocking. +status-message queue (StatusMessagePool) — deferred systems, cleanly marked. [UPDATE 2026-07-19 Gitea #11: +`SeekVoltageGraph` (@004c6934, a cluster-child not config-called) is now FULLY reconstructed and its Seek* +attributes published on Emitter+Myomers — see the AUDIT finding A addendum below.] ### Phase 3 — SIM/MODEL - ✅ **Aggregate HeatSink-bank** (0xBBE) — DONE (commit 16f5f54). Reconstructed `AggregateHeatSink : HeatSink` @@ -627,7 +628,10 @@ Byte-exact re-base of the PoweredSubsystem:HeatSink LEAF (Sensor/Emitter/Myomers (delete the sensor.cpp:287-288 guard — ✅ LANDED: the guard is deleted and the leaf is byte-exact, sensor.cpp:407-413) + the raw-offset cluster reads (partially locked since — MechWeapon byte-exact); radar pip/name symbology + staticEntities (task #17, under reconstruction 2026-07-12); -CycleDisplayMode→ModeManager mask (machinery reconstructed, gauge-layer toggle unverified [T3]); +CycleDisplayMode→ModeManager mask (✅ RESOLVED 2026-07-19, Gitea #6 [T2]: @4d1ae4 relabeled the +NotifyOfDisplayModeChange override (vtbl+0x4C) and made virtual; desktop 'N'/pad RightThumb → +CycleDisplayModeNow; secondary schematic swap dama→crit→heat pixel-verified via +BT_DEV_GAUGES_DOCK+BT_SHOT; pod buttons = streamed EventMappings 0x15→msg 0x15 / 0x18→msg 0x14); numericSpeed units (open). ### Authentically-static (do NOT "fix") @@ -635,3 +639,171 @@ numericSpeed units (open). Degradation/Failure temps (fixed threshold markers); AmbientTemperature 300; MaxRunSpeed (per-mech constant); cmArmor/cmCrit all-green (undamaged solo player — BT is PvP-only); CoolantMassLeakRate 0 on a pristine mech (damage-gated); Player RANK 1 in solo. + +--- + +## AUDIT 2026-07-19 (Gitea #10) — systematic per-widget gauge-data audit + +Every widget on the cockpit displays audited for BINARY-CORRECT data: static pass (CFG configure +blocks + `BTL4MethodDescription[]` + widget sources vs the decomp) + live pass (two instrumented +sessions, BLH/ARENA1, docked composite + BT_SHOT series; drivers: BT_PRESET_TEST, BT_VIEWCYCLE_TEST, +BT_FLUSH_TEST, BT_AUTOFIRE(+AF_MISSILE), BT_GOTO movement; logs audit2/3/4.log). Verdicts: +**CORRECT** (binary-correct data, evidence cited) / **WRONG** (defect; fixed or filed) / +**AUTH-STATIC** (authentically static — do not fix) / **DEFERRED-FEED** (widget correct, data +source a documented deferred system). + +### Coverage inventory (static pass) +All 26 distinct CFG primitives resolve: 17 BT-specific registrations (btl4grnd.cpp:135-152) + +engine base (numeric/numericSpeed/digitalClock/rankAndScore/bgPixelMap/bgBitMap/configure/ +reconfigure/externalConfigure); parse-skip list EMPTY ([gskip]=0), all 50 attr bindings resolve +(0 NULL) — the gauge-complete wave claims re-verified against btl4grnd.cpp + the CFG keyword census. + +### Verdict table + +| # | Surface / widget | Binding → producer | Evidence | Tier | Verdict | +|---|---|---|---|---|---| +| 1 | Heat: condenser temp two-part bars ×6 (TMP) | HeatSink/CurrentTemperature + Degradation/Failure endpoints → heat.cpp table | attr wave incr.1 + tasks #9/#10 heat economy; LIVE: bank S 515→680 climbing under autofire (audit4) | T2 | CORRECT | +| 2 | Heat: SETS valve sliders ×6 (vertNormalSlider) | Condenser/ValveSetting → coolantFlowScale (RecomputeCondenserValves FUN_0049f788, 1/N) | task #13 live valveState 1→5 → flow 1/6→5/10 | T2 | CORRECT | +| 3 | Heat: LEK LeakGauge column | CoolantMassLeakRate → coolantDraw (damage-gated) | writer census; pristine=0 | T2 | AUTH-STATIC (pristine) | +| 4 | Heat: COOLANT A numeric | HeatSink/AmbientTemperature → AggregateHeatSink 300 (ctor @4ae8d0) | LIVE: reads 300 (audit3/4 shots) | T2 | AUTH-STATIC (hardcoded 300) | +| 5 | Heat: COOLANT S numeric | HeatSink/CurrentTemperature (bank) | LIVE: 515→653→680 climbing | T2 | CORRECT | +| 6 | Heat: RES coolant vertBar C | Reservoir/CoolantMass → coolantLevel@0x12C (CFG:4526) | Gitea #7; re-verified LIVE this audit: [flush] 6→0 in ~1.0s held, PFX cloud | T2 | CORRECT | +| 7 | GeneratorCluster A-D panels | Generator/OutputVoltage → outputVoltage@0x1DC + state lamps | gauge wave (@004c72ac); task #12 GenSelect live | T2 | CORRECT | +| 8 | Comm: pilotList KILLS | victim-posted ScoreMessage → killCount (cross-player credit) | commit 34aaa7d; solo 0 observed (authentic) | T2 | CORRECT | +| 9 | Comm: pilotList DEATHS | observed-death tally + −2/−1 display clamp (BTPilotDeaths) | 2026-07-12 resolution; solo 0 observed | T2 | CORRECT | +| 10 | Comm: SELECT-TARGET highlight | BTPilotIsSelected accessors (ex raw-offset AV) | commit 34aaa7d root-cause note | T2 | CORRECT | +| 11 | Plasma: rankAndScore | engine primitive → external serial annunciator | not a D3D surface (L4PLASMA=com2) | T0 | CORRECT (n/a on dev composite) | +| 12 | Quad mini-panels: temp bars / cooling-loop / power-bus lamps / evolt bar / cmode lamp | SubsystemCluster children (@004c8140 map) reading InputVoltage/HeatSink/temps | VEHICLE_SUBSYSTEMS.md phase 2; LIVE: panels render per-subsystem, [vss] roster matches authored aux screens 1-10 | T2 | CORRECT | +| 13 | Quad: recharge dial (SegmentArc270) | PercentDone → rechargeLevel@0x320 | writer census: Emitter::ComputeOutputVoltage @004ba738 only — projectile dial full-once | T1/T2 | CORRECT (emitters) + AUTH-STATIC (projectile) | +| 14 | Quad: ammo count (ammoCountA) | AmmoBin::ammoCount via BTAmmoBinCountPtr bridge | AFC100 fix 2026-07-12; LIVE: 24 fresh (audit4) vs 19 after missile autofire (audit3) — counter moves | T2 | CORRECT | +| 15 | Quad: ConfigMap regroup lamps | LBE4ControlsManager buttonGroup map state | task #6: NO SetColor caller in the binary → color=0, Execute early-outs | T1 | AUTH-STATIC (dormant; BT_CONFIGMAP dev enable) | +| 16 | Eng pages: prepEngr SYSTEM NN + subsystem label + class cells | roster walk, sub+0x1dc auxScreen == screen (BTGetSubsystemAuxScreen) | FUN_004c7e48 matched line-by-line; [vss] dump: scr1/2/4=PPC/Strk6/ERMed, 5-8=Sens/Myo/ERMed/ERMed, 9/10=Strk6/PPC — page headers+labels CORRECT | T1/T2 | CORRECT | +| 17 | Eng ballistic page: ammoCountB / jam / fire / reload numeric / destroyed lamp | bin count bridge + weaponAlarm@0x364 (MechWeapon layout static_assert-locked) | ctor @004c9558 coords matched; LIVE renders | T2 | CORRECT | +| 18 | Eng ballistic page: eject wipe (BitMapInverseWipeScalar @004c61c8) | subsys+0x3f8 eject timer | port tracked NULL (class not reconstructed) | T3 | DEFERRED-FEED | +| 19 | Eng emitter/myomer pages: SeekVoltageGraph (POWER curve) | 4 Seek* attrs + subsystem vtbl+0x3C sampler | WAS WRONG (bring-up no-op Execute) — **FIXED (Gitea #11, 2026-07-19)**: full reconstruction, see finding A addendum | T2 | CORRECT (fixed) | +| 20 | Eng emitter page: seek-step lamp (bteseek) | Emitter::seekVoltageIndex@0x3F0 (static_assert-locked) | @004c93b0 :2146 | T2 | CORRECT | +| 21 | Eng myomer page: seek-step lamp | Myomers@0x320 INFERRED (ctor @004c8df4 not in the decomp export) | marked in code | T3 | CORRECT (best-effort; flagged) | +| 22 | Eng page: linked heat-sink number (hs+0x1d4 guarded raw) | PrepEngr numeric #2 | marked BEST-EFFORT (guarded) | T3 | CORRECT (guarded; flagged) | +| 23 | sec: radar map (wedge/blips/scale) | Mech Radar* attrs; wedge ← Torso::currentTwist@0x1D8 (Gitea #1) | #1 [T2]; LIVE: blips + wedge render, zoom via SetTargetRange | T2 | CORRECT | +| 24 | sec: radar pip/name symbology | GetVideoObject/LookUpPip/GetNameID | task #17 under reconstruction — cross-blip stand-in | T3 | DEFERRED-FEED | +| 25 | sec: headingPointer needle + numeric | YawPitchRoll decomposition (engine-convention port of euler[0]) | LIVE: 089→120 while turning under BT_GOTO | T2 | CORRECT | +| 26 | sec: numericSpeed | LinearSpeed (abs(adv)/dt) | LIVE: 0 parked → 176-182 walking | T2 | CORRECT (units question stays open) | +| 27 | sec: digitalClock MISSION TIME | engine mission clock (format enum arg) | LIVE: 09:19→06:04 counting | T0/T2 | CORRECT | +| 28 | sec: sectorDisplay (overlay) | localOrigin → Round(−Z·0.01)+500 / Round(X·0.01)+500 | commit 4a4ec68 [BT_SECTOR_LOG live]; NOTE: overlay plane not composited into the dev sec cell (polish item) — data verified by log | T2 | CORRECT | +| 29 | sec: schematic ARMOR view (cmArmor/multiArmor, dama.pcc) | zone damageLevel ×100 → adpal ramp | incr.3/4; all-green pristine LIVE | T2 | CORRECT (AUTH-STATIC all-green solo) | +| 30 | sec: schematic CRITICAL view (cmCrit) | subsystem simulationState/damage | LIVE this audit: N-cycle shows the full subsystem list (GEN A-D, LOOP 1-6, HUD, SENSORS, GYRO, TORSO, weapons) | T2 | CORRECT | +| 31 | sec: schematic HEAT view (cmHeat) | subsystem currentTemperature tint | #6 pixel-verified; re-cycled this audit (mask 0x450421→0x490421→0x510421) | T2 | CORRECT | +| 32 | sec: view cycling (N / pod 0x15) | CycleDisplayMode → vtbl+0x4C @4d1ae4 | #6 resolution re-verified live ([mode] display notify 0/1/2) | T2 | CORRECT | +| 33 | sec: CONTROL MODE lamp (BAS/MID/ADV) | ControlsMapper/DisplayMode oneOfSeveralPixInt | attr wave incr.5; M-cycle verified #6 | T2 | CORRECT | +| 34 | sec: duck / searchlight button lamps | duckState (ctor-zeroed only) / Searchlight LightOn | duckState writer missing (P3 leftover) | T3 | DEFERRED-FEED (duck); CORRECT (light attr published) | +| 35 | sec: messageBoard ticker | StatusMessagePool (NULL stub) + kill ticker strip 0 | 7fc4acb; kill ticker live 2026-07-12, other strips unsurveyed | T2/T3 | DEFERRED-FEED (partial) | +| 36 | MFD preset paging (J/K/L, pod RIO banks) | SetPresetMode table @0051dbf0 (little-endian re-decode) | #9; re-verified live this audit: cycles visit EXACTLY the populated set (MFD1: 1,2,4; MFD2: 1-4; MFD3: 1,2) | T2 | CORRECT | +| 37 | HUD: range ladder + caret (500 m/s slide) | BTSetHudTargetRange ← targeting; HudSimulation :5652-5670 | task #37 [T1] + live | T1/T2 | CORRECT | +| 38 | HUD: weapon pips (loaded/dark/hidden; group filter) | attr 0x1C WeaponState == stateConst2; damage state 1 hides | 2026-07-18 fix (projectile pips un-pinned); pips visible in audit shots | T2 | CORRECT | +| 39 | HUD: bottom 21-tick TORSO-TWIST tape | RotationOfTorsoHorizontal / HorizontalTorsoLimit (attrs 4/5/6) | task #58; BLH fixed-torso → centred (authentic) in audit shots | T2 | CORRECT (AUTH-STATIC on BLH) | +| 40 | HUD: compass + threat trail | CompassHeading (attr 0xD, yaw+twist :5676); ThreatVector (attr 0xC) ← TakeDamage push | task #37/#38; compass rose renders in shots | T2 | CORRECT | +| 41 | HUD: lock ring (4°/frame spin) | Lock rule :5619-5634 (own zone <0.75, target zone <1.0) | task #38 | T2 | CORRECT | +| 42 | HUD: target hotbox / edge arrows | HotBoxVector projection (live per-axis) | task #37 | T2 | CORRECT | +| 43 | HUD: simple-X mode | PrimaryHudOn mask 0x20 state switch | ctor @4689-4705 | T1/T2 | CORRECT | +| 44 | HUD: PNAME1-8.bgf 3D name marker chain | — | deferred | T4 | DEFERRED-FEED | +| 45 | HUD attr table @005110b8 (ids 3-0xD) | hud.hpp/CLASSMAP corrected this audit | finding B below | T1 | CORRECT (fixed) | + +**Counts: 34 CORRECT · 1 WRONG (filed: SeekVoltageGraph — since FIXED, Gitea #11) · 6 AUTH-STATIC +(incl. dual-verdict rows 13/29/39) · 6 DEFERRED-FEED (rows 18/24/34-duck/35/44 + the sec +messageBoard partial).** +(Dual-verdict rows counted in both columns.) + +### Finding A — the "SYSTEM 10 PPC ammo readout" (issue entry point a) — ROOT-CAUSED, FILED +**The page header, label and screen→subsystem mapping are CORRECT** ([vss] live dump matches the +authored sub+0x1dc assignments; CFG prepEngr(ModeMFD3Eng2, 10, ...) pairing consistent; +FUN_004c7e48 reconstruction line-exact). The ammo readout on the SYSTEM 10 (PPC) page is a +**GHOST — stale pixels from the previously-shown SYSTEM 9 (Streak 6) page** on the SAME shared +Eng3 bit-plane. Reproduced live (audit3): SYSTEM 04 ERMED LASER (emitter) showed the "0019" +ammo box left by the SYSTEM 02 Streak page; SYSTEM 08's POWER box showed the SENSOR RANGE label +left by SYSTEM 05. Mechanism chain, all decomp-verified: +- Ballistic/sensor eng children (ammoCountB @(0xdc,0x104)-(0x140,0x12C), jam/fire lamps, reload + numeric — ctor @004c9558) draw INSIDE the top data box (0x97,0x80)-(0x17d,0x13b). +- On a page switch the engine moves gauges active↔inactive (DeactivateGaugeBases, + GAUGREND.cpp:3909) — BecameInactive() is a no-op everywhere [T0]: NOTHING erases on deactivation. +- PrepEngrScreen::BecameActive (@004c7e48) erases the top box ONLY for the ballistic/sensor/gauss + cases (topBox=1); the emitter/PPC and myomer cases pass 0 — **because in the binary those pages' + box is owned by the SeekVoltageGraph**: its localView IS the box rect (ctor @004c6798 + SetPositionWithinPort(0x97,0x80,0x17d,0x13b)), BecameActive @004c6920 poisons the cached + sample (this+0xAC = 10000.0f), and the next Execute (@004c6934) calls the clear helper + @004c6be4 (SetColor 0 + filled rect 1000×1000 clipped to the view = FULL BOX ERASE) before + re-plotting the voltage curve. Every topBox=0 page has a graph child (EnergyWeaponCluster + @004c93b0 :2137; MyomerCluster @004c8df4) — the design is coherent and ghost-free on the pod. +- **The port's SeekVoltageGraph::Execute is a marked bring-up NO-OP** (btl4gau2.cpp:331), so the + erase never runs and the POWER curve never draws → the ghost. +**Fix required (LARGE — filed, not half-fixed per the no-stand-ins rule):** full SeekVoltageGraph +reconstruction — publish the 4 CurrentSeekVoltageIndex/MinSeekVoltageIndex/MaxSeekVoltageIndex/ +SeekVoltage attributes on Emitter(+PPC)+Myomers, identify + reconstruct the subsystem vtbl+0x3C +voltage-response sampler, x87-recover the plot math (@004c6934 loop constants _DAT_004c6bdc/_be0, +DrawTicks @004c6c6c, DrawCursor @004c6c30, clear @004c6be4 — Ghidra dropped the FPU args; +tools/disas2.py). An erase-only patch was deliberately NOT made (it would be a stand-in for the +missing curve). + +**✅ RESOLVED (Gitea #11, 2026-07-19) — the full widget landed (btl4gau2.cpp/.hpp):** +- **x87 recovery (capstone, tools/disas2.py):** plot step/limit _DAT_004c6bdc/_be0 = 1200/12000; + the 80-bit consts @004c6bd0/@004c6d74 = EXACTLY 1/12000 (the voltage→y normaliser); + x = Round(response(v)·xScale 230), y = Round(v·(1/12000)·yScale 187); change-test = the + response sampled at 12000 V vs the @0xAC cache (9999 activation sentinel); clear @004c6be4 = + SetOperation(Replace)+color 0+filled 1000×1000 clipped to the view; ticks @004c6c6c = per-gear + loop [*min..*max], current gear = full L, others 10-px stubs, XOR pair to move; cursor + @004c6c30 = 8×8 XOR square at (x-4,y-4) from the LIVE voltage (*this[0x29]); destroyed branch = + centred edestryd.pcc via (Round(xScale)-w)/2; revive → own vtbl+0xC == BecameActive (slot 3 of + PTR_0051a1fc, vtable-dump verified). Members this[0x30..0x32] = cursorX/cursorY/tickIndexShown. +- **The vtbl+0x3C sampler identified from the vtable dumps** (Emitter @00512078 slot 15 = + @004bb42c; Myomers @005117dc slot 15 = @004b8f94): Emitter = sqrt(P(v)/2.0e8f [0x4d3ebc20]), + P @004bb3f4 = damageFraction@0x444 · v² · 0.5 [_DAT_004bb428] · energyCoefficient@0x454; + Myomers = sqrt(AvailableOutput(v)·3.6/350). **FUN_004dd138 == sqrt** (part_015.c:4026) — the + myomers.cpp "fabs / fp-magnitude" reading was corrected and the best-effort name + GetSpeedReading renamed **SeekVoltageResponse**. Port dispatch: `BTSeekVoltageSample` + (emitter.cpp) → `BTMyomersSeekSample` (myomers.cpp) complete-type bridges; + `BTSubsystemDestroyed` (powersub.cpp) for the simulationState==1 test. +- **Emitter's AUTHENTIC attribute table recovered + published** (binary @0x511dd4, ids + 0x1D-0x25 chained after MechWeapon's 0x1C): LaserOn@0x418(firingActive), LaserScale@0x42C, + LaserRotation@0x41C, CurrentSeekVoltageIndex@0x3F0, RecommendedSeekVoltageIndex@0x3F4, + MinSeekVoltageIndex@0x3F8, MaxSeekVoltageIndex@0x3FC, SeekVoltage@0x400, + OutputVoltage@0x414(currentLevel — RAW volts, the cursor feed). Emitter field renames per the + table: @0x3F8 minSeekVoltageIndex, @0x3FC maxSeekVoltageIndex (static_assert-locked). The + MechWeapon 0x1D "OutputVoltage" PORT ALIAS was RETIRED (binary table ends 0x1C; Find walks + lowest-id-first so the alias shadowed the authentic row — the cursor would have read the 0..1 + rechargeLevel instead of volts). PPC::DefaultData → Emitter::GetAttributeIndex(). +- **Verified live** (BLH DEV.EGG, BT_AUTOFIRE + BT_PRESET_TEST + the new **BT_PRESET_HOLD=** + steady-state hold): both #10 repro pairs ghost-free held for minutes (SYS09 Streak→SYS10 PPC; + SYS02 Streak→SYS04 ERMed; SYS05 Sensors→SYS06 Myomers); curves draw (emitter = straight line, + authentically — sqrt of v² is linear; myomer = steep low curve), cursors track the charge + cycle, gear ticks render; [seek] log (BT_SEEK_LOG) shows exactly ONE replot per activation, + all 6 graph instances. NOTE: a BT_SHOT landing exactly on a page-switch frame can capture the + 1-frame label-vs-box transition (PrepEngr labels draw in BecameActive; the box clears at the + graph's next rated Execute) — the binary has the same lag class; it self-heals next frame and + the 90/120 cadence phase-lock made it look persistent during cycling verification. + +### Finding B — HUD binary attr table @005110c0 conflict (issue entry point b) — RESOLVED, FIXED +Re-dumped section_dump.txt @5110b8: the table starts ONE RECORD EARLIER than transcribed — a full +16-byte record {id 3, "FlickerRate", 0x1D8|1, 0} (the old reading took the FlickerRate name ptr +as a "category label"), so every subsequent offset in hud.hpp was shifted one slot. Binary truth: +Rotation@0x1DC ← Torso::currentTwist(+0x1D8), LimitR@0x1E0, LimitL@0x1E4, Speed@0x1E8, +RangeToTarget@0x1EC, (0x1F0 unbound), Visible@0x1F4(init 1), Lock@0x1F8(init 0), HotBoxVector +P3D@0x1FC, ThreatVector P3D@0x208, CompassHeading Scalar@0x214 (= yaw euler[0] + twist :5676). +Cross-checked against the ctor @004b7f94 and HudSimulation @004b7830 — ALL consistent (the Lock +rule :5622/:5633 writes @0x1F8; the 1200-default + 500 m/s slide writes @0x1EC). **Behavioral +impact: NONE** — the port reticle feeds through its own globals (gBTHudLockState etc.) which +implement the correct semantics, l4gauge.cfg never binds HUD attrs, and the mislabeled +SetCompassHeading (@004b7810, actually SetThreatVector — writes @0x208) has no port caller. +FIXED: hud.hpp member renames + table comment, hud.cpp ctor inits (Visible=1/Lock=0 per binary), +SetThreatVector rename, CLASSMAP.md entry. The ids 4-0xD NAME↔USE pairings from task #37 stand. + +### Finding C — [T3]/[T4] tag review (issue entry point c) +context/gauges-hud.md contains NO [T3] tags (all previously-T3 claims were promoted in the +#1/#6/#7/#9 waves). The single [T4] — the un-exported reticlePosition writer / fixed-torso +free-aim channel (mech+0x36c) — remains a genuine unknown and stays flagged. In-code BEST-EFFORT +markers audited as table rows 18/21/22/24/34. + +### Finding D — BT_SHOT dock-capture regression (found + fixed during this audit) +The 2026-07-18 capture reorder left BT_SHOT BEFORE BTDrawGaugeInset, so BT_DEV_GAUGES_DOCK +screenshots silently omitted the gauge panel (audit runs 1-2 captured cockpit-only frames). +Moved the capture after the inset (L4VIDEO.cpp) — dock shots again show what the screen shows. +Also noted: BT_DEV_GAUGES_DOCK alone does NOT enable the composite — DevGaugeComposite() gates +on BT_DEV_GAUGES (L4VB16.cpp:105); set BOTH. diff --git a/engine/MUNGA/CULTURAL.cpp b/engine/MUNGA/CULTURAL.cpp index aa6675f..4a53421 100644 --- a/engine/MUNGA/CULTURAL.cpp +++ b/engine/MUNGA/CULTURAL.cpp @@ -73,6 +73,16 @@ void { Check(damage_message); + // BT_CULT_LOG (issue #3): does damage ever reach a cultural icon? + if (getenv("BT_CULT_LOG")) + { + DEBUG_STREAM << "[cult] TakeDamage entity=" << (long)GetEntityID() + << " type=" << (int)damage_message->damageData.damageType + << " amount=" << (float)damage_message->damageData.damageAmount + << " zone=" << damage_message->damageZone + << " state=" << GetSimulationState() + << "\n" << std::flush; + } if (damage_message->damageZone == -1 ) { @@ -144,6 +154,15 @@ void Explosion::Make(&exp_message); Register_Object(new_exp); + // BT_CULT_LOG (issue #3): the death transition fired + if (getenv("BT_CULT_LOG")) + { + DEBUG_STREAM << "[cult] DYING entity=" << (long)GetEntityID() + << " explosion res=" << explosion_ID + << " delay=" << (float)timeDelay + << " -> WaitingToBurn\n" << std::flush; + } + Message dummy_message(SetBurningStateMessageID, sizeof(Message)); Check(application); Time now = Now(); @@ -239,6 +258,15 @@ CulturalIcon::AttributeIndexSet& CulturalIcon::GetAttributeIndex() // Construction and Destruction // +// +// Live-icon census (issue #3 diag): lets the BT_FIRE_AT_ICON harness designate +// a real map icon deterministically (headless weapon-vs-icon verification). +// Registration bookkeeping only -- no behavior change; entries are removed in +// the dtor (interest streaming can delete icons on zone transitions). +// +Entity *gBTCultIcons[256]; +int gBTCultIconCount = 0; + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // CulturalIcon::CulturalIcon( @@ -247,6 +275,10 @@ CulturalIcon::CulturalIcon( ) : UnscalableTerrain(creation_message, shared_data) { + if (gBTCultIconCount < 256) + { + gBTCultIcons[gBTCultIconCount++] = this; + } Str_Copy(teamName, "Defualt", sizeof(teamName)); // @@ -335,6 +367,21 @@ CulturalIcon::CulturalIcon( } SetValidFlag(); dmg_res->Unlock(); + // BT_CULT_LOG: per-icon creation census (issue #3 -- truck destruction) + if (getenv("BT_CULT_LOG")) + { + DEBUG_STREAM << "[cult] made icon entity=" << (long)GetEntityID() + << " res=" << resourceID + << " zones=" << damageZoneCount + << " explode=" << explosionResourceID + << " crunch=" << crunchExplosionResourceID + << " removeOnDeath=" << (int)RemoveCollisionVolumeOnDeath() + << " stopping=" << (int)IsStoppingCollisionVolume() + << " pos=(" << localOrigin.linearPosition.x + << "," << localOrigin.linearPosition.y + << "," << localOrigin.linearPosition.z << ")" + << "\n" << std::flush; + } Check_Fpu(); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -394,7 +441,15 @@ CulturalIcon* // CulturalIcon::~CulturalIcon() { - + // live-icon census removal (swap-with-last; see the registration above) + for (int ii = 0; ii < gBTCultIconCount; ++ii) + { + if (gBTCultIcons[ii] == this) + { + gBTCultIcons[ii] = gBTCultIcons[--gBTCultIconCount]; + break; + } + } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // diff --git a/engine/MUNGA/GAUGREND.cpp b/engine/MUNGA/GAUGREND.cpp index 61978fe..1c2f6af 100644 --- a/engine/MUNGA/GAUGREND.cpp +++ b/engine/MUNGA/GAUGREND.cpp @@ -3677,22 +3677,16 @@ void Check(application->GetModeManager()); ModeMask current_mode_mask = application->GetModeManager()->GetModeMask(); - // DEV-COMPOSITE (2026-07-12, the frozen-dial fix): page-gated gauges (mode - // = their MFD page bit) freeze when their page isn't mode-active -- on the - // pod you FLIP pages, but the dev window shows every page surface at once, - // so six of seven weapon dials sat frozen on screen (user-hit; the arcs - // executed exactly 8 times at startup then never again). Under - // BT_DEV_GAUGES force the 15 MFD page-plane bits (BTL4 mode bits 0..14 = - // MFD1/2/3 Quad+Eng1-4) active so every visible page runs. The secondary - // trio (bits 18-20) stays authentic -- those views SHARE pixels and are - // exclusive by design. - { - static int s_devAllPages = -1; - if (s_devAllPages < 0) - s_devAllPages = (getenv("BT_DEV_GAUGES") != NULL) ? 1 : 0; - if (s_devAllPages) - current_mode_mask |= (ModeMask)0x7FFF; - } + // DEV-COMPOSITE history: a 2026-07-12 scaffold ("the frozen-dial fix") + // forced the 15 MFD page-plane bits (BTL4 mode bits 0..14 = MFD1/2/3 + // Quad+Eng1-4) active under BT_DEV_GAUGES, because the dev panel then drew + // every page plane at once and page-gated dials froze. REMOVED for Gitea + // #9 (2026-07-19): the dev composite now honors the mode-driven + // `reconfigure` (a BlankColor plane is skipped -- L4VB16.cpp + // BTDrawGaugeSurfaces), so exactly the ACTIVE page of each MFD is shown, + // like the pod -- and the working SetPresetMode page switching NEEDS the + // authentic gating (the force made every eng screen paint over the shared + // Eng plane, pinning it on the highest screen and ignoring page flips). ModeMask previous_mode_mask = application->GetModeManager()->GetPreviousModeMask(); ModeMask diff --git a/engine/MUNGA_L4/L4CTRL.cpp b/engine/MUNGA_L4/L4CTRL.cpp index eb5334a..f0df062 100644 --- a/engine/MUNGA_L4/L4CTRL.cpp +++ b/engine/MUNGA_L4/L4CTRL.cpp @@ -2286,6 +2286,27 @@ void Receiver::MessageID msg_id = mapping->messageID; + // + // BT_CTRLMAP_LOG=1: dump the streamed EVENT records too (button -> + // subsystem message). Gitea #6 needed this to identify which pod + // console buttons carried the mapper's CycleControlMode (0x14) / + // CycleDisplayMode (0x15) messages. + // + { + static const int s_ctrlmapLog = + (getenv("BT_CTRLMAP_LOG") != 0 && *getenv("BT_CTRLMAP_LOG") != '0'); + if (s_ctrlmapLog) + { + DEBUG_STREAM << "[ctrlmap] rec " << i + << " subsys " << mapping->subsystemID + << " group " << (int)mapping->controlsGroup + << " elem 0x" << std::hex << element + << " mask 0x" << (unsigned)mode_mask + << " EVENT msg 0x" << (unsigned)msg_id + << std::dec << "\n" << std::flush; + } + } + switch (mapping->controlsGroup) { case ScalarGroup: diff --git a/engine/MUNGA_L4/L4D3D.cpp b/engine/MUNGA_L4/L4D3D.cpp index 60b129f..f1e8107 100644 --- a/engine/MUNGA_L4/L4D3D.cpp +++ b/engine/MUNGA_L4/L4D3D.cpp @@ -437,7 +437,12 @@ d3d_OBJECT* d3d_OBJECT::LoadObjectBGF(LPDIRECT3DDEVICE9 device, char *fileName) // become alpha-0 holes below; DrawMesh alpha-TESTS them in the opaque pass. static int s_punch = -1; if (s_punch < 0) { const char *pv = getenv("BT_PUNCH"); s_punch = (pv == 0 || pv[0] != '0') ? 1 : 0; } - object->mDrawOps[i].punchThrough = (s_punch && data.batches[i].punch); + // PUNCH batches alpha-test load-KEYED holes (black texels). TEXTURE-ALPHA + // batches (RGBA4444 BSL slice, issue #3: the wreck scorch splat / rail + // lattice / tree cards) alpha-test their AUTHORED alpha channel -- same + // draw states, no keying (the near-black splat centre must stay opaque). + object->mDrawOps[i].punchThrough = + (s_punch && data.batches[i].punch) || data.batches[i].texAlpha; object->mDrawOps[i].copRole = data.batches[i].copRole; // punch stencil-cut kit (task #55) object->mDrawOps[i].texture.texture = NULL; object->mDrawOps[i].texture.wrap_u = L4TEXOP::REPEAT; @@ -493,7 +498,10 @@ d3d_OBJECT* d3d_OBJECT::LoadObjectBGF(LPDIRECT3DDEVICE9 device, char *fileName) { // PUNCH keying: black texels of a punch batch become alpha 0 // (holes). Keyed on the ORIGINAL texel, before any ramp. - const bool punchKey = object->mDrawOps[i].punchThrough; + // NOT applied to texture-alpha (RGBA4444) batches -- their + // cutout is the authored alpha channel, and black-keying + // would hole the scorch splat's charred (near-black) centre. + const bool punchKey = (s_punch && data.batches[i].punch); if (useRamp) { // Colorize: for each texel, index the low->high ramp by diff --git a/engine/MUNGA_L4/L4VB16.cpp b/engine/MUNGA_L4/L4VB16.cpp index ecec317..aebb191 100644 --- a/engine/MUNGA_L4/L4VB16.cpp +++ b/engine/MUNGA_L4/L4VB16.cpp @@ -167,7 +167,7 @@ void SVGA16::DrawDevSurface(LPDIRECT3DDEVICE9 device, int slot, int mask, int pa { int w = pixelBuffer.Data.Size.x; // 640 int h = pixelBuffer.Data.Size.y; // 480 - if (device == NULL || slot < 0 || slot >= 8 || w <= 0 || h <= 0) return; + if (device == NULL || slot < 0 || slot >= 10 || w <= 0 || h <= 0) return; // Lazily create this surface's texture on the MAIN device (MANAGED -> lockable + // survives device reset). @@ -289,7 +289,13 @@ struct BTGaugeSurfaceDesc // buffer, so past ~1.5x you magnify, not sharpen. static const float kBTPanelW = 1320.0f; static const float kBTPanelH = 480.0f; -static const BTGaugeSurfaceDesc kBTGaugeSurfaces[6] = +// Gitea #9: each preset-able MFD is TWO bit-planes sharing one physical +// monitor -- the base Quad plane (Mfd1/2/3) and the engineering-page plane +// (Eng1/2/3). The mode-driven `reconfigure` swaps which of the pair is +// channel-enabled and which is BlankColor; the draw loop below skips the +// blanked plane, so the dev cell shows exactly what the pod monitor shows. +// The Eng entries share their sibling's cell rect and FOLLOW it in the list. +static const BTGaugeSurfaceDesc kBTGaugeSurfaces[9] = { { "Heat", 0xFFFF, 0.0f / kBTPanelW, 0.0f, 320.0f / kBTPanelW, 0.5f, 0 }, { "Mfd2", 0xFFFF, 320.0f / kBTPanelW, 0.0f, 320.0f / kBTPanelW, 0.5f, 0 }, @@ -297,6 +303,9 @@ static const BTGaugeSurfaceDesc kBTGaugeSurfaces[6] = { "Mfd3", 0xFFFF, 320.0f / kBTPanelW, 0.5f, 320.0f / kBTPanelW, 0.5f, 0 }, { "sec", -1, 640.0f / kBTPanelW, 0.0f, 360.0f / kBTPanelW, 1.0f, 1 }, { "Comm", 0xFFFF, 1000.0f / kBTPanelW, 0.0f, 320.0f / kBTPanelW, 0.5f, 0 }, + { "Eng2", 0xFFFF, 320.0f / kBTPanelW, 0.0f, 320.0f / kBTPanelW, 0.5f, 0 }, + { "Eng1", 0xFFFF, 0.0f / kBTPanelW, 0.5f, 320.0f / kBTPanelW, 0.5f, 0 }, + { "Eng3", 0xFFFF, 320.0f / kBTPanelW, 0.5f, 320.0f / kBTPanelW, 0.5f, 0 }, }; // @@ -345,11 +354,17 @@ void BTDrawGaugeSurfaces(LPDIRECT3DDEVICE9 device, float px, float py, float pw, } if (svga == NULL) return; - for (int i = 0; i < 6; i++) + for (int i = 0; i < 9; i++) { const BTGaugeSurfaceDesc &d = kBTGaugeSurfaces[i]; L4GraphicsPort *port = static_cast(gr->GetGraphicsPort(d.portName)); if (port == NULL) continue; // this surface's port isn't configured -> skip + // Gitea #9: honor the live mode-driven `reconfigure` -- a mono plane + // whose channel is currently BlankColor contributes nothing on the pod + // monitor (its palette bits translate to black), so skip it; its + // non-blank sibling (Mfd or Eng, same cell rect) draws instead. + if (d.monoTint >= 0 && port->GetEnableID() == L4GraphicsPort::BlankColor) + continue; // Secondary-CRT unrotation direction is env-flippable (BT_GAUGE_SEC_ROT=3 // for the other way) in case the assumed pod mounting is mirrored. int rot = d.rotateCCW; @@ -4309,7 +4324,7 @@ SVGA16::SVGA16( ):Video16BitBuffered(init_width, init_height) { BuildWindows(init_width,init_height,windowed, secondaryIndex, aux1Index, aux2Index); - for (int _i = 0; _i < 8; _i++) // DEV-COMPOSITE: lazily created on first surface draw + for (int _i = 0; _i < 10; _i++) // DEV-COMPOSITE: lazily created on first surface draw mDevSurfaceTex[_i] = NULL; //STUBBED: VIDEO RB 1/15/07 # if defined(DEBUG) diff --git a/engine/MUNGA_L4/L4VB16.h b/engine/MUNGA_L4/L4VB16.h index 140c7e6..55f56c2 100644 --- a/engine/MUNGA_L4/L4VB16.h +++ b/engine/MUNGA_L4/L4VB16.h @@ -292,8 +292,9 @@ private: int mDisplayToUpdate; - LPDIRECT3DTEXTURE9 mDevSurfaceTex[8]; // DEV-COMPOSITE: one texture per gauge surface - // (sec/radar + the 5 MFD bit-planes), on the MAIN device. + LPDIRECT3DTEXTURE9 mDevSurfaceTex[10]; // DEV-COMPOSITE: one texture per gauge surface + // (sec/radar + the 5 MFD bit-planes + the Eng1-3 + // preset-page planes, Gitea #9), on the MAIN device. public: // DEV-COMPOSITE: extract ONE gauge surface from the shared pixelBuffer into // mDevSurfaceTex[slot] on the given (main) device + draw it as a quad at (dstX,dstY, @@ -462,6 +463,15 @@ public: Check(this); return bitMask; } + // DEV-COMPOSITE (Gitea #9): the dock honors the live reconfigure state -- + // a plane whose channel is currently BlankColor contributes nothing on the + // pod's shared-word palette LUT, so the dev panel skips drawing it. + L4GraphicsPort::ChannelEnableID + GetEnableID() + { + Check(this); + return channelEnable; + } SVGA16::PaletteID paletteID; diff --git a/engine/MUNGA_L4/L4VIDEO.cpp b/engine/MUNGA_L4/L4VIDEO.cpp index 5e028e4..d2c6328 100644 --- a/engine/MUNGA_L4/L4VIDEO.cpp +++ b/engine/MUNGA_L4/L4VIDEO.cpp @@ -3813,6 +3813,20 @@ void DPLRenderer::RecurseStaticObject(HierarchicalDrawComponent *obj) // (translucent + depth bias); merging them would draw them opaque. if (dobj->GetIsShadow()) banded = true; + // Issue #3 (cultural destruction): CulturalIcon/Landmark objects are + // DESTRUCTIBLE -- their intact model must vanish (and rubble appear) at + // BurningState via the revived StateInstanceSwitchRenderable's + // SetDrawObj swap. A model merged into the consolidated static mesh + // draws forever regardless of DrawObj, so destructibles stay OUT of + // consolidation and keep drawing individually (the same mechanism as + // the banded-LOD exclusion above). + { + Entity *owning = obj->GetOwningEntity(); + if (owning != NULL + && (owning->GetClassID() == CulturalIconClassID + || owning->GetClassID() == LandmarkClassID)) + banded = true; + } if (!banded) { //Only load it for a static object if it has a valid mesh- @@ -7086,12 +7100,6 @@ void case CulturalIconClassID: case LandmarkClassID: { - dpl_INSTANCE *this_instance = NULL; // BT bring-up: the per-object instance - // assignments below are DPL-stubbed - // (commented out) -> initialise so the - // /RTC1 uninitialised-variable check does - // not fault when this_instance is passed - // to StateInstanceSwitchRenderable. d3d_OBJECT *this_object; dpl_ISECT_MODE intersect_mode; HierarchicalDrawComponent *component = NULL; @@ -7131,6 +7139,15 @@ void continue; // next object } + // BT_CULT_LOG (issue #3): per-object census -- name/type + draw-op + // flags (hunting the opaque-black scorch quad on rubble models). + if (getenv("BT_CULT_LOG")) + { + DEBUG_STREAM << "[cultobj] entity=" << (long)entity->GetEntityID() + << " file=" << object_filename + << " type=" << (int)resource_type << std::flush; + } + SET_VIDEO_LOAD_OBJECT(); // HACK !!! load the object as uncached so entity identification will work right // this_object = dpl_LoadObject(object_filename, dpl_load_normal); @@ -7145,6 +7162,21 @@ void continue; // next object } } + else if (getenv("BT_CULT_LOG")) + { + // [cultobj] tail: the loaded object's per-op flags + DEBUG_STREAM << " ops=" << this_object->GetDrawOpCount(); + for (int opn = 0; opn < this_object->GetDrawOpCount(); opn++) + { + const L4DRAWOP *op = this_object->GetDrawOp(opn); + DEBUG_STREAM << " [" << opn + << " aT=" << (int)op->alphaTest + << " dcl=" << (int)op->drawAsDecal + << " sky=" << (int)op->drawAsSky << "]"; + } + DEBUG_STREAM << " shadow=" << (int)this_object->GetIsShadow() + << "\n" << std::flush; + } if (first_object) { first_object = False; @@ -7169,42 +7201,29 @@ void } else { - if (renderer_modes | L4VideoObject::BillboardObject) - { - //------------------------------------------ - // attach additional object to separate DCS - // because it is billboarded - //------------------------------------------ - component = new DPLStaticChildRenderable( - entity, - false, - this_object, - intersect_mode, - intersect_mask, - offset_matrix, - NULL); -// this_DCS = this_child->GetDCS(); -// this_instance = this_child->GetInstance(); - } - else - { - //---------------------------------------- - // attach additional objects to root_DCS - // (HACK) temporary implementation (HACK) - //---------------------------------------- - component = new DCSInstanceRenderable( - entity, // Entity to attach the renderable to - DCSInstanceRenderable::Static, // How/when to execute the renderable - this_object, // object to connect to the instance - NULL, // the DCS to add the instance to - intersect_mode, // type of intersections to do on this object - intersect_mask, // intersection mask for the object - True); // initial visibility setting - //this_DCS = NULL; -// this_instance = another_instance->GetInstance(); - } + //------------------------------------------------------------ + // Issue #3 (cultural destruction): additional cultural objects + // (the RUBBLE model above all) used to go to + // DPLStaticChildRenderable / DCSInstanceRenderable -- but as + // TOP-LEVEL mRenderables entries those classes read the bare + // matrix stack (identity), so the rubble drew at the WORLD + // ORIGIN, not at the icon (and pre-revival it consolidated + // there permanently). RootRenderable places from + // myEntity->localToWorld -- the same path the intact (first) + // object draws through -- so every cultural object is one. + //------------------------------------------------------------ + component = new RootRenderable( + entity, // Entity to attach the renderable to + RootRenderable::Static, // How/when to execute the renderable + this_object, // the additional object (rubble etc.) + false, // DPL Zone (for culling) + intersect_mode, // type of intersections to do on this object + intersect_mask); // intersection mask for the object } - // add the new object to our renderables list + // add the new object to our renderables list. Keep the draw + // component in hand -- the revived state switch (issue #3) + // controls IT (SetDrawObj show/hide), not a dead DPL instance. + HierarchicalDrawComponent *draw_component = component; if (component) mRenderables.Add(component); component = NULL; @@ -7212,6 +7231,7 @@ void // Hook up the object to an instance switch renderable responsive to state // StateIndicator* simulation_state = (StateIndicator *)entity->GetAttributePointer("SimulationState"); + if (simulation_state != NULL && draw_component != NULL) switch(resource_type) { case L4VideoObject::Object: @@ -7219,7 +7239,8 @@ void component = new StateInstanceSwitchRenderable( entity, // Entity to attach the renderable to StateInstanceSwitchRenderable::Watcher, // How/when to execute the renderable - this_instance, // the instance to control + draw_component, // the draw component to control + this_object, // its drawable (restored on show) False, // true to turn on in this state, false for off simulation_state, // State dial we use to control the on/off CulturalIcon::BurningState); // State that we look for @@ -7230,7 +7251,8 @@ void component = new StateInstanceSwitchRenderable( entity, // Entity to attach the renderable to StateInstanceSwitchRenderable::Watcher, // How/when to execute the renderable - this_instance, // the instance to control + draw_component, // the draw component to control + this_object, // its drawable (restored on show) True, // true to turn on in this state, false for off simulation_state, // State dial we use to control the on/off CulturalIcon::BurningState); // State that we look for @@ -8563,12 +8585,19 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte if (mCamShipHUD) mCamShipHUD->Render(0, &viewTransform); + // DEV-COMPOSITE: in DOCKED mode (BT_DEV_GAUGES_DOCK) blit the 6-surface gauge panel + // into this window as the LAST draw before EndScene (no-op otherwise / off pod). + extern void BTDrawGaugeInset(LPDIRECT3DDEVICE9 device); + BTDrawGaugeInset(mDevice); + // DIAG (off by default): BT_SHOT= dumps this instance's backbuffer to a // PNG every 90 frames -- non-disruptive per-instance frame capture without // foregrounding the window. Built for the -net render glitch (task #53); // moved AFTER the 2D pass (2026-07-18, camera-seat bring-up) so captures - // include the HUD overlays (reticle, camera-ship ranking window) the screen - // actually shows. + // include the HUD overlays (reticle, camera-ship ranking window), and AFTER + // the docked gauge inset (2026-07-19, Gitea #10 audit -- the 07-18 reorder + // had left the capture BEFORE BTDrawGaugeInset, so BT_DEV_GAUGES_DOCK shots + // were missing the gauge panel the screen actually shows). { const char *shotPath = getenv("BT_SHOT"); if (shotPath) @@ -8591,11 +8620,6 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte } } - // DEV-COMPOSITE: in DOCKED mode (BT_DEV_GAUGES_DOCK) blit the 6-surface gauge panel - // into this window as the LAST draw before EndScene (no-op otherwise / off pod). - extern void BTDrawGaugeInset(LPDIRECT3DDEVICE9 device); - BTDrawGaugeInset(mDevice); - hr = mDevice->EndScene(); // DEV-COMPOSITE: default mode -- render the 6 cockpit surfaces into a SEPARATE window diff --git a/engine/MUNGA_L4/L4VIDRND.cpp b/engine/MUNGA_L4/L4VIDRND.cpp index 28b04d6..e3ba10e 100644 --- a/engine/MUNGA_L4/L4VIDRND.cpp +++ b/engine/MUNGA_L4/L4VIDRND.cpp @@ -11,6 +11,11 @@ float gBTEyeSwayX = 0.0f; // lateral weight-shift (the walk "swagger"), same sou // through the jointtorso HingeRenderable in the draw traversal (see the note in // DPLEyeRenderable::Execute). Kept published for diagnostics/correlation. float gBTEyeTwist = 0.0f; +// The player torso's live ELEVATION aim (rad), published by mech4.cpp's HUD +// tick. The aim-boresight leveling below (task #48) strips the TERRAIN body +// pitch from the pick ray -- this is the pilot's DELIBERATE pitch, re-applied +// after the leveling so aimed shots follow the R/F (stick-Y) elevation. +float gBTEyeElev = 0.0f; #pragma hdrstop #include "l4vidrnd.h" @@ -4118,51 +4123,47 @@ void // Constructor for StateInstanceSwitchRenderable // StateInstanceSwitchRenderable::StateInstanceSwitchRenderable( - Entity *entity, // Entity to attach the renderable to - ExecutionType execution_type, // How/when to execute the renderable - dpl_INSTANCE *this_instance, // the instance to control - Logical sense, // true to turn on in this state, false for off - StateIndicator *state_dial, // State dial we use to control the on/off - unsigned trigger_state // State that we look for + Entity *entity, // Entity to attach the renderable to + ExecutionType execution_type, // How/when to execute the renderable + HierarchicalDrawComponent *controlled_component,// the draw component to show/hide + d3d_OBJECT *controlled_object, // the drawable it shows when visible + Logical sense, // true to turn on in this state, false for off + StateIndicator *state_dial, // State dial we use to control the on/off + unsigned trigger_state // State that we look for ): VideoRenderable(entity, execution_type) { - //STUBBED: DPL RB 1/14/07 - //// - //// Check incoming data for correctness - //// - //Check_Pointer(this_instance); - //Check(state_dial); - //// - //// Initialize variables - //// - //myInstance = this_instance; - //mySense = sense; - //myStateDial = state_dial; - //myTriggerState = trigger_state; - //// - //// Put the instance in the proper state and std::flush it - //// - //if(myStateDial->GetState() == myTriggerState) - //{ - // if(mySense == True) - // myLastInstanceState = True; - // else - // myLastInstanceState = False; - //} - //else - //{ - // if(mySense == True) - // myLastInstanceState = False; - // else - // myLastInstanceState = True; - //} - //dpl_SetInstanceVisibility ( myInstance, myLastInstanceState ); - //dpl_FlushInstance ( myInstance ); - //// - //// Connect us to the state dial's watcher hook - //// - //state_dial->AddVideoWatcher(this); + // REVIVED (issue #3): the 1995 body verbatim, retargeted from the dead DPL + // instance-visibility API onto the port's SetDrawObj in-place drawable swap. + // + // Check incoming data for correctness + // + Check_Pointer(controlled_component); + Check(state_dial); + // + // Initialize variables + // + myComponent = controlled_component; + myObject = controlled_object; + mySense = sense; + myStateDial = state_dial; + myTriggerState = trigger_state; + // + // Put the instance in the proper state + // + if(myStateDial->GetState() == myTriggerState) + { + myLastInstanceState = mySense ? True : False; + } + else + { + myLastInstanceState = mySense ? False : True; + } + myComponent->SetDrawObj(myLastInstanceState ? myObject : NULL); + // + // Connect us to the state dial's watcher hook + // + state_dial->AddVideoWatcher(this); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Destructor for StateInstanceSwitchRenderable @@ -4178,7 +4179,7 @@ Logical { // Call our parent's TestInstance first VideoRenderable::TestInstance(); - Check_Pointer(myInstance); + Check_Pointer(myComponent); Check(myStateDial); return True; } @@ -4191,37 +4192,38 @@ Logical void StateInstanceSwitchRenderable::Execute() { - //STUBBED: DPL RB 1/14/07 - //unsigned - // new_state; - //// - //// Check our variables - //// - //Check(this); - //// - //// See what state we should go into - //// - //if(myStateDial->GetState() == myTriggerState) - //{ - // if(mySense == True) - // new_state = True; - // else - // new_state = False; - //} - //else - //{ - // if(mySense == True) - // new_state = False; - // else - // new_state = True; - //} - //if(new_state != myLastInstanceState) - //{ - // myLastInstanceState = new_state; - // dpl_SetInstanceVisibility ( myInstance, myLastInstanceState ); - // dpl_FlushInstance ( myInstance ); - //} - //// Call the next lower execute method + // REVIVED (issue #3): the 1995 body verbatim on the SetDrawObj swap. + unsigned + new_state; + // + // Check our variables + // + Check(this); + // + // See what state we should go into + // + if(myStateDial->GetState() == myTriggerState) + { + new_state = mySense ? True : False; + } + else + { + new_state = mySense ? False : True; + } + if((Logical)new_state != myLastInstanceState) + { + myLastInstanceState = (Logical)new_state; + myComponent->SetDrawObj(myLastInstanceState ? myObject : NULL); + if (getenv("BT_CULT_LOG")) + { + DEBUG_STREAM << "[cultvis] switch: entity=" << (long)myEntity->GetEntityID() + << " sense=" << (int)mySense + << " state=" << myStateDial->GetState() + << " -> " << (myLastInstanceState ? "SHOW" : "HIDE") + << "\n" << std::flush; + } + } + // Call the next lower execute method VideoRenderable::Execute(); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -5838,11 +5840,24 @@ void if (D3DXVec3LengthSq(&fwdLevel) > 1e-6f) { D3DXVec3Normalize(&fwdLevel, &fwdLevel); - zax = D3DXVECTOR3(-fwdLevel.x, 0.0f, -fwdLevel.z); // back = -level fwd + // TORSO-ELEVATION AIM (pitch-fix follow-through): the + // leveling above strips the TERRAIN body pitch -- but the + // pilot's DELIBERATE R/F elevation must survive into the + // pick ray, or aimed shots fire level over low targets + // (the "still fires over the truck" report). Re-apply + // the published torso elevation about the level axis: + // fwd = (cos e * level, sin e). +e = aim UP (matches + // the view compose's pixel-calibrated sign). + extern float gBTEyeElev; + const float ce = cosf(gBTEyeElev); + const float se = sinf(gBTEyeElev); + D3DXVECTOR3 fwdAim(fwdLevel.x * ce, se, fwdLevel.z * ce); + D3DXVec3Normalize(&fwdAim, &fwdAim); + zax = D3DXVECTOR3(-fwdAim.x, -fwdAim.y, -fwdAim.z); // back = -aimed fwd D3DXVECTOR3 wup(0.0f, 1.0f, 0.0f); D3DXVec3Cross(&xax, &wup, &zax); D3DXVec3Normalize(&xax, &xax); - D3DXVec3Cross(&yax, &zax, &xax); // == world +Y + D3DXVec3Cross(&yax, &zax, &xax); } else { diff --git a/engine/MUNGA_L4/L4VIDRND.h b/engine/MUNGA_L4/L4VIDRND.h index a1c1057..1805ed9 100644 --- a/engine/MUNGA_L4/L4VIDRND.h +++ b/engine/MUNGA_L4/L4VIDRND.h @@ -72,6 +72,12 @@ public: // task #58 diagnostics: expose the parent link so probes can walk the tree. HierarchicalDrawComponent *GetParentProbe() const { return m_parent; } + // Issue #3 (cultural destruction): the consolidation pass must identify which + // entity a static component belongs to (destructible CulturalIcon/Landmark + // objects stay OUT of the merged static mesh so their visibility can switch). + // Base components have no entity link; VideoRenderable overrides with myEntity. + virtual Entity *GetOwningEntity() const { return NULL; } + d3d_OBJECT *GetDrawObj() { return this->graphicalObject; } // Swap this component's drawable in place. Execute() re-reads graphicalObject // every frame, so changing it makes the segment draw a different mesh next @@ -241,6 +247,8 @@ class VideoRenderable: virtual bool IsStatic() { return (this->myExecutionType == ExecutionType::Static); } + virtual Entity *GetOwningEntity() const { return myEntity; } // issue #3 (cultural destruction) + protected: Entity *myEntity; // The entity we are linked to ExecutionType myExecutionType; // How/when to execute the renderable @@ -1594,17 +1602,28 @@ class InstanceSwitchRenderable: //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // StateInstanceSwitchRenderable +// REVIVED 2026-07-19 (issue #3, cultural destruction): the 2007 port stubbed the +// whole class ("STUBBED: DPL RB 1/14/07"), so the intact->rubble visual swap on +// CulturalIcon/Landmark death NEVER fired -- destroyed trucks/buildings kept +// their intact model. The DPL instance-visibility mechanism is dead in the +// D3D9 renderer; the port-native equivalent is the SetDrawObj() in-place +// drawable swap (the same mechanism the mech damage-model RemakeEntity swap +// uses). The switch now controls a HierarchicalDrawComponent directly: +// visible = component draws its captured d3d_OBJECT, hidden = DrawObj is NULL. +// Semantics preserved verbatim from the commented 1995 body: initial state set +// in the ctor, AddVideoWatcher on the state dial, toggle only on a real change. class StateInstanceSwitchRenderable: public VideoRenderable { public: StateInstanceSwitchRenderable( - Entity *entity, // Entity to attach the renderable to - ExecutionType execution_type, // How/when to execute the renderable - dpl_INSTANCE *this_instance, // the instance to control + Entity *entity, // Entity to attach the renderable to + ExecutionType execution_type, // How/when to execute the renderable + HierarchicalDrawComponent *controlled_component,// the draw component to show/hide + d3d_OBJECT *controlled_object, // the drawable it shows when visible Logical sense, // true to turn on in this state, false for off StateIndicator *state_dial, // State dial we use to control the on/off - unsigned trigger_state); // State that we look for + unsigned trigger_state); // State that we look for ~StateInstanceSwitchRenderable(); @@ -1618,8 +1637,10 @@ class StateInstanceSwitchRenderable: Logical myLastInstanceState, mySense; - dpl_INSTANCE - *myInstance; + HierarchicalDrawComponent + *myComponent; // the controlled draw component + d3d_OBJECT + *myObject; // its drawable (restored on show) unsigned myTriggerState; StateIndicator diff --git a/engine/MUNGA_L4/bgfload.cpp b/engine/MUNGA_L4/bgfload.cpp index 0918420..197bc6e 100644 --- a/engine/MUNGA_L4/bgfload.cpp +++ b/engine/MUNGA_L4/bgfload.cpp @@ -1,6 +1,7 @@ // bgfload.cpp - see bgfload.h. Parse logic ported from port/src/bgf.cpp; directory // scanning reimplemented on Win32 (FindFirstFile) so it builds under C++14. #include "bgfload.h" +#include "image.h" // truecolor-slice grayscale probe (rampableSlice, issue #3) #include #include @@ -247,6 +248,40 @@ bool hasNormals(uint16_t id) { return id == TAG_VERTEX_XYZ_N || id == TAG_VERTEX_XYZ_N_UV; } +// TRUECOLOR-slice ramp eligibility (issue #3, the wreck scorch quad). Only 4 +// shipped textures use a truecolor BSL slice (bdam8=RGB444; bdet9/bexp9/tree9= +// RGBA4444). bexp9 (MECHMD's scorch splat, material basev:bvx9_mtl ramp +// 'cdusty') and bdet9 (the trans-rail lattice, ramps 'softer'/'cdusty') are +// GRAYSCALE in their RGB (100% / 98.8% r==g==b texels, corpus-measured) -- they +// author the same luminance->ramp colouring as every mono slice, and the old +// blanket "channel < 6" gate left the scorch a raw dark quad instead of the +// cdusty dark-brown->dusty-tan gradient. bdam8 (the damage sheet) is genuinely +// COLOURED (33% non-gray texels) -- luminance-ramping it would destroy its +// colour, which is what the blanket gate was protecting. Gate on the actual +// slice content: a truecolor slice ramps only when its RGB is effectively +// grayscale. Cached per path#channel (decode runs once per texture). +bool rampableSlice(const std::string& texPath, int channel) { + if (channel < 6 || texPath.empty()) return true; // mono slices always ramp-eligible + static std::map cache; + std::string key = texPath; + key += '#'; + key += (char)('0' + (channel % 10)); + auto it = cache.find(key); + if (it != cache.end()) return it->second; + bool gray = false; + Image img = decodeImage(texPath, channel); + if (img.ok && !img.argb.empty()) { + size_t same = 0; + for (uint32_t p : img.argb) { + const uint8_t r = (uint8_t)(p >> 16), g = (uint8_t)(p >> 8), b = (uint8_t)p; + if (r == g && g == b) ++same; + } + gray = ((float)same / (float)img.argb.size()) >= 0.95f; + } + cache[key] = gray; + return gray; +} + struct Chunk { uint16_t id = 0; const uint8_t* data = nullptr; @@ -421,6 +456,36 @@ struct MaterialResolver { return reg; } + // CROSS-LIBRARY TEXTURE REGISTRY (issue #3, the black scorch quad): material + // libs also reference TEXTURES defined in OTHER libs -- MECHMD.bgf's scorch + // base uses basev:bvx9_mtl whose texture `bexp9_tex` is defined in the FX + // libs (image in BEXP.BSL), not in BASEV.BMF. A per-file texMaps lookup + // left the quad untextured, drawn with its zero ambient = an opaque BLACK + // rectangle. Same shape as the ramp registry above: on first cross-file + // miss, sweep the indexed BMFs once and cache every TEXTURE definition + // (first stem in priority order wins; same-file defs are preferred before + // this registry is hit). BT_TEX_XLIB=0 disables. + static const std::map& globalTexMaps() { + static std::map reg; + static bool built = false; + if (!built) { + built = true; + const char* v = getenv("BT_TEX_XLIB"); + if (v == nullptr || v[0] != '0') { + for (const auto& kv : bmfIndex()) { + std::vector buf = readFileBytes(kv.second); + if (buf.size() <= 8 || std::memcmp(buf.data(), "DIV-BIZ2", 8) != 0) continue; + std::vector roots; + if (!parseRange(buf.data() + 8, buf.data() + buf.size(), roots)) continue; + std::map maps; + collectTextureMaps(roots, maps); + for (const auto& m : maps) reg.insert(m); // first wins + } + } + } + return reg; + } + static void collectMaterials(const std::vector& cs, const std::map& texMaps, const std::map >& ramps, @@ -438,7 +503,13 @@ struct MaterialResolver { info.color = packColor(rdF32(ch.data), rdF32(ch.data + 4), rdF32(ch.data + 8)); haveColor = true; info.hasDiffuse = true; - } else if (ch.id == TAG_AMBIENT && ch.len >= 12 && !haveColor) { + } else if (ch.id == TAG_AMBIENT && ch.len >= 12 && !haveColor + && (rdF32(ch.data) + rdF32(ch.data + 4) + rdF32(ch.data + 8)) > 0.001f) { + // AMBIENT fallback only when it carries a real colour: an + // all-zero ambient means "unset", not "tint black" -- e.g. + // basev:bvx9_mtl (the wreck scorch quad, issue #3) authors + // ambient (0,0,0) + texture + ramp; the black fallback + // painted the whole quad as an opaque BLACK rectangle. info.color = packColor(rdF32(ch.data), rdF32(ch.data + 4), rdF32(ch.data + 8)); haveColor = true; info.hasDiffuse = true; @@ -467,16 +538,25 @@ struct MaterialResolver { } } if (!name.empty() && (haveColor || !texName.empty() || info.hasRamp)) { + const TexRef* ref = nullptr; auto tm = texMaps.find(texName); - if (tm != texMaps.end()) { + if (tm != texMaps.end()) ref = &tm->second; + else if (!texName.empty()) { + // cross-library texture reference (see globalTexMaps): + // e.g. basev:bvx9_mtl -> bexp9_tex (defined in the FX libs) + const auto& reg = globalTexMaps(); + auto gt = reg.find(texName); + if (gt != reg.end()) ref = >->second; + } + if (ref != nullptr) { const auto& imgs = imageIndex(); - auto f = imgs.find(stemLower(tm->second.map)); + auto f = imgs.find(stemLower(ref->map)); if (f != imgs.end()) { info.texPath = f->second; - info.texChannel = tm->second.channel; - info.texScroll = tm->second.scroll; - info.texScrollU = tm->second.scrollU; - info.texScrollV = tm->second.scrollV; + info.texChannel = ref->channel; + info.texScroll = ref->scroll; + info.texScrollU = ref->scrollU; + info.texScrollV = ref->scrollV; } } out[name] = info; @@ -644,10 +724,13 @@ struct Builder { // RAMP applies ONLY to no-normal geometry (see hasNormals()): normal-bearing // vehicle/missile batches keep their material diffuse colour + texture (lit path), // so they must NOT be ramped even when the material carries a ramp ref (cdusty). - // TRUECOLOR BSL slices (channel >= 6: RGB444/RGBA4444, e.g. damcolor's bdam8 - // damage sheet) are real colour images -- luminance-ramping them would - // destroy their colour, so they take the plain textured path. - const bool useRamp = currentHasRamp && (!hasNormals(vtag) || currentTSphere) && currentTexChannel < 6; + // TRUECOLOR BSL slices (channel >= 6: RGB444/RGBA4444) ramp only when their RGB + // is effectively GRAYSCALE (rampableSlice): bexp9/bdet9 are authored gray + ramp + // (the wreck scorch splat's cdusty gradient, issue #3); damcolor's bdam8 damage + // sheet is a real colour image -- luminance-ramping it would destroy its colour, + // so it takes the plain textured path. + const bool useRamp = currentHasRamp && (!hasNormals(vtag) || currentTSphere) + && rampableSlice(currentTex, currentTexChannel); const uint32_t base = (uint32_t)px.size(); const uint32_t localCount = (uint32_t)lx.size(); @@ -794,6 +877,10 @@ struct Builder { batch.texScrollU = currentTexScrollU; batch.texScrollV = currentTexScrollV; batch.vertexAlpha = vertexAlpha; + // RGBA4444 slice (channel >= 8): the texture's AUTHORED alpha channel is + // a cutout mask (bexp9 scorch splat / bdet9 rail lattice / tree9 cards) + // -- the renderer alpha-tests it in the opaque pass (see bgfload.h). + batch.texAlpha = (currentTexChannel >= 8) && !currentTex.empty(); batch.hasRamp = useRamp; // tsphere warp flag: warpBands>0 signals L4D3D's ramp bake to CONTRAST- // COMPRESS the bintA cloud toward mid-grey (BT_WARP_CONTRAST) before applying diff --git a/engine/MUNGA_L4/bgfload.h b/engine/MUNGA_L4/bgfload.h index 5c27558..32e2d8e 100644 --- a/engine/MUNGA_L4/bgfload.h +++ b/engine/MUNGA_L4/bgfload.h @@ -44,6 +44,16 @@ struct BgfDrawBatch { // alpha). The verts keep their authored RGBA gradient and the draw routes // to the alpha-blend pass, unlit, colour = texture x vertex gradient. bool vertexAlpha = false; + // TEXTURE ALPHA CUTOUT (issue #3, the wreck scorch splat): an RGBA4444 BSL + // slice (texChannel >= 8) carries an AUTHORED alpha channel -- in the shipped + // content it is a BINARY 0/240 cutout mask (bexp9 = the scorch splat + // silhouette, 78.5% transparent; bdet9 = the trans-rail lattice; tree9 = the + // tree/leaf cards). Drawn opaque, the quad's transparent surround renders as + // a hard-edged rectangle (the "dark square" under the wreck). The renderer + // alpha-TESTS these batches in the opaque pass (same states as PUNCH, but the + // alpha comes from the texture itself -- no black-texel keying, which would + // hole the near-black charred splat centre). + bool texAlpha = false; // RAMP (task #20): colorize the grayscale texture by luminance across // rampLo->rampHi (the authentic IG-board terrain colouring). When set, the // renderer bakes lerp(rampLo, rampHi, texLum) into the texture and draws with diff --git a/game/original/BT/PPC.CPP b/game/original/BT/PPC.CPP index f067a69..3540b58 100644 --- a/game/original/BT/PPC.CPP +++ b/game/original/BT/PPC.CPP @@ -34,7 +34,8 @@ PPC::SharedData &PPC::ClassDerivations, Emitter::GetMessageHandlers(), // task #6: the port's accessor idiom -- inherits // MechWeapon's ConfigureMappables/ChooseButton - MechWeapon::GetAttributeIndex(), // gauge wave: inherit temps/InputVoltage/OutputVoltage/PercentDone (was empty) + Emitter::GetAttributeIndex(), // issue #11: the authentic Emitter table (Seek*/Laser*/ + // OutputVoltage=currentLevel) -- chains MechWeapon's PPC::StateCount ); diff --git a/game/reconstructed/CLASSMAP.md b/game/reconstructed/CLASSMAP.md index 595d59e..fe47bc4 100644 --- a/game/reconstructed/CLASSMAP.md +++ b/game/reconstructed/CLASSMAP.md @@ -31,13 +31,18 @@ Subsystem (MUNGA base — have source: RP/MUNGA/SUBSYSTM.HPP) │ │ │ cockpit heads-up display (torso-horizon gimbal + reticle/threat/status read-outs). Confirms the │ │ │ "Resolved conflict" note: @4b7f94 is the HUD, mislabeled "MechTech" by mech.cpp's 0xBD6 factory case. │ │ │ DefaultData 00511078; resource +0xF4 FlickerRate, +0xF8 SegmentPageName(idx), +0xFC HorizontalMovementPerSecond, +0x100 HorizontalLimit. -│ │ │ members: flickerRate@0x1D8, torso/target read-outs@0x1DC-0x1EC, lockFlag@0x1F4, visible@0x1F8, -│ │ │ threatVector(Vec3)@0x1FC, compassHeading(Vec3)@0x208 (setter @4b7810), blink/slew accums@0x218-0x230, +│ │ │ members (re-based 2026-07-19, Gitea #10, per the re-dumped attr table @5110b8): flickerRate@0x1D8, +│ │ │ rotationOfTorsoHorizontal@0x1DC(<-Torso+0x1D8 twist), torsoLimits@0x1E0/0x1E4, torsoSpeed@0x1E8, +│ │ │ rangeToTarget@0x1EC, @0x1F0 unbound, visible@0x1F4(init 1), lockFlag@0x1F8(init 0), +│ │ │ hotBoxVector(Vec3)@0x1FC, threatVector(Vec3)@0x208 (setter @4b7810 = SetThreatVector, ex-"SetCompassHeading" mislabel), +│ │ │ compassHeading(Scalar)@0x214(=yaw+twist), blink/slew accums@0x218-0x230, │ │ │ linkedEntity@0x234(=Mech+0x438), statusAlarm(2)@0x238, horizontalTorsoOffset@0x294, HMPS@0x298, HorizontalLimit@0x29C, flickerActive@0x2A0. │ │ │ overrides: HandleMessage@4b7780(s9, body not captured), ResetToInitialState@4b77bc(s10). HudSimulation@4b7830 (Performance, PTR@511170). -│ │ │ attr table @5110c0 (ids 4-0xd: RotationOfTorsoHorizontal/HorizontalTorsoLimit{Right,Left}/SpeedOfTorsoHorizontal/ -│ │ │ RangeToTarget/Visible/Lock/HotBoxVector/ThreatVector/CompassHeading) — NB attr name<->offset bindings only -│ │ │ partially agree with ctor/sim usage (UNCERTAIN). TestInstance@4b81b8 (deriv @511088 "HUD"). UpdateFlicker@4b7ed4. +│ │ │ attr table @5110b8 (16-byte records; ids 3-0xd: FlickerRate@0x1D8 + RotationOfTorsoHorizontal@0x1DC/ +│ │ │ HorizontalTorsoLimit{Right@0x1E0,Left@0x1E4}/SpeedOfTorsoHorizontal@0x1E8/RangeToTarget@0x1EC/Visible@0x1F4/ +│ │ │ Lock@0x1F8/HotBoxVector@0x1FC/ThreatVector@0x208/CompassHeading@0x214) — RESOLVED 2026-07-19: the old +│ │ │ "@5110c0, ids 4-0xd" transcription had dropped the leading id-3 FlickerRate record and shifted every offset +│ │ │ one slot; ctor+HudSimulation usage now agrees exactly. TestInstance@4b81b8 (deriv @511088 "HUD"). UpdateFlicker@4b7ed4. │ │ ├─ Searchlight vtable 005114d4 classID 0xBD8 ctor @4b84dc dtor @4b8568 CSS @4b85a8 [searchlight.cpp DONE] model 0xF4 │ │ │ illumination subsystem (corroborated by btl4vid MakeMechRenderables "0xBD8 SearchLight"). DefaultData 00511198. │ │ │ empty resource (CSS only stamps). members: lightState@0x1D8 ("LightState" attr id5), @@ -102,7 +107,7 @@ Subsystem (MUNGA base — have source: RP/MUNGA/SUBSYSTM.HPP) │ │ resource (size 0x1B0) adds +0x190 VelocityEfficiency, +0x194 AccelerationEfficiency, +0x198 SeekVoltage[5](fractions×ratedVoltage), │ │ +0x1AC SeekVoltageRecommendedIndex (parser @4b9140). attr table @00511588; string pool @00511660 ("Myomers"@0051166d). │ │ vtable overrides vs PoweredSubsystem: slot0 dtor@4b9114, slot9 HandleMessage@4b8a8c(→4b0efc), slot10 ResetToInitialState@4b8aa4(→4b0e6c), -│ │ slot15 GetSpeedReading@4b8f94 (base 4b1780; km/h÷350 gauge readout, best-effort), slot16 HasAdequateVoltage@4b8f3c (overrides IsSourceShorted base 4b0b5c). +│ │ slot15 SeekVoltageResponse@4b8f94 (base 4b1780; the SeekVoltageGraph vtbl+0x3C sampler: sqrt(AvailableOutput(v)·3.6/350) — issue #11 resolved the old "GetSpeedReading km/h gauge" guess; FUN_004dd138 = sqrt not fabs), slot16 HasAdequateVoltage@4b8f3c (overrides IsSourceShorted base 4b0b5c). │ │ helpers: AvailableOutput@4b8ac0 (drive = MechBaseSpeed × V/recommendedSeekV × heatFactor × (1-damage)), │ │ RegisterMaxOutput@4b8ef0 (→ Mech maxSpeed@0x7A0; loop @part_012:10322 over Mech myomer list@0x1EB), │ │ MyomersSimulation@4b8d18 (Performance, per-tick work→heat accum@0x1C8, gated by FUN_004ad7d4; best-effort registration), @@ -147,8 +152,13 @@ Subsystem (MUNGA base — have source: RP/MUNGA/SUBSYSTM.HPP) │ throttleForward@0x1ac/throttleReverse@0x1b0/leftPedal@0x1b4/rightPedal@0x1b8 (cleared by derived ctors). │ InterpretControls@4d196c (throttle detent snap 1.0±0.05, BuildPilotArray, review-mode bit 0x200000, │ target-range ramp range=250*2^exp, exp clamp[0,5]; then chains MechControlsMapper::InterpretControls @4afd10). -│ ZoomTargetRangeIn@4d1b64 / ZoomTargetRangeOut@4d1b9c (±1.0). NotifyOfDisplayModeChange-override@4d1ae4 (vtbl+0x4C; ex-"SetControlMode", (mode bits18-20, -│ table @0051dbe4={0x40000,0x80000,0x100000}). SetPresetMode@4d1b24 (group*5+item -> {clear,set} mask table @0051dbf0[15]). +│ ZoomTargetRangeIn@4d1b64 / ZoomTargetRangeOut@4d1b9c (±1.0). NotifyOfControlModeChange-override@4d1acc (vtbl+0x48; forwards to base no-op @4b048c — +│ control mode never touches the secondary view). NotifyOfDisplayModeChange-override@4d1ae4 (vtbl+0x4C; ex-"SetControlMode"; Gitea #6 reconstructed +│ 2026-07-19 = THE secondary Damage/Critical/Heat selector, mode bits18-20, table @0051dbe4={0x40000,0x80000,0x100000}; pod input = streamed +│ EventMapping secondary-panel button 0x15 -> msg 0x15 CycleDisplayMode; button 0x18 -> msg 0x14 CycleControlMode). +│ SetPresetMode@4d1b24 (group*5+item -> {clear,set} mask table @0051dbf0[15]; Gitea #9 decode: set = ModeMFD{1,2,3}{Quad,Eng1-4} +│ = 1<<(group*5+item), bits 0-14 [section_dump:72901-72908 little-endian]; clear = the group's other/all page bits. +│ Group = the MFD (0/1/2 = lower-left/upper-center/lower-right), item 0 = Quad overview, 1-4 = eng-detail pages). │ EnterConfiguration@4d1840 (+0x38) / ExitConfiguration@4d18dc (+0x3C) (task #6 relabel (buttons 0x40..0x47, hat-skip 0x41->0x45, │ mode 0x8000=ConfigReady). AddOrErase evt/dir RET no-ops @4d195c/@4d1964 (+0x40/+0x44; TM can't regroup). KeypressMessageHandler@4d1bf0 │ (shared dispatcher: zoom/preset/pilot-cycle/look keys + fake-button dev keys; default -> @4d141c). @@ -477,11 +487,19 @@ Searchlight 0xBD8 and ThermalSight 0xBDE are recovered, not gaps.) NB: MechWeapon's recharge-level slot @0x320 is reused by `Emitter` as **outputVoltage** (the normalised discharge readiness; ==1.0 ⇒ fully charged, used in EMITTER.TCP). - `Emitter` adds from @0x3F0 (object ~0x478): seekVoltageIndex@0x3F0, seekVoltageRecommendedIndex@0x3F4, - seekVoltageCount@0x3FC, seekVoltage[5]@0x400, currentLevel@0x414, firingActive@0x418, + minSeekVoltageIndex@0x3F8, maxSeekVoltageIndex@0x3FC (issue #11 renames — the binary attribute + table @0x511dd4 publishes them as Min/MaxSeekVoltageIndex; the old seekStepCounter/seekVoltageCount + names were guesses), seekVoltage[5]@0x400, currentLevel@0x414, firingActive@0x418, beamOrientation@0x41C, beamScale@0x42C, beamLengthRatio@0x434, graphicLength@0x438, dischargeTime@0x43C, dischargeTimer@0x440, damageFraction@0x444, energyTotal@0x448, damagePortion@0x44C, heatPortion@0x450, energyCoefficient@0x454, seekRate@0x45C, beamEndpoint@0x460, beamFlag@0x46C, targetLocalFlag@0x470, targetEntity@0x474. Firing state machine carried in weaponAlarm level @0x364 (0=Firing,2=Loaded,3=Loading,4=TriggerDuringLoad). + Attr table @0x511dd4 (ids 0x1D-0x25, issue #11 dump): LaserOn@0x418, LaserScale@0x42C, + LaserRotation@0x41C, CurrentSeekVoltageIndex@0x3F0, RecommendedSeekVoltageIndex@0x3F4, + MinSeekVoltageIndex@0x3F8, MaxSeekVoltageIndex@0x3FC, SeekVoltage@0x400, OutputVoltage@0x414 + (currentLevel — RAW volts, the SeekVoltageGraph cursor feed). vtable slot 15 @4bb42c = + SeekVoltageResponse (the graph sampler): sqrt(damageFraction·v²·0.5·energyCoefficient / 2.0e8) + (power helper @4bb3f4; FUN_004dd138 = sqrt). ## Recovered resource structs (parsed by each CreateStreamedSubsystem) - HeatableSubsystem/HeatSink: +0xE4 StartingTemperature, +0xE8 Degradation, +0xEC Failure, +0xF0 ThermalConductance, +0xF4 ThermalMass, +0xF8 HeatSink index diff --git a/game/reconstructed/btinput.cpp b/game/reconstructed/btinput.cpp index 2a0eceb..f2af152 100644 --- a/game/reconstructed/btinput.cpp +++ b/game/reconstructed/btinput.cpp @@ -70,13 +70,18 @@ enum BTActionID BTActLookBehind, BTActAllStop, BTActModeCycle, + BTActDisplayCycle, // Gitea #6: secondary schematic cycle (Damage/Critical/Heat) BTActValve, + BTActFlush, // Gitea #7: HELD = coolant flush (InjectCoolant) BTActConfigHold, BTActGenerator1, BTActGenerator2, BTActGenerator3, BTActGenerator4, BTActReconnect, + BTActMfd1Cycle, // Gitea #9: cycle the lower-left MFD preset page + BTActMfd2Cycle, // Gitea #9: cycle the upper-center MFD preset page + BTActMfd3Cycle, // Gitea #9: cycle the lower-right MFD preset page BTActCount }; @@ -192,10 +197,13 @@ static const BTName sActionNames[] = { {"ViewToggle", BTActViewToggle}, {"LookBehind", BTActLookBehind}, {"AllStop", BTActAllStop}, {"ModeCycle", BTActModeCycle}, - {"Valve", BTActValve}, {"ConfigHold", BTActConfigHold}, + {"DisplayCycle", BTActDisplayCycle}, + {"Valve", BTActValve}, {"Flush", BTActFlush}, {"ConfigHold", BTActConfigHold}, {"Generator1", BTActGenerator1}, {"Generator2", BTActGenerator2}, {"Generator3", BTActGenerator3}, {"Generator4", BTActGenerator4}, {"Reconnect", BTActReconnect}, + {"Mfd1Cycle", BTActMfd1Cycle}, {"Mfd2Cycle", BTActMfd2Cycle}, + {"Mfd3Cycle", BTActMfd3Cycle}, {0, 0} }; @@ -263,7 +271,12 @@ static const char *sDefaultProfile = "key D4 button 0x45\n" "key G action ConfigHold\n" "key C action Valve\n" + "key H action Flush\n" "key M action ModeCycle\n" + "key N action DisplayCycle\n" + "key J action Mfd1Cycle\n" + "key K action Mfd2Cycle\n" + "key L action Mfd3Cycle\n" "key V action ViewToggle\n" "key B action LookBehind\n" "key F5 action Generator1\n" @@ -287,6 +300,7 @@ static const char *sDefaultProfile = "pad Y action ViewToggle\n" "pad X action LookBehind\n" "pad Start action ModeCycle\n" + "pad RightThumb action DisplayCycle\n" "pad Back action Valve\n"; //============================================================================= @@ -961,8 +975,13 @@ void case BTActLookBehind: next.lookBehind = 1; break; case BTActAllStop: next.allStop = 1; break; case BTActModeCycle: next.modeCycle = 1; break; + case BTActDisplayCycle: next.displayCycle = 1; break; case BTActValve: next.valve = 1; break; + case BTActFlush: next.flush = 1; break; case BTActConfigHold: next.configHold = 1; break; + case BTActMfd1Cycle: next.mfdCycle[0] = 1; break; + case BTActMfd2Cycle: next.mfdCycle[1] = 1; break; + case BTActMfd3Cycle: next.mfdCycle[2] = 1; break; case BTActGenerator1: next.genSel = 4; break; case BTActGenerator2: next.genSel = 5; break; case BTActGenerator3: next.genSel = 6; break; diff --git a/game/reconstructed/btinput.hpp b/game/reconstructed/btinput.hpp index 9698e6c..11be1a6 100644 --- a/game/reconstructed/btinput.hpp +++ b/game/reconstructed/btinput.hpp @@ -51,9 +51,14 @@ struct BTInputState int lookBehind; int allStop; int modeCycle; + int displayCycle; // Gitea #6: secondary schematic cycle (Damage/Critical/Heat) int valve; + int flush; // Gitea #7: coolant flush HELD (InjectCoolant) int configHold; int genSel; // 0 = none, 4..7 = Generator A..D, 8 = reconnect + // Gitea #9: per-MFD preset-page cycle (desktop senders for SetPresetMode; + // index 0/1/2 = Mfd1 lower-left / Mfd2 upper-center / Mfd3 lower-right) + int mfdCycle[3]; }; extern BTInputState gBTInput; diff --git a/game/reconstructed/btl4gau2.cpp b/game/reconstructed/btl4gau2.cpp index d7b8292..8b85705 100644 --- a/game/reconstructed/btl4gau2.cpp +++ b/game/reconstructed/btl4gau2.cpp @@ -57,7 +57,9 @@ // 0x463b8000=12000.0f generator max voltage // 0x42c80000=100.0f HeatSinkCluster numeric scale base // _DAT_004c8df0 heat numeric scale _DAT_004c92e0 warning threshold -// _DAT_004c6bdc/_DAT_004c6be0 seek-curve plot step/limit +// _DAT_004c6bdc=1200.0f / _DAT_004c6be0=12000.0f seek-curve plot step/limit +// ld80 @004c6bd0 / @004c6d74 = 8.3333333333333e-05 (exactly 1/12000) -- +// the voltage->y normaliser (y = v * (1/12000) * yScale) // DAT_0051936c PrepEngr engineer-screen number (1..12) // @@ -84,6 +86,10 @@ static const Scalar SeekVoltageYScale = 187.0f; // 0x433b0000 static const Scalar RedrawSentinel = 9999.0f; // 0x461c3c00 static const Scalar GeneratorMaxVoltage = 12000.0f; // 0x463b8000 static const Scalar HeatNumericBase = 100.0f; // 0x42c80000 +// SeekVoltageGraph plot constants (capstone disasm of @004c6934, task/issue #11): +static const Scalar SeekCurveStep = 1200.0f; // _DAT_004c6bdc (10 segments) +static const Scalar SeekCurveLimit = 12000.0f; // _DAT_004c6be0 (y-axis full scale) +static const Scalar SeekCurveYNorm = (Scalar)(1.0 / 12000.0); // ld80 @004c6bd0/@004c6d74 // (DebugStream warning channel, DAT_00524e20, is provided by the shared // reconstruction headers -- heat.hpp / mechrecon.hpp.) @@ -97,15 +103,6 @@ static char DAT_00518f24[] = "GeneratorCluster"; // subsystem name to resolve static int DAT_00518ee0 = 0; // GaugeRate static char DAT_00518f68[] = ""; // background image name -// -// Original FUN_0041bfc0 = Entity::FindAttributeIndex(name) -> ordinal. The -// WinTesla engine exposes attribute lookup via Simulation::AttributeIndexSet -// (Find/FindEntry by name) rather than a name->ordinal call, so this best-effort -// shim stands in for it; the stored indices are consumed by the (summarised) -// SeekVoltageGraph::Execute plot. -// -static int FindAttributeIndex(Entity *, const char *) { return -1; } - // // FUN_0041bfc0 == Entity::FindAttributeIndex(name) returns a POINTER into the // subsystem (subsystem + attributeMemberOffset), or 0 on miss. The composite @@ -250,14 +247,31 @@ protected: //########################################################################### //########################################################################### // SeekVoltageGraph @004c6798 +// (issue #11: fully reconstructed from the capstone disasm of +// @004c6798/@004c6920/@004c6934/@004c6be4/@004c6c30/@004c6c6c -- +// Ghidra had dropped every x87 argument; tools/disas2.py recovered them.) //########################################################################### //########################################################################### +// +// The subsystem's voltage-response sampler == the binary subsystem vtbl+0x3C +// (slot 15): Emitter @004bb42c = sqrt(seekPower(v)/2.0e8), Myomers @004b8f94 = +// sqrt(speed(v)*3.6/350). Reached through a complete-type dispatch bridge +// (emitter.cpp -> myomers.cpp) per the databinding rule (gotcha 8). +// +extern Scalar BTSeekVoltageSample(void *subsystem, Scalar voltage); +// +// The destroyed test == *(subsystem+0x40) == 1 (MechSubsystem::simulationState, +// 1 = Destroyed). Named-member bridge in powersub.cpp (databinding rule). +// +extern int BTSubsystemDestroyed(Subsystem *subsystem); + // // @004c6798 -- ctor (vtable PTR_FUN_0051a1fc). GraphicGauge base, interns and // ref-counts the "destroyed" bitmap, resolves the four SeekVoltage attribute -// indices off the weapon subsystem, sets the port extent (0x97,0x80,0x17d,0x13b) -// and stashes the 230x187 plot scale. +// POINTERS off the weapon subsystem (FUN_0041bfc0 returns subsystem+offset or +// 0), sets the view XOR draw op + the port extent (0x97,0x80,0x17d,0x13b) and +// stashes the 230x187 plot scale. // SeekVoltageGraph::SeekVoltageGraph( GaugeRate rate, @@ -278,22 +292,34 @@ SeekVoltageGraph::SeekVoltageGraph( destroyedImage = (char *)destroyed_image; renderer->warehousePointer->bitMapBin.Get(destroyedImage); // FUN_00442aec (AddRef) - currentSeekIndex = FindAttributeIndex(subsystem, "CurrentSeekVoltageIndex"); // FUN_0041bfc0 - minSeekIndex = FindAttributeIndex(subsystem, "MinSeekVoltageIndex"); - maxSeekIndex = FindAttributeIndex(subsystem, "MaxSeekVoltageIndex"); - seekVoltageTable = FindAttributeIndex(subsystem, "SeekVoltage"); + // FUN_0041bfc0(subsystem, name) == the engine's name-keyed attribute-pointer + // resolve (subsystem+memberOffset, or 0 on a miss); the composite ctors gate + // the tick/cursor draws on a 0 result. + currentSeekIndexPtr = (int *)AttributePointerOf(subsystem, "CurrentSeekVoltageIndex"); // this[0x25] + minSeekIndexPtr = (int *)AttributePointerOf(subsystem, "MinSeekVoltageIndex"); // this[0x26] + maxSeekIndexPtr = (int *)AttributePointerOf(subsystem, "MaxSeekVoltageIndex"); // this[0x27] + seekVoltageTablePtr = (Scalar *)AttributePointerOf(subsystem, "SeekVoltage"); // this[0x28] - voltageSubsystem = (Entity *)seek_voltage_pointer; // this[0x29] + liveVoltage = (Scalar *)seek_voltage_pointer; // this[0x29] this->subsystem = (Subsystem *)subsystem; // this[0x2A] - // graphicsView -> GraphicGauge::localView. SetExtent == SetPositionWithinPort; - // the original SetClip() (vtbl+0x0C) constrained drawing to that extent. + // @004c6798: (vtbl+0x0C)(3) then (vtbl+0x08)(0x97,0x80,0x17d,0x13b) -- + // SetOperation(Xor) + SetPositionWithinPort (GRAPH2D.h vtable order). The + // XOR op is what lets DrawTicks/DrawCursorBox erase by redrawing. + localView.SetOperation((GraphicsDisplay::Operation)3); // vtbl+0x0C Xor localView.SetPositionWithinPort(0x97, 0x80, 0x17d, 0x13b); // vtbl+0x08 xScale = SeekVoltageXScale; // this[0x2C] yScale = SeekVoltageYScale; // this[0x2D] drawCursor = draw_cursor; // this[0x2F] destroyedShown = 0; // this[0x2E] + // (binary leaves this[0x2B]/[0x30..0x32] uninitialised -- activation's + // BecameActive sentinel forces the first replot, which seeds them before + // any read. Seeded here for determinism.) + previousVoltage = RedrawSentinel; // this[0x2B] + cursorX = -1; // this[0x30] + cursorY = -1; // this[0x31] + tickIndexShown = -1; // this[0x32] } // @@ -311,38 +337,187 @@ Logical SeekVoltageGraph::TestInstance() const } // -// @004c6920 -- BecameActive: previousVoltage = 9999.0f (force first redraw). +// @004c6920 -- BecameActive: previousVoltage = 9999.0f (poison the cached +// response so the next Execute clears + replots the whole box -- THE top-box +// eraser on a page switch; the binary body writes ONLY this[0x2B]). // void SeekVoltageGraph::BecameActive() { - previousVoltage = RedrawSentinel; // @0xAC force first redraw - destroyedShown = 0; + previousVoltage = RedrawSentinel; // @0xAC force full redraw + if (getenv("BT_SEEK_LOG")) + DEBUG_STREAM << "[seek] BecameActive sub=" + << (subsystem ? subsystem->GetName() : "?") << "\n" << std::flush; } // -// @004c6934 -- Execute (the seek-voltage curve plot). BRING-UP: the full plot -// (SetupClip/DrawTicks/DrawCursor over a 0..1 voltage polyline) needs the live -// seek-voltage sampler (subsys vtbl+0x3c) + the four attribute indices; the -// attribute resolve is summarised (FindAttributeIndex returns -1), so the plot is -// deferred and Execute is inert until the sampler + attribute reads land. Kept as -// a defined no-op so the gauge's vtable is complete (no /FORCE crash when the -// Myomer/Energy cluster ticks its SeekVoltageGraph child). +// @004c6be4 -- the full-box erase ("SetupClip" in the old notes; it is really +// a clear): SetOperation(Replace), SetColor(0), MoveToAbsolute(0,0), +// DrawFilledRectangleToAbsolute(1000,1000) -- clipped to the view = the whole +// (0x97,0x80)-(0x17d,0x13b) top data box painted black. NOTE: leaves the view +// in Replace mode (the polyline plot relies on that; Execute restores Xor). +// +void SeekVoltageGraph::ClearGraph() +{ + localView.SetOperation((GraphicsDisplay::Operation)0); // vtbl+0x0C Replace + localView.SetColor(0); // vtbl+0x18 + localView.MoveToAbsolute(0, 0); // vtbl+0x24 + localView.DrawFilledRectangleToAbsolute(1000, 1000); // vtbl+0x48 +} + +// +// @004c6c30 -- the live cursor: an 8x8 filled square centred on +// (cursorX,cursorY), drawn in the view's current (Xor) op so a second call +// erases it. MoveToAbsolute(x-4,y-4) + DrawFilledRectangleToRelative(8,8). +// +void SeekVoltageGraph::DrawCursorBox() +{ + localView.MoveToAbsolute(cursorX - 4, cursorY - 4); // vtbl+0x24 + localView.DrawFilledRectangleToRelative(8, 8); // vtbl+0x4C +} + +// +// @004c6c6c -- the per-gear tick marks (XOR). For each seek index i in +// [*min..*max]: V = seekVoltage[i]; x = Round(sample(V) * xScale); +// y = Round(V * (1/12000) * yScale) [ld80 @004c6d74]. The gear the highlight +// was last drawn at (this[0x32]) gets the full L (axis->point->axis); the rest +// get 10-pixel axis stubs. Called in pairs to move the highlight (XOR erase + +// redraw). +// +void SeekVoltageGraph::DrawTicks() +{ + // (port guard: the binary derefs min/max unguarded -- its call sites only + // guarantee current+table non-null; all four always resolve on the + // emitter/myomer subsystems that own a graph.) + if (minSeekIndexPtr == 0 || maxSeekIndexPtr == 0) + return; + int last = *maxSeekIndexPtr; // this[0x27] deref + for (int i = *minSeekIndexPtr; i <= last; i++) // this[0x26] deref + { + Scalar gearVoltage = seekVoltageTablePtr[i]; // this[0x28][i] + int x = Round(BTSeekVoltageSample(subsystem, gearVoltage) * xScale); // (vtbl+0x3C)(V)*230 + int y = Round(gearVoltage * SeekCurveYNorm * yScale); // V/12000*187 + if (i == tickIndexShown) // this+0xC8 + { + localView.MoveToAbsolute(0, y); // vtbl+0x24 + localView.DrawLineToAbsolute(x, y); // vtbl+0x30 + localView.DrawLineToAbsolute(x, 0); + } + else + { + localView.MoveToAbsolute(0, y); + localView.DrawLineToAbsolute(10, y); + localView.MoveToAbsolute(x, 0); + localView.DrawLineToAbsolute(x, 10); + } + } +} + +// +// @004c6934 -- Execute (the seek-voltage response plot; full x87 recovery). +// +// DESTROYED branch (subsys simulationState==1): once, clear the box and draw +// the centred "destroyed" bitmap (edestryd.pcc) at +// ((Round(xScale)-w)/2, (Round(yScale)-h)/2), color 0xFF. +// +// LIVE branch: +// * on the destroyed->alive edge, call BecameActive (own vtbl+0xC == slot 3 +// of PTR_0051a1fc = @004c6920) to poison the cache; +// * change-test: sample the response at 12000 V (0x463b8000) and compare with +// the cached this[0x2B]; on change (or the 9999 activation sentinel): +// CLEAR (@004c6be4 -- the ghost eraser), SetColor(0xFF), MoveTo(0,0), then +// the polyline v = 0,1200,...,10800 (step _DAT_004c6bdc=1200 while +// v < _DAT_004c6be0=12000): LineTo(Round(sample(v)*230), +// Round(v * [1/12000 * 187])), reset the cursor slots to -1, restore +// SetOperation(Xor), and draw the gear ticks at the current index; +// * tick maintenance: when the current seek index moved, XOR-erase the old +// highlight + redraw at the new (two DrawTicks calls); +// * cursor (draw_cursor pages only): XOR-erase the old box (if any), sample +// at the LIVE voltage (*this[0x29]) and draw the new 8x8 box at +// (Round(sample(V)*230), Round(V * [1/12000 * 187])). // void SeekVoltageGraph::Execute() { - // (seek-voltage plot deferred -- see note above.) -} + if (BTSubsystemDestroyed(subsystem)) // *(subsys+0x40) == 1 + { + if (destroyedShown == 0) + { + destroyedShown = 1; // this[0x2E] + ClearGraph(); // @004c6be4 + L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer; + BitMap *image = warehouse->bitMapBin.Get(destroyedImage); // FUN_00442aec + if (image != NULL) // (port guard; binary derefs unchecked) + { + localView.MoveToAbsolute( // vtbl+0x24 centre the bitmap + (Round(xScale) - image->Data.Size.x) >> 1, + (Round(yScale) - image->Data.Size.y) >> 1); + localView.SetColor(0xff); // vtbl+0x18 + localView.DrawBitMap(0, image, // vtbl+0x54 + 0, 0, image->Data.Size.x - 1, image->Data.Size.y - 1); + } + warehouse->bitMapBin.Release(destroyedImage); // FUN_00442c12 + } + return; + } -// -// @004c6934 (original notes preserved) -- Execute. If the subsystem is destroyed -// (subsys+0x40 == 1) draw -// the centred "destroyed" bitmap once. Otherwise, when the live seek voltage -// (subsys vtbl+0x3c sampler) changes: clear (SetupClip @004c6be4), re-plot the -// 0..1 voltage curve as a polyline (step _DAT_004c6bdc up to _DAT_004c6be0, -// Round each point), then draw the min/max tick marks (DrawTicks @004c6c6c) -// and, if drawCursor, the live cursor at the current seek index -// (DrawCursor @004c6c30). -// + if (destroyedShown != 0) + { + destroyedShown = 0; + BecameActive(); // own vtbl+0xC (@004c6920) + } + + Scalar yStep = SeekCurveYNorm * yScale; // fld tbyte @004c6bd0 * this[0x2D] + + // change-test: the response curve's value at max voltage. + Scalar response = BTSeekVoltageSample(subsystem, GeneratorMaxVoltage); // (vtbl+0x3C)(12000.0f) + if (previousVoltage != response) // this[0x2B] + { + if (getenv("BT_SEEK_LOG")) + DEBUG_STREAM << "[seek] replot sub=" + << (subsystem ? subsystem->GetName() : "?") + << " response12k=" << response << "\n" << std::flush; + previousVoltage = response; + ClearGraph(); // @004c6be4 -- erases the WHOLE box + localView.SetColor(0xff); // vtbl+0x18 (Replace mode from the clear) + localView.MoveToAbsolute(0, 0); // vtbl+0x24 + for (Scalar v = 0.0f; v < SeekCurveLimit; v += SeekCurveStep) + { + int x = Round(BTSeekVoltageSample(subsystem, v) * xScale); + int y = Round(v * yStep); + localView.DrawLineToAbsolute(x, y); // vtbl+0x30 + } + cursorX = -1; // this[0x30] + cursorY = -1; // this[0x31] + localView.SetOperation((GraphicsDisplay::Operation)3); // vtbl+0x0C back to Xor + if (currentSeekIndexPtr != 0 && seekVoltageTablePtr != 0) + { + tickIndexShown = *currentSeekIndexPtr; // this[0x32] + DrawTicks(); // @004c6c6c + } + } + + // gear-change tick maintenance (XOR pair: erase old highlight, draw new). + if (currentSeekIndexPtr != 0 && seekVoltageTablePtr != 0) + { + int currentIndex = *currentSeekIndexPtr; + if (currentIndex != tickIndexShown) + { + DrawTicks(); // erase at the old index + tickIndexShown = currentIndex; + DrawTicks(); // draw at the new + } + } + + // live-voltage cursor (drawCursor pages: the emitter/PPC graphs). + if (drawCursor != 0) // this[0x2F] + { + if (cursorX >= 0) + DrawCursorBox(); // XOR-erase the old box + Scalar live = *liveVoltage; // *this[0x29] + cursorX = Round(BTSeekVoltageSample(subsystem, live) * xScale); + cursorY = Round(live * yStep); + DrawCursorBox(); // @004c6c30 + } +} //########################################################################### diff --git a/game/reconstructed/btl4gau2.hpp b/game/reconstructed/btl4gau2.hpp index ac26752..44e2b20 100644 --- a/game/reconstructed/btl4gau2.hpp +++ b/game/reconstructed/btl4gau2.hpp @@ -86,13 +86,22 @@ //####################################################################### - // SeekVoltageGraph -- the energy-weapon "seek voltage" curve display. + // SeekVoltageGraph -- the energy-weapon / myomer "seek voltage" response + // graph on the engineering detail pages (the "POWER" box). // @004c6798 ctor / @004c68ac dtor / @004c6920 BecameActive / @004c6934 Execute. - // vtable PTR_FUN_0051a1fc. Reads the four SeekVoltage attribute indices off - // the weapon subsystem ("CurrentSeekVoltageIndex" / "Min" / "Max" / - // "SeekVoltage") and plots a 0..230 x 0..187 curve, with min/max tick marks - // and a live cursor at the current seek index. When the subsystem is - // destroyed (subsys+0x40==1) it shows a centred "destroyed" bitmap instead. + // vtable PTR_FUN_0051a1fc. Resolves the four SeekVoltage attribute + // POINTERS off the weapon subsystem (FUN_0041bfc0 returns subsystem+offset: + // "CurrentSeekVoltageIndex" / "MinSeekVoltageIndex" / "MaxSeekVoltageIndex" + // / "SeekVoltage") and plots the subsystem's voltage-response curve + // (vtbl slot 15 sampler: Emitter @004bb42c / Myomers @004b8f94) over the + // top data box (0x97,0x80)-(0x17d,0x13b): x = response(v)*230 (0..1 + // normalised sqrt response), y = v/12000*187 (voltage axis), with per-gear + // tick marks (the current gear drawn as a full L) and, when draw_cursor, + // a live 8x8 XOR cursor at the subsystem's live voltage. When the + // subsystem is destroyed (simulationState@0x40==1) it shows a centred + // "destroyed" bitmap instead. Its Execute's full-box CLEAR (@004c6be4) + // is what erases the sibling eng pages' stale pixels on the shared Eng + // bit-plane -- the widget IS the top-box eraser (Gitea #10 finding A). //####################################################################### class SeekVoltageGraph : public GraphicGauge @@ -109,19 +118,30 @@ void BecameActive(); // @004c6920 void Execute(); // @004c6934 protected: + void ClearGraph(); // @004c6be4 (full-box erase) + void DrawTicks(); // @004c6c6c (per-gear ticks, XOR) + void DrawCursorBox(); // @004c6c30 (8x8 cursor, XOR) + char *destroyedImage; // @0x90 this[0x24] - int currentSeekIndex; // @0x94 this[0x25] attr idx - int minSeekIndex; // @0x98 this[0x26] attr idx - int maxSeekIndex; // @0x9C this[0x27] attr idx - int seekVoltageTable; // @0xA0 this[0x28] attr idx - Entity *voltageSubsystem; // @0xA4 this[0x29] + int *currentSeekIndexPtr; // @0x94 this[0x25] attr ptr (subsys+off) + int *minSeekIndexPtr; // @0x98 this[0x26] attr ptr + int *maxSeekIndexPtr; // @0x9C this[0x27] attr ptr + Scalar *seekVoltageTablePtr; // @0xA0 this[0x28] attr ptr (array base) + Scalar *liveVoltage; // @0xA4 this[0x29] the ctor's + // seek_voltage_pointer ("OutputVoltage" + // attr ptr / cluster seekValue cache) -- + // Execute derefs it for the live cursor Subsystem *subsystem; // @0xA8 this[0x2A] - Scalar previousVoltage; // @0xAC this[0x2B] (9999 = redraw) + Scalar previousVoltage; // @0xAC this[0x2B] cached response at + // 12000 V (9999 sentinel = force replot) Scalar xScale; // @0xB0 this[0x2C] =230.0f Scalar yScale; // @0xB4 this[0x2D] =187.0f int destroyedShown; // @0xB8 this[0x2E] Logical drawCursor; // @0xBC this[0x2F] - // helpers: SetupClip @004c6be4, DrawCursor @004c6c30, DrawTicks @004c6c6c + int cursorX; // @0xC0 this[0x30] (-1 = no cursor drawn) + int cursorY; // @0xC4 this[0x31] + int tickIndexShown; // @0xC8 this[0x32] gear index the tick + // highlight was last drawn at }; diff --git a/game/reconstructed/btl4mppr.cpp b/game/reconstructed/btl4mppr.cpp index 75c4baf..bec20b0 100644 --- a/game/reconstructed/btl4mppr.cpp +++ b/game/reconstructed/btl4mppr.cpp @@ -30,10 +30,12 @@ // @004d17ac ctor @004d1814 dtor // @004d196c InterpretControls (target-range ramp + review-mode watch) // @004d1b64 ZoomTargetRangeIn @004d1b9c ZoomTargetRangeOut -// @004d1ae4 SetControlMode (mode-mask switch; calls base NotifyOfDisplayMode) +// @004d1acc NotifyOfControlModeChange (+0x48; forwards to base no-op @004b048c) +// @004d1ae4 NotifyOfDisplayModeChange (+0x4C; the secondary-view mask swap +// -- Gitea #6 relabel; ex-"SetControlMode") // @004d1b24 SetPresetMode (group/item -> preset mode-mask) -// @004d1840 CreateTemporaryEventMappings @004d18dc RemoveTemporaryEventMappings -// @004d195c NotifyOfControlModeChange (no-op) @004d1964 NotifyOfConfigurationModeChange (no-op) +// @004d1840 EnterConfiguration @004d18dc ExitConfiguration +// @004d195c/@004d1964 AddOrErase evt/dir (+0x40/+0x44, RET no-ops) // @004d1bf0 KeypressMessageHandler (the shared L4 keyboard dispatcher) // MechThrustmasterMapper --------------------------- vtable @0051e3f0 // @004d21d0 ctor @004d22b4 dtor @004d22e0 TestInstance @@ -81,8 +83,11 @@ // _DAT_004d1ac8 = 0x437a0000 = 250.0f (target-range scale: range = 250*2^x) // _DAT_004d1b94 = 1.0f zoom-in step _DAT_004d1b98 = 0.0f min exponent // _DAT_004d1bcc = 1.0f zoom-out step _DAT_004d1bd0 = 5.0f max exponent -// DAT_0051dbe4[3] = {0x40000,0x80000,0x100000} control-mode mode-masks +// DAT_0051dbe4[3] = {0x40000,0x80000,0x100000} secondary-VIEW mode-masks +// (ModeSecondaryDamage/Critical/Heat, indexed by displayMode) // DAT_0051dbf0[15] = {clear,set} mode-mask pairs (group*5+item), preset select +// (little-endian: set = ModeMFD{1,2,3}{Quad,Eng1-4} bits 0-14 +// -- Gitea #9; see SetPresetMode below) // DAT_0051dcd0[8] = {0x37,0x36,0x35,0x34,0x33,0x32,0x31,0x30} hotbox buttons // // ModeManager (app+0x50): +0x4 currentMode mask, +0x8 savedMode mask. @@ -422,6 +427,23 @@ L4MechControlsMapper::MessageHandlerSet& } } + // + // (3b) PORT (Gitea #9): the desktop J/K/L per-MFD preset-page cycle + // edges (mech4 poll) -- the sender for the pod's per-MFD page-button + // banks; the body is the authentic SetPresetMode. + // + { + extern int gBTPresetCycle[3]; + for (int mfd_group = 0; mfd_group < 3; ++mfd_group) + { + if (gBTPresetCycle[mfd_group]) + { + gBTPresetCycle[mfd_group] = 0; + CyclePresetModeNow(mfd_group); + } + } + } + // // (4) Target-range zoom: slew current exponent toward demand, then set // the Mech's target range = 250 * 2^exponent. @@ -482,25 +504,61 @@ L4MechControlsMapper::MessageHandlerSet& // //############################################################################# -// SetControlMode @004d1ae4 +// NotifyOfControlModeChange @004d1acc (vtbl+0x48) +// NotifyOfDisplayModeChange @004d1ae4 (vtbl+0x4C) //############################################################################# // -// Swap the application mode-manager's control-mode bits (18..20) for the bit -// selected by new_mode, then notify the base of the implied display change. +// Gitea #6 RELABEL (2026-07-19). The old reconstruction carried @004d1ae4 as +// a non-virtual "SetControlMode" that nothing ever called, so the desktop's +// secondary MFD stayed pinned on the Damage view. The vtable @0051e440 +// (section_dump.txt:73033) pins the truth: +// +// +0x48 = @004d1acc -- dispatched by CycleControlModeMessageHandler +// (FUN_004afbe0) with the new controlMode. Capstone disasm: pushes +// its two args and forwards to the BASE no-op @004b048c. A +// control-mode (BAS/MID/ADV) change never touches the secondary +// view -- in the shipped binary OR here. +// +0x4C = @004d1ae4 -- dispatched by CycleDisplayModeMessageHandler +// (FUN_004afcac) with the new displayMode. THE secondary-view +// selector: clears mode bits 18..20 (& 0xffe3ffff = +// ~ModeSecondaryAll), sets the mask picked from the table @0051dbe4 +// = {ModeSecondaryDamage 0x40000, ModeSecondaryCritical 0x80000, +// ModeSecondaryHeat 0x100000}, then chains the base no-op +// @004b0494. displayMode 0/1/2 = Damage/Critical/Heat -- the +// manual-p13 "'Mech status Info center" schematic cycle (bottom +// left of the secondary screen). // void - L4MechControlsMapper::SetControlMode(int new_mode) + L4MechControlsMapper::NotifyOfControlModeChange(int new_mode) { - static const ModeMask control_mode_mask[] = // @0051dbe4 - { 0x40000, 0x80000, 0x100000 }; + MechControlsMapper::NotifyOfControlModeChange(new_mode); // @004b048c (no-op) + } + + void + L4MechControlsMapper::NotifyOfDisplayModeChange(int new_mode) + { + static const ModeMask display_mode_mask[] = // @0051dbe4 + { + BTL4ModeManager::ModeSecondaryDamage, // 0 -> 0x040000 + BTL4ModeManager::ModeSecondaryCritical, // 1 -> 0x080000 + BTL4ModeManager::ModeSecondaryHeat // 2 -> 0x100000 + }; BTL4ModeManager *mode_manager = (BTL4ModeManager*)application->GetModeManager(); // DAT_004efc94+0x50 - mode_manager->RemoveModeMask(0x001c0000); // clear bits 18..20 (~0xffe3ffff) - mode_manager->AddModeMask(control_mode_mask[new_mode]); + mode_manager->RemoveModeMask( // clear bits 18..20 + BTL4ModeManager::ModeSecondaryAll); // (& 0xffe3ffff) + mode_manager->AddModeMask(display_mode_mask[new_mode]); - NotifyOfDisplayModeChange(displayMode); // FUN_004b0494 + // DIAG (BT_MODE_LOG): the resulting manager mask -- proves the + // secondary-view bit actually flipped for the gauge layer gating. + if (getenv("BT_MODE_LOG")) + DEBUG_STREAM << "[mode] display notify " << new_mode + << " -> manager mask 0x" << std::hex + << mode_manager->GetModeMask() << std::dec << "\n" << std::flush; + + MechControlsMapper::NotifyOfDisplayModeChange(new_mode); // @004b0494 (no-op) } // @@ -508,9 +566,24 @@ L4MechControlsMapper::MessageHandlerSet& // SetPresetMode @004d1b24 //############################################################################# // -// Each (group,item) selects a preset; the table @0051dbf0 holds {clear,set} -// mode-mask pairs that remove the previous preset of that group and enable the -// chosen one. +// Each (group,item) selects an upper-MFD PRESET page; the table @0051dbf0 +// holds {clear,set} mode-mask pairs that remove the previous preset of that +// group and enable the chosen one. Group = the MFD (0/1/2 = Mfd1 lower-left / +// Mfd2 upper-center / Mfd3 lower-right); item 0 = the Quad overview page +// (four subsystem cluster panels), items 1-4 = the four full-screen +// engineering-detail pages (aux screens group*4+1..group*4+4, prepEngr). +// +// Gitea #9 TABLE FIX (2026-07-19): the set column had been transcribed from +// the section dump as BIG-endian dwords ({0x1e,0x01000000} instead of +// {0x1e,0x01}, etc.) -- so a preset press set a garbage high bit (and, for +// group 1 items 3-4 / group 2 items 0-2, stomped the LIVE ModeNonMapping / +// ModeIntercom / ModeSecondary* bits) while the ModeMFD* page bits never +// moved: the panels stayed pinned on their Quad pages. Ground truth +// (section_dump.txt:72901-72908, little-endian) decodes the table to exactly +// the BTL4MODE.HPP [T0] ModeMFD{1,2,3}{Quad,Eng1-4} allocation, bits 0-14: +// set = 1 << (group*5 + item); clear = the group's other four page bits +// (item 0) or all five (items 1-4). The presets are fully disjoint from the +// Mapping/NonMapping (15/16), Intercom (17) and Secondary* (18-20) bits. // void L4MechControlsMapper::SetPresetMode(int group, int item) @@ -518,14 +591,31 @@ L4MechControlsMapper::MessageHandlerSet& struct PresetMaskPair { ModeMask clear, set; }; static const PresetMaskPair preset_mask[15] = // @0051dbf0 (group*5+item) { - {0x1e, 0x01000000}, {0x1f, 0x02000000}, {0x1f, 0x04000000}, - {0x1f, 0x08000000}, {0x1f, 0x10000000}, - {0x3c0, 0x20000000}, {0x3e0, 0x40000000}, {0x3e0, 0x80000000}, - {0x3e0, 0x00010000}, {0x3e0, 0x00020000}, - {0x7800,0x00040000}, {0x7c00,0x00080000}, {0x7c00,0x00100000}, - {0x7c00,0x00200000}, {0x7c00,0x00400000} + // group 0 = Mfd1 (lower left) + {0x001e, BTL4ModeManager::ModeMFD1Quad}, // 0x0001 + {0x001f, BTL4ModeManager::ModeMFD1Eng1}, // 0x0002 + {0x001f, BTL4ModeManager::ModeMFD1Eng2}, // 0x0004 + {0x001f, BTL4ModeManager::ModeMFD1Eng3}, // 0x0008 + {0x001f, BTL4ModeManager::ModeMFD1Eng4}, // 0x0010 + // group 1 = Mfd2 (upper center) + {0x03c0, BTL4ModeManager::ModeMFD2Quad}, // 0x0020 + {0x03e0, BTL4ModeManager::ModeMFD2Eng1}, // 0x0040 + {0x03e0, BTL4ModeManager::ModeMFD2Eng2}, // 0x0080 + {0x03e0, BTL4ModeManager::ModeMFD2Eng3}, // 0x0100 + {0x03e0, BTL4ModeManager::ModeMFD2Eng4}, // 0x0200 + // group 2 = Mfd3 (lower right) + {0x7800, BTL4ModeManager::ModeMFD3Quad}, // 0x0400 + {0x7c00, BTL4ModeManager::ModeMFD3Eng1}, // 0x0800 + {0x7c00, BTL4ModeManager::ModeMFD3Eng2}, // 0x1000 + {0x7c00, BTL4ModeManager::ModeMFD3Eng3}, // 0x2000 + {0x7c00, BTL4ModeManager::ModeMFD3Eng4} // 0x4000 }; + if (group < 0 || group > 2 || item < 0 || item > 4) + { + return; + } + int index = group * 5 + item; BTL4ModeManager *mode_manager = @@ -533,6 +623,110 @@ L4MechControlsMapper::MessageHandlerSet& mode_manager->RemoveModeMask(preset_mask[index].clear); mode_manager->AddModeMask(preset_mask[index].set); + + // DIAG (BT_MODE_LOG): prove the ModeMFD* page bit actually flipped for + // the gauge reconfigure/prepEngr layer gating. + if (getenv("BT_MODE_LOG")) + DEBUG_STREAM << "[mode] preset (" << group << "," << item + << ") -> manager mask 0x" << std::hex + << mode_manager->GetModeMask() << std::dec << "\n" << std::flush; + } + +// +//############################################################################# +// CyclePresetModeNow (PORT desktop sender -- Gitea #9, no binary analog) +//############################################################################# +// +// On the pod every MFD has its own 8-button bank (Mfd1 lower-left = RIO +// buttons 0x08-0x0F, Mfd2 upper-center = 0x20-0x27, Mfd3 lower-right = +// 0x00-0x07) whose meanings are MODE-MASK-gated by the streamed "L4" .CTL +// EventMappings: on a Quad page the bank's outer buttons DIRECT-SELECT the +// populated Eng pages (mapper messages Aux1Eng1..Aux3Eng4 = 0x4-0x7 / 0x9-0xC +// / 0xE-0x11 -> SetPresetMode), on an Eng page one button returns to Quad +// (Aux1Quad/Aux2Quad/Aux3Quad = 0x3/0x8/0xD) and the rest drive the shown +// subsystem. A desktop keyboard cannot host 24 mode-dependent buttons, so +// the port cycles each MFD with one key (J/K/L -> mech4 edge poll -> +// gBTPresetCycle -> here); the message BODY is the authentic SetPresetMode. +// The cycle visits exactly the pages the pod's streamed buttons could reach: +// Quad always, an Eng page only if a roster subsystem is assigned to that +// aux screen (screens group*4+1 .. group*4+4; the shipped .CTL carries NO +// select button for an unpopulated screen -- e.g. the Blackhawk's screens +// 3, 11 and 12 are authored empty). +// + // The vehicleSubSystems/prepEngr aux-screen bridge (powersub.cpp). + extern bool BTGetSubsystemAuxScreen( + Subsystem *sub, int *screen, int *placement, char *label64); + + void + L4MechControlsMapper::CyclePresetModeNow(int group) + { + if (group < 0 || group > 2) + { + return; + } + + BTL4ModeManager *mode_manager = + (BTL4ModeManager*)application->GetModeManager(); + + // + // Current page = the set bit among the group's five ModeMFD* bits. + // + ModeMask mask = mode_manager->GetModeMask(); + int current = 0; // default: Quad + for (int i = 0; i < 5; ++i) + { + if (mask & (1 << (group * 5 + i))) + { + current = i; + break; + } + } + + // + // Which aux screens are populated on this mech (streamed sub+0x1dc)? + // + Mech *mech = GetMech(); + bool populated[5] = { true, false, false, false, false }; // Quad always + if (mech != NULL) + { + int count = mech->GetSubsystemCount(); + for (int i = 1; i < count; ++i) + { + Subsystem *sub = mech->GetSubsystem(i); + if (sub == NULL) + { + continue; + } + int screen = 0, placement = -1; + if (!BTGetSubsystemAuxScreen(sub, &screen, &placement, NULL)) + { + continue; + } + int item = screen - group * 4; // Eng item 1..4 of THIS group? + if (item >= 1 && item <= 4) + { + populated[item] = true; + } + } + } + + // + // Advance to the next reachable page (wraps through Quad). + // + int next = current; + for (int step = 1; step <= 5; ++step) + { + int candidate = (current + step) % 5; + if (populated[candidate]) + { + next = candidate; + break; + } + } + if (next != current) + { + SetPresetMode(group, next); + } } // diff --git a/game/reconstructed/btl4mppr.hpp b/game/reconstructed/btl4mppr.hpp index e6a15cf..5a12fef 100644 --- a/game/reconstructed/btl4mppr.hpp +++ b/game/reconstructed/btl4mppr.hpp @@ -104,13 +104,35 @@ ZoomTargetRangeOut(); // @004d1b9c //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Control / display modes + // Control / display modes (Gitea #6 relabel -- the vtable pins the slots). + // + // The binary's L4 vtable @0051e440 (section_dump): +0x48 = @004d1acc, + // +0x4C = @004d1ae4. CycleControlModeMessageHandler (FUN_004afbe0) + // dispatches vtbl+0x48 with the new controlMode; CycleDisplayModeMessage- + // Handler (FUN_004afcac) dispatches vtbl+0x4C with the new displayMode. + // So @004d1ae4 -- the {0x40000,0x80000,0x100000} secondary-view mask swap + // the old reconstruction called "SetControlMode" (never called by anyone) + // -- is the NotifyOfDisplayModeChange OVERRIDE: the secondary screen's + // Damage / Critical / Heat schematic is selected by the DISPLAY mode + // (manual p13: the "'Mech status Info center", bottom left of the + // secondary screen), NOT by the BAS/MID/ADV control mode (manual p6: top + // right of the secondary screen). @004d1acc (NotifyOfControlModeChange) + // just forwards to the base no-op @004b048c -- a control-mode change + // never touches the secondary view. // public: - void - SetControlMode(int new_mode); // @004d1ae4 (mode-mask switch) + virtual void + NotifyOfControlModeChange(int new_mode); // @004d1acc (+0x48, chains base no-op) + virtual void + NotifyOfDisplayModeChange(int new_mode); // @004d1ae4 (+0x4C, secondary-view mask swap) void SetPresetMode(int group, int item); // @004d1b24 (preset mode-mask) + void + CyclePresetModeNow(int group); // PORT (Gitea #9): desktop J/K/L + // per-MFD page cycle; visits Quad + + // the POPULATED Eng pages only (the + // reachable set of the pod's streamed + // page buttons); body = SetPresetMode //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Configuration support (overrides the MechControlsMapper traps; task #6 @@ -119,8 +141,8 @@ // old reconstruction mislabeled "NotifyOfControlModeChange / // NotifyOfConfigurationModeChange". Only MechRIOMapper overrides the // AddOrErase pair with real bodies -- a Thrustmaster cockpit cannot - // regroup. (The REAL NotifyOf* L4 overrides are @004d1acc/@004d1ae4, - // vtable +0x48/+0x4C -- out of scope here.) + // regroup. (The REAL NotifyOf* L4 overrides @004d1acc/@004d1ae4, + // vtable +0x48/+0x4C, are declared in the mode section above.) // public: virtual void diff --git a/game/reconstructed/emitter.cpp b/game/reconstructed/emitter.cpp index aa0d31d..37f626b 100644 --- a/game/reconstructed/emitter.cpp +++ b/game/reconstructed/emitter.cpp @@ -73,6 +73,7 @@ # include #endif #include // HostManager::GetEntityPointer -- the update-record target resolve (task #51) +#include // sqrt -- the SeekVoltageResponse sampler (issue #11) // E8 (bring-up): the player fire input. The controls mapper that would normally write // the weapon's fireImpulse is bypassed (mech4.cpp), so EmitterSimulation reads this @@ -153,13 +154,49 @@ Receiver::MessageHandlerSet& MechWeapon::GetMessageHandlers()); // copy-inherit return messageHandlers; } -Simulation::AttributeIndexSet Emitter::AttributeIndex; +// +// issue #11: the AUTHENTIC Emitter attribute table (binary @0x511dd4, dumped +// records {id, name*, offset|1, 0}, ids 0x1D..0x25 -- a dense run chained +// straight after MechWeapon's 0x1C WeaponState, so no pads needed). The four +// Seek* rows are what the SeekVoltageGraph ctor (@004c6798) resolves by name; +// OutputVoltage here binds currentLevel@0x414 -- the RAW charge voltage (the +// graph's live-cursor feed), which the retired MechWeapon 0x1D port alias +// (rechargeLevel, 0..1) used to shadow. Function-local statics per the +// static-init-order rule (reconstruction-gotchas #9). +// +// The dense-prefix lock (gotcha #11): the run must start exactly one past +// MechWeapon's last id (0x1C WeaponState) == the binary's own numbering. +static_assert((int)Emitter::LaserOnAttributeID == 0x1D, + "Emitter attr run must chain at 0x1D (binary table @0x511dd4) -- " + "did a MechWeapon attribute get added/removed?"); + +Emitter::AttributeIndexSet& + Emitter::GetAttributeIndex() +{ + static const Emitter::IndexEntry entries[] = + { + ATTRIBUTE_ENTRY(Emitter, LaserOn, firingActive), // 0x1D @0x418 + ATTRIBUTE_ENTRY(Emitter, LaserScale, beamScale), // 0x1E @0x42C + ATTRIBUTE_ENTRY(Emitter, LaserRotation, beamOrientation), // 0x1F @0x41C + ATTRIBUTE_ENTRY(Emitter, CurrentSeekVoltageIndex, seekVoltageIndex), // 0x20 @0x3F0 + ATTRIBUTE_ENTRY(Emitter, RecommendedSeekVoltageIndex, seekVoltageRecommendedIndex), // 0x21 @0x3F4 + ATTRIBUTE_ENTRY(Emitter, MinSeekVoltageIndex, minSeekVoltageIndex), // 0x22 @0x3F8 + ATTRIBUTE_ENTRY(Emitter, MaxSeekVoltageIndex, maxSeekVoltageIndex), // 0x23 @0x3FC + ATTRIBUTE_ENTRY(Emitter, SeekVoltage, seekVoltage), // 0x24 @0x400 (array base) + ATTRIBUTE_ENTRY(Emitter, OutputVoltage, currentLevel) // 0x25 @0x414 (RAW volts) + }; + static Emitter::AttributeIndexSet attributeIndex( + ELEMENTS(entries), entries, + MechWeapon::GetAttributeIndex() + ); + return attributeIndex; +} Emitter::SharedData Emitter::DefaultData( &Emitter::ClassDerivations, Emitter::GetMessageHandlers(), - MechWeapon::GetAttributeIndex(), // gauge wave: inherit temps/InputVoltage/OutputVoltage/PercentDone (was empty) + Emitter::GetAttributeIndex(), // own table (Seek*/Laser*/OutputVoltage) chained to MechWeapon's Emitter::StateCount ); @@ -590,7 +627,7 @@ int } int prev = seekVoltageIndex; // 0x3f0 - int modulus = seekVoltageCount + 1; // 0x3fc + 1 + int modulus = maxSeekVoltageIndex + 1; // 0x3fc + 1 int next = (prev + 1) % modulus; seekVoltageIndex = next; // 0x3f0 if (prev < next) @@ -663,6 +700,35 @@ void } } +// +// @004bb3f4 -- the seek discharge power at a supply voltage (issue #11, +// capstone disasm: fld v; fmul v; fmul [0x4bb428]=0.5f; fmul [this+0x454]; +// -> * [this+0x444]): +// P(v) = damageFraction * (v^2 * 0.5 * energyCoefficient) +// i.e. the damage share of the E = 1/2 k V^2 discharge energy at voltage v. +// +Scalar + Emitter::SeekPower(Scalar voltage) +{ + return damageFraction * (voltage * voltage * 0.5f * energyCoefficient); // 0x444 / 0x454 +} + +// +// @004bb42c (vtable slot 15) -- the SeekVoltageGraph voltage-response sampler +// (issue #11). Disasm: locals A = 0x4d3ebc20 (exactly 2.0e8f), B = 0; +// r = (SeekPower(v) - B) / (A - B); FUN_004dd138(r) == sqrt (part_015.c:4026 -- +// the old "fp magnitude" reading was wrong). So: the graph's x-axis is the +// square root of the discharge power normalised to a 2.0e8 full scale. +// +Scalar + Emitter::SeekVoltageResponse(Scalar voltage) +{ + Scalar r = (SeekPower(voltage) - 0.0f) / (2.0e8f - 0.0f); // 0x4d3ebc20 + // (FUN_004dd138 routes a negative operand to the matherr handler; P >= 0 + // always here -- guarded for port safety.) + return (r > 0.0f) ? (Scalar)sqrt((double)r) : 0.0f; +} + //############################################################################# // Subsystem virtual overrides @@ -855,7 +921,7 @@ Emitter::Emitter( energyCoefficient = 1.0f; // 0x454 seekVoltageIndex = 0; // 0x3f0 seekVoltageRecommendedIndex = 0; // 0x3f4 - seekVoltageCount = 0; // 0x3fc + maxSeekVoltageIndex = 0; // 0x3fc for (int sv = 0; sv < 5; ++sv) seekVoltage[sv] = 1.0f; // 0x400[] @@ -894,7 +960,7 @@ Emitter::Emitter( { if (subsystem_resource->seekVoltage[i] == -1.0f) // _DAT_004bb3b0 sentinel { - seekVoltageCount = i - 1; // 0x3fc + maxSeekVoltageIndex = i - 1; // 0x3fc (= count-1) break; } // the authored values are FRACTIONS of the generator's rated @@ -904,7 +970,7 @@ Emitter::Emitter( } seekVoltageRecommendedIndex = subsystem_resource->seekVoltageRecommendedIndex; // 0x3f4 <- +0x1d8 seekVoltageIndex = seekVoltageRecommendedIndex; // 0x3f0 - seekStepCounter = 0; // 0x3f8 + minSeekVoltageIndex = 0; // 0x3f8 // energyCoefficient = energyTotal / (seekVoltage[rec]^2 * 0.5) // -> E = 0.5 * V^2 * EC lands exactly on energyTotal at V=seekV[rec] @@ -935,7 +1001,7 @@ Emitter::Emitter( // damageFraction = damageAmount / (damageAmount + heatCostToFire) damageFraction = damageData.damageAmount / (damageData.damageAmount + heatCostToFire); // 0x444 - seekStepCounter = 0; // 0x3f0/0x45c bookkeeping + minSeekVoltageIndex = 0; // 0x3f8 (always 0 -- attr 0x22) Check_Fpu(); } @@ -1063,3 +1129,27 @@ Subsystem *CreateEmitterSubsystem(Mech *owner, int id, void *seg) return (Subsystem *) new (Memory::Allocate(0x478)) Emitter(owner, id, (Emitter::SubsystemResource *)seg, Emitter::DefaultData); } + +//===========================================================================// +// issue #11 bridge -- the SeekVoltageGraph's voltage-response sample (the +// binary calls the SUBSYSTEM vtbl+0x3C / slot 15 virtually; the port's gauge +// TU cannot include the weapon headers, so the dispatch lives here in a +// complete-type TU per the databinding rule, gotcha 8). Emitter (and PPC, +// which IS an Emitter both by derivation and by factory) -> @004bb42c; +// everything else forwards to the Myomers bridge (myomers.cpp, @004b8f94). +//===========================================================================// +extern Scalar BTMyomersSeekSample(void *subsystem, Scalar voltage); // myomers.cpp + +Scalar BTSeekVoltageSample(void *subsystem, Scalar voltage) +{ + Entity *entity = (Entity *)subsystem; + if (entity == 0) + { + return 0.0f; + } + if (entity->IsDerivedFrom(Emitter::ClassDerivations)) + { + return ((Emitter *)entity)->SeekVoltageResponse(voltage); // @004bb42c + } + return BTMyomersSeekSample(subsystem, voltage); // @004b8f94 (guarded) +} diff --git a/game/reconstructed/emitter.hpp b/game/reconstructed/emitter.hpp index 8559302..1444df9 100644 --- a/game/reconstructed/emitter.hpp +++ b/game/reconstructed/emitter.hpp @@ -89,7 +89,29 @@ class NotationFile; public: static Derivation ClassDerivations; static Receiver::MessageHandlerSet& GetMessageHandlers(); // inherits MechWeapon's (task #6) - static AttributeIndexSet AttributeIndex; + + // + // The AUTHENTIC Emitter attribute table (binary @0x511dd4, 9 records + // {id, name, offset|1, 0}, ids 0x1D..0x25 chained after MechWeapon's + // 0x1C WeaponState -- dumped issue #11). The Seek* rows feed the + // SeekVoltageGraph engineering-page widget (ctor @004c6798 resolves + // them by NAME); OutputVoltage binds the RAW charge voltage + // currentLevel@0x414 (the graph's live cursor feed) -- NOT the 0..1 + // rechargeLevel (the retired MechWeapon port alias). + // + enum { + LaserOnAttributeID = MechWeapon::NextAttributeID, // 0x1D @0x418 firingActive + LaserScaleAttributeID, // 0x1E @0x42C beamScale + LaserRotationAttributeID, // 0x1F @0x41C beamOrientation + CurrentSeekVoltageIndexAttributeID, // 0x20 @0x3F0 + RecommendedSeekVoltageIndexAttributeID, // 0x21 @0x3F4 + MinSeekVoltageIndexAttributeID, // 0x22 @0x3F8 + MaxSeekVoltageIndexAttributeID, // 0x23 @0x3FC + SeekVoltageAttributeID, // 0x24 @0x400 (array base) + OutputVoltageAttributeID, // 0x25 @0x414 currentLevel (RAW volts) + NextAttributeID // 0x26 + }; + static AttributeIndexSet& GetAttributeIndex(); static SharedData DefaultData; // Update record (replication) -- extends the MechWeapon record with the @@ -172,6 +194,8 @@ class NotationFile; // slot 9 TakeDamage @004bafc8 (NOT yet reconstructed) // slot 10 ResetToInitialState (MechWeapon base @004b96ec) // slot 13 PrintState (MechWeapon base @004b9d00) + // slot 15 SeekVoltageResponse @004bb42c (the SeekVoltageGraph sampler; + // Myomers' analog @004b8f94 shares the slot) // slot 16 ReadyToDischarge (Simulation interface) // slot 17 ComputeOutputVoltage (Simulation interface) // slot 18 FireWeapon @@ -184,6 +208,13 @@ class NotationFile; void PrintState(); // slot 13, @004bb014 + // @004bb42c (vtable slot 15) -- the SeekVoltageGraph's voltage-response + // sampler: sqrt((SeekPower(v) - 0) / (2.0e8f - 0)) [FUN_004dd138 = sqrt; + // consts from the capstone disasm, issue #11]. Maps a supply voltage to + // the graph's 0..1 x-axis (normalised sqrt of the discharge power). + Scalar + SeekVoltageResponse(Scalar voltage); // slot 15, @004bb42c + // // Beam-render accessors (the visible-beam layer draws each weapon's own // beam from this live sim state -- the authentic per-weapon fire look). @@ -233,6 +264,12 @@ class NotationFile; // Internal charge / discharge helpers (non-virtual; called by the sim) // protected: + // @004bb3f4 -- the seek discharge power at a supply voltage: + // damageFraction * v^2 * 0.5 [_DAT_004bb428] * energyCoefficient. + // (== the E = 1/2 k V^2 discharge energy scaled by the damage split.) + Scalar + SeekPower(Scalar voltage); // @004bb3f4 + // @004ba838 -- track the linked voltage source: derive a seek rate and // integrate currentLevel; dump the I^2R heat back into the source. void @@ -313,8 +350,15 @@ class NotationFile; // * beamLengthRatio -> is beamScale.z@0x434 int seekVoltageIndex; // @0x3F0 (param_1[0xfc]) current charge-curve index int seekVoltageRecommendedIndex;// @0x3F4 (param_1[0xfd]) recommended index - int seekStepCounter; // @0x3F8 (param_1[0xfe]) reset 0 - int seekVoltageCount; // @0x3FC (param_1[0xff]) index modulus / count-1 + // issue #11 IDENTITY CORRECTION (binary attribute table @0x511dd4): + // @0x3F8 is published as "MinSeekVoltageIndex" and @0x3FC as + // "MaxSeekVoltageIndex" -- the old names (seekStepCounter, "reset 0" / + // seekVoltageCount, "count-1") were best-effort guesses that happened + // to carry exactly those values (min is always 0; max = count-1, the + // AdvanceSeekVoltage modulus-1). Renamed to the attribute truth -- + // the exact analog of Myomers @0x328/@0x32C. + int minSeekVoltageIndex; // @0x3F8 (param_1[0xfe]) always 0 + int maxSeekVoltageIndex; // @0x3FC (param_1[0xff]) count-1 (index modulus - 1) Scalar seekVoltage[5]; // @0x400 (param_1[0x100..0x104]) per-index target voltages Scalar currentLevel; // @0x414 (param_1[0x105]) accumulated charge int firingActive; // @0x418 (param_1[0x106]) reset 0; FireWeapon sets 1 @@ -340,6 +384,11 @@ class NotationFile; struct EmitterLayoutCheck { static_assert(offsetof(Emitter, seekVoltageIndex) == 0x3F0, "Emitter::seekVoltageIndex @0x3F0 (MechWeapon must end 0x3F0)"); + // issue #11: the four attribute-published Seek* slots (table @0x511dd4) + static_assert(offsetof(Emitter, minSeekVoltageIndex) == 0x3F8, "Emitter::minSeekVoltageIndex @0x3F8 (attr 0x22)"); + static_assert(offsetof(Emitter, maxSeekVoltageIndex) == 0x3FC, "Emitter::maxSeekVoltageIndex @0x3FC (attr 0x23)"); + static_assert(offsetof(Emitter, seekVoltage) == 0x400, "Emitter::seekVoltage @0x400 (attr 0x24)"); + static_assert(offsetof(Emitter, firingActive) == 0x418, "Emitter::firingActive @0x418 (attr 0x1D LaserOn)"); static_assert(offsetof(Emitter, currentLevel) == 0x414, "Emitter::currentLevel @0x414"); static_assert(offsetof(Emitter, beamOrientation) == 0x41C, "Emitter::beamOrientation @0x41C (Quaternion 16B)"); static_assert(offsetof(Emitter, beamScale) == 0x42C, "Emitter::beamScale @0x42C"); diff --git a/game/reconstructed/heatfamily_reslice.cpp b/game/reconstructed/heatfamily_reslice.cpp index 0722b72..ea2f78e 100644 --- a/game/reconstructed/heatfamily_reslice.cpp +++ b/game/reconstructed/heatfamily_reslice.cpp @@ -293,8 +293,12 @@ HeatWatcher::HeatWatcher( watchedSubsystem = subsystem_resource->watchedSubsystem; // this[0x4a] = res +0xE4 heatAlarm.Initialize(3); // FUN_0041b9ec(this+0x4b, 3) - if ((subsystem_resource->subsystemFlags & SegmentCopyMask) == 0 // (flags & 0xC) == 0 - && (subsystem_resource->subsystemFlags & MasterHeatSinkFlag) != 0) // flags & 0x100 + // (Gitea #7 sweep) the binary @4aeb40 tests *(param_2+0x28) = the OWNER + // MECH's simulationFlags (the same misread fixed in the Reservoir ctor; + // the watcher happened to work because its resource flags mirror the + // master bits on the player mech -- read the authentic source anyway). + if ((owner->simulationFlags & SegmentCopyMask) == 0 // (flags & 0xC) == 0 + && (owner->simulationFlags & MasterHeatSinkFlag) != 0) // flags & 0x100 { SetPerformance(&HeatWatcher::WatchSimulation); // this[7..9] = PTR 0050e634 -> @4aeac4 } @@ -559,17 +563,28 @@ Reservoir::Reservoir( coolantFlowScale = 0.0f; // this[0x57] @0x15C (== "word57") reservoirAlarm.SetLevel(0); // FUN_0041bbd8(this+0x74, 0) -- inject flag = 0 (was redundant injectActive=0) - if ((subsystem_resource->subsystemFlags & SegmentCopyMask) == 0 - && (subsystem_resource->subsystemFlags & MasterHeatSinkFlag) != 0) + // Gitea #7 gate correction: the binary @4af408 tests *(param_2+0x28) -- + // param_2 is the OWNER MECH (the factory passes the mech as arg 2), so + // this is owner->simulationFlags, NOT the resource's subsystemFlags (the + // old resource read was false on the player mech -> no CoolantSimulation + // Performance -> the flush never drained). Same gate the AggregateHeatSink + // ctor (@4ae8d0, below) already reads correctly. [T1] + if ((owner->simulationFlags & SegmentCopyMask) == 0 + && (owner->simulationFlags & MasterHeatSinkFlag) != 0) { SetPerformance(&Reservoir::CoolantSimulation); // this[7..9] = PTR 0050e6b4 -> @4aef78 HeatSink *link = (HeatSink *)linkedSinks.Resolve(); // FUN_00417ab4(this+0x59) link->Attach(this); // (**(link+0x1d8+4))(link+0x1d8, this) - // The binary reads *(float*)(link+0x1d0) -- the master heat sink's first OWN - // field (HeatSink now ends exactly at 0x1d0, so this is the master subclass's - // own scale word). field_1d0 was deleted from HeatSink; mirror the raw byte - // read (the master's dynamic subclass isn't known statically here). - Scalar masterScale = *(Scalar *)((char *)link + 0x1d0); + // Gitea #7 decode correction: the binary loads `*(int *)(link+0x1d0)` + // and FILDs it -- `(float10)*(int *)(iVar1 + 0x1d0)` @4af408 -- i.e. + // the master bank's INTEGER HeatSinkCount (AggregateHeatSink @0x1D0), + // not a float scale word. CoolantCapacity = 0.05 x heatSinkCount x + // streamed capacity (MadCat: 0.05 x 14 x 3800 = 2660). The old raw + // FLOAT reinterpret read the int bits as ~1e-44 -> capacity ~0 (latent + // while the master-gate bug kept this branch dead). The master's + // dynamic subclass isn't known statically here, so the read stays a + // guarded raw offset -- but of the documented int slot. + Scalar masterScale = (Scalar)*(int *)((char *)link + 0x1d0); // FILD heatSinkCount thermalCapacity = CoolantCapacityScale * masterScale * thermalCapacity; // scale by master (coolantCapacity reuses thermalCapacity) coolantLevel = thermalCapacity; } @@ -587,30 +602,80 @@ Logical Reservoir::TestInstance() const // @4af564 -> IsDerivedFrom 0x50e650 Logical Reservoir::TestClass(Mech &) { return True; } // -// @4aee70 -- "InjectCoolant" message / per-state update. When the watched -// segment has positive count and the reservoir is active above 0 temperature, -// raise the inject alarm and zero the inject accumulator; otherwise clear it. +// @4aee70 -- the "InjectCoolant" (COOLANT FLUSH) message handler, registered +// under id 4 in the Reservoir handler table @0x50e680 (one entry, name string +// @0x50eba5 "InjectCoolant") [T1]. Gitea #7. // -Logical - Reservoir::HandleMessage(int message) // FUN_004aee70 +// Decode (task-#7-flush correction of the old best-effort body): the binary +// bVar1 = FUN_004ac9c8(this); // the novice lockout gate (the +// // same gate MoveValve uses; reads +// // owning BTPlayer @mech+0x190, +// // +0x274 experience level == 0) +// if (!bVar1) { +// if (*(int*)(msg+0xc) < 1) // RELEASE -> stop the flush +// SetLevel(alarm@0x1d0, 0); +// else { // PRESS (or hold) +// if (level@0x1e4 != 1 && _DAT_004aeedc(=0.0f) < *(float*)(this+0x12C)) +// SetLevel(alarm@0x1d0, 1); // ... gate is +0x12C = COOLANT- +// *(this+0x228) = 0; // LEVEL (the old recon misread it +// } // as currentTemperature@0x114: +// *(ushort*)(this+0x18) |= 1; // can't flush an EMPTY tank) +// } +// While the alarm is 1 the CoolantSimulation Performance runs InjectCoolant +// every frame (the drain the coolant gauge reads); the audio watcher bound to +// ReservoirState fires the CoolantDump layers on the 0/1 transitions, and the +// view side spawns the condensation cloud (flush.pfx, psfx 19 -- BTDPL.INI +// "Coolant flush") on the 0->1 edge, matching the binary's mode-1 +// BTTracerEffectRenderable (built for classID 0xBC0 on the "ReservoirState" +// attr, part_014.c:5439; its tick @part_007.c:8780 starts the pfx exactly +// when the watched state CHANGES to 1). +// +void + Reservoir::InjectCoolantMessageHandler(ReceiverDataMessageOf *message) // FUN_004aee70 { - if (!HeatableSubsystem::HandleMessage(message)) // FUN_004ac9c8 + extern int BTPlayerRoleLocksAdvanced(void *owner_mech); // btplayer.cpp (FUN_004ac9c8) + if (BTPlayerRoleLocksAdvanced(owner)) + return; + if (message->dataContents < 1) // *(int*)(msg+0xc) < 1 = release { - if (message /*->count*/ < 1) - { - reservoirAlarm.SetLevel(0); // this+0x1d0 - } - else - { - if (reservoirAlarm.GetLevel() != 1 && currentTemperature > 0.0f) // injectActive == alarm level @0x1e4 - { - reservoirAlarm.SetLevel(1); - } - injectAccumulator = 0; // this+0x228 - } - ForceUpdate(); // this+0x18 + reservoirAlarm.SetLevel(0); // this+0x1d0 -- flush OFF } - return True; + else + { + if (reservoirAlarm.GetLevel() != 1 // injectActive == alarm level @0x1e4 + && coolantLevel > 0.0f) // _DAT_004aeedc = 0.0f; this+0x12C + { + reservoirAlarm.SetLevel(1); // flush ON + // The binary's flush-cloud renderable spawns on this 0->1 edge + // (see the header comment); the port's effect renderables are + // consolidated in the psfx layer, so the spawn hook rides the + // same transition here. + extern void BTSpawnFlushCloud(void *owner_mech); // mech4.cpp (psfx 19) + BTSpawnFlushCloud(owner); + } + injectAccumulator = 0; // this+0x228 + } + ForceUpdate(); // this+0x18 |= 1 +} + +// +// Gitea #7 -- the Reservoir handler registration (table @0x50e680: exactly +// one entry {4, "InjectCoolant", @4aee70}). Chained onto the engine Receiver +// root set exactly like the Condenser's (task #13); function-local statics +// per the static-init-order rule (reconstruction-gotchas #9). +// +Receiver::MessageHandlerSet& + Reservoir::GetMessageHandlers() +{ + static const Receiver::HandlerEntry entries[]= + { + MESSAGE_ENTRY(Reservoir, InjectCoolant) // id 4 @4aee70 + }; + static Receiver::MessageHandlerSet messageHandlers( + ELEMENTS(entries), entries, + Receiver::GetMessageHandlers() + ); + return messageHandlers; } // @@ -623,6 +688,22 @@ void if (reservoirAlarm.GetLevel() == 1) // injectActive == alarm level @0x1e4 { injectAccumulator += time_slice; // this+0x228 + + // DIAG (BT_FLUSH_LOG, Gitea #7): the reservoir drain the coolant + // gauge reads (vertBar C binds Reservoir/CoolantMass = coolantLevel). + if (getenv("BT_FLUSH_LOG")) + { + static Scalar s_flAcc = 1.0e9f; // log the first active frame too + s_flAcc += time_slice; + if (s_flAcc >= 0.5f) + { + s_flAcc = 0.0f; + DEBUG_STREAM << "[flush] " << GetName() + << " level=" << coolantLevel << "/" << thermalCapacity + << " squirtMass=" << coolantSquirtMass + << " held=" << injectAccumulator << "s\n" << std::flush; + } + } InjectCoolant(time_slice); // FUN_004aefa4 } } @@ -648,28 +729,135 @@ Scalar } // -// @4aefa4 (1019 bytes) -- distribute the reservoir charge across the linked -// heat-sink network. Iterates three engine collections off the owner -// (+0x7ac/+0x7bc/+0x7cc) plus the local sink, builds a work list, and for each -// member moves a clamped, time-scaled "squirt" of coolant -- bounded by the -// per-sink remaining capacity and by the reservoir's own coolantLevel -- while -// tracking the per-sink coolant-flow delta (+0x1c8). Bails out as soon as the -// reservoir's currentTemperature drops to ~0 (|temp| <= 1e-4). +// @4aefa4 (1019 bytes) -- the COOLANT FLUSH proper (Gitea #7; was a stub that +// did NOTHING -- the reported "gauge doesn't show a drop"). Distribute the +// reservoir charge across the mech's heat-sink network: build a work list +// from the owner's three capability chains -- condensers @+0x7cc (GUID +// 0x50e4fc), weapons @+0x7bc (0x511830 = MechWeapon), heatables @+0x7ac +// (0x51155c) -- plus the linked master sink (linkedSinks.Resolve @+0x164); +// then, for each member with a nonzero coolant-flow share (+0x15C -- the +// valve set% bias, manual p24 "with a bias based on your set% levels"), move +// a squirt of coolantSquirtMass x flowScale x dt coolant out of the +// reservoir into the sink (clamped to what the reservoir still holds, and +// the sink to its capacity), and credit the sink's pendingHeat (+0x1C8) with +// the NEGATIVE heat delta carried by the fresh coolant -- capped at +// sink->thermalMass x reservoir->startingTemperature (+0x154 x +0x13C, the +// coolant enters at the reservoir's base temperature) and clamped to only +// ever COOL. Bails out as soon as the reservoir runs dry +// (|coolantLevel@0x12C| <= 1e-4 -- the old stub's "currentTemperature" +// reading of +0x12C was a misdecode; +300 decimal == 0x12C == coolantLevel). +// Constants: _DAT_004af3a0 = 1e-4f, _DAT_004af3a4 = _DAT_004af3a8 = 0.0f +// (byte-verified @4af3a0). // -// (Uses the engine collection-iterator helpers @4af9cf..@4afb96 as locals -- -// see notes at the foot of this file. Body reproduced structurally; the -// per-sink arithmetic mirrors HeatSink::ComputeHeatFlow.) +// PORT NOTE (chain emulation, same pattern as BTRecomputeCondenserValves): +// the factory's capability-chain fill loops ride the SubProxy stub in the +// port (loops 2/4 add nothing), so the work list is rebuilt here by walking +// the populated roster with the SAME membership tests, in the binary's +// order: condensers, weapons, HeatSink-derived heatables, the linked master +// sink. Condensers and weapons therefore appear TWICE (they are heatable- +// derived too), the bank twice (heatable + linked sink) -- exactly the +// binary's duplicate-visit weighting. ONE guarded deviation [T2]: the +// binary's heatable chain also contains the WATCHER branch (HeatWatcher +// chains the HeatableSubsystem derivation), whose bytes at +0x15C/+0x12C are +// interior alarm state in OUR layout (writing them = the databinding trap); +// real coolant carriers are all HeatSink-derived, so the walk filters to +// HeatSink -- a watcher's incidental +0x15C in the 1995 layout is alarm +// interior there too, so the faithful economics are identical. // void Reservoir::InjectCoolant(Scalar time_slice) // FUN_004aefa4 { - if (fabsf(currentTemperature) <= 1.0e-4f) // _DAT_004af3a0 + if (fabsf(coolantLevel) <= 1.0e-4f) // _DAT_004af3a0; +0x12C (tank empty) { return; } - // ... iterate owner collections (+0x7ac/+0x7bc/+0x7cc) and self, - // squirt clamped coolant to each linked sink, update +0x1c8 ... - // (full per-sink math recovered in heatfamily_gap.c @4aefa4) + + extern int BTWeaponIsRearFiring(::Subsystem *sub); // mechweap.cpp (-1 = not a weapon) + + enum { kMaxWork = 96 }; // 20-subsystem roster x duplicate passes + HeatSink *work[kMaxWork]; + int workCount = 0; + Derivation &condenserClass = *Condenser::GetClassDerivations(); // 0x50e4fc + Derivation &heatSinkClass = *HeatSink::GetClassDerivations(); + Entity *own = (Entity *)owner; // roster access (Entity-complete TU) + int count = own->GetSubsystemCount(); + + for (int i = 2; i < count && workCount < kMaxWork; ++i) // chain @+0x7cc + { + ::Subsystem *s = own->GetSubsystem(i); + if (s != 0 && s->IsDerivedFrom(condenserClass)) + work[workCount++] = (HeatSink *)s; + } + for (int i = 2; i < count && workCount < kMaxWork; ++i) // chain @+0x7bc + { + ::Subsystem *s = own->GetSubsystem(i); + if (s != 0 && BTWeaponIsRearFiring(s) >= 0) // MechWeapon-derived + work[workCount++] = (HeatSink *)s; // (weapons are HeatSinks) + } + for (int i = 2; i < count && workCount < kMaxWork; ++i) // chain @+0x7ac + { + ::Subsystem *s = own->GetSubsystem(i); + if (s != 0 && s->IsDerivedFrom(heatSinkClass)) // see PORT NOTE + work[workCount++] = (HeatSink *)s; + } + { + HeatSink *link = (HeatSink *)linkedSinks.Resolve(); // FUN_00417ab4(this+0x164) + if (link != 0 && workCount < kMaxWork) + work[workCount++] = link; + } + + for (int w = 0; w < workCount; ++w) + { + if (fabsf(coolantLevel) <= 1.0e-4f) // ran dry mid-pass -> stop + { + return; + } + HeatSink *sink = work[w]; + if (sink->coolantFlowScale == 0.0f) // +0x15C == _DAT_004af3a4 + { + continue; + } + + // squirt = -(squirtMass x flowScale x dt), clamped to [-coolantLevel, 0] + Scalar move = -coolantSquirtMass // this+0x224 + * sink->coolantFlowScale // sink+0x15C + * time_slice; + Scalar lo = -coolantLevel; // local_14 + if (move < lo) move = lo; // can't move more than the tank holds + if (move > 0.0f) move = 0.0f; // squirts only ever LEAVE the tank + + // the heat delta riding the moved mass (computed BEFORE the level + // updates, as in the binary): + Scalar den = (fabsf(sink->coolantLevel) > 1.0e-4f) + ? sink->coolantLevel // sink+0x12C + : sink->thermalCapacity; // sink+0x128 (empty-sink fallback) + Scalar fracSink = fabsf(move) / den; // local_20 + Scalar fracRes = fabsf(move) / coolantLevel; // local_1c + Scalar heatDelta = + sink->heatEnergy * fracSink // sink+0x158 + - heatEnergy * fracRes; // this+0x158 + + coolantLevel += move; // the reservoir DRAINS (move <= 0) + sink->coolantLevel -= move; // the sink gains + if (sink->coolantLevel >= 0.0f) // clamp to [0, capacity] + { + if (sink->coolantLevel > sink->thermalCapacity) + sink->coolantLevel = sink->thermalCapacity; + } + else + { + sink->coolantLevel = 0.0f; // _DAT_004af3a8 + } + + Scalar cap = sink->thermalMass // sink+0x154 + * startingTemperature; // this+0x13C (reservoir base temp) + if (heatDelta > cap) + heatDelta = cap; + Scalar chill = -heatDelta; + if (chill > 0.0f) // _DAT_004af3a4 < local_2c -> clamp + chill = 0.0f; // the flush only ever COOLS + sink->pendingHeat += chill; // sink+0x1C8 + } } // diff --git a/game/reconstructed/heatfamily_reslice.hpp b/game/reconstructed/heatfamily_reslice.hpp index af79695..dbda458 100644 --- a/game/reconstructed/heatfamily_reslice.hpp +++ b/game/reconstructed/heatfamily_reslice.hpp @@ -222,6 +222,15 @@ { public: static Derivation *GetClassDerivations(); // name "Reservoir" + // Gitea #7: the Reservoir handler table @0x50e680 -- exactly ONE entry, + // {4, "InjectCoolant", @4aee70} (PE bytes: 04000000 a5eb5000 70ee4a00; + // the name string @0x50eba5 = "InjectCoolant"). Same per-receiver id + // space as the Condenser's: id 4 to a condenser = MoveValve, to the + // reservoir = InjectCoolant (the COOLANT FLUSH -- manual p24: "punch + // (or hold down) the coolant button to flush fresh coolant through + // your 'Mech's loops"). Function-local statics per the static-init- + // order rule. [T1] + static Receiver::MessageHandlerSet& GetMessageHandlers(); static SharedData DefaultData; // Attribute Support -- audio binds an AudioStateWatcher to ReservoirState; @@ -267,7 +276,12 @@ // @4af3b0 -- slot-14 DrawCoolant override: hand out up to coolantLevel. Scalar DrawCoolant(Scalar requested); - Logical HandleMessage(int message); // @4aee70 ("InjectCoolant" msg) + // @4aee70 -- the "InjectCoolant" (COOLANT FLUSH) message handler, id 4 + // (table @0x50e680 [T1]). Press (data >= 1) starts the flush while + // coolantLevel > 0; release (data < 1) stops it. + enum { InjectCoolantMessageID = 4 }; + void InjectCoolantMessageHandler( + ReceiverDataMessageOf *message); void PrintState(); // @4aeef8 (ReservoirState) void GetState(/*...*/); // @4aef40 (slot 7 state query) diff --git a/game/reconstructed/hud.cpp b/game/reconstructed/hud.cpp index 5ab57bb..d078894 100644 --- a/game/reconstructed/hud.cpp +++ b/game/reconstructed/hud.cpp @@ -126,17 +126,21 @@ HUD::HUD( Check(owner); Check_Pointer(subsystem_resource); - flickerRate = 0.0f; // @0x1D8 (overwritten below from resource) - torsoLimitRight = 0.0f; // @0x1DC - torsoLimitLeft = 0.0f; // @0x1E0 - torsoSpeed = 0.0f; // @0x1E4 - rangeToTarget = 0.0f; // @0x1E8 - targetRange = 0.0f; // @0x1EC - lockFlag = True; // @0x1F4 = 1 - visible = False; // @0x1F8 - threatVector = HudZeroVector; // @0x1FC - compassHeading = HudZeroVector; // @0x208 - hotBoxScalar = 0.0f; // @0x214 + // (member names re-based 2026-07-19 per the re-dumped attr table @005110b8; + // the ctor @004b7f94 zeroes [0x76..0x7b] then sets [0x7d]=1 / [0x7e]=0 -- + // note [0x7c]=@0x1F0 is left uninitialized in the binary) + flickerRate = 0.0f; // @0x1D8 (overwritten below from resource) + rotationOfTorsoHorizontal = 0.0f; // @0x1DC + torsoLimitRight = 0.0f; // @0x1E0 + torsoLimitLeft = 0.0f; // @0x1E4 + torsoSpeed = 0.0f; // @0x1E8 + rangeToTarget = 0.0f; // @0x1EC + scratch1F0 = 0; // @0x1F0 (binary leaves it uninitialized; zeroed here) + visible = True; // @0x1F4 = 1 ("Visible" starts shown) + lockFlag = False; // @0x1F8 = 0 ("Lock" starts un-locked) + hotBoxVector = HudZeroVector; // @0x1FC + threatVector = HudZeroVector; // @0x208 + compassHeading = 0.0f; // @0x214 blinkTimer = 0.0f; // @0x218 flickerTimer = 0.0f; // @0x21C blinkState = True; // @0x224 = 1 @@ -162,12 +166,14 @@ HUD::HUD( // owner+0x438 is the torso-orientation source -- a Mech RAW OFFSET that does not // hold a valid pointer in the reconstructed Mech layout (and isn't linked yet at // subsystem-construction time), so dereferencing it crashes. Default the - // torso/range readouts to 0; HudSimulation refreshes them per-frame once the link + // torso/twist readouts to 0; HudSimulation refreshes them per-frame once the link // is live. TODO: wire a real Mech torso-orientation-source accessor. + // (binary snapshot @004b7f94: [0x77]@0x1DC <- linked+0x1D8 twist, [0x78]@0x1E0 + // <- +0x1DC limitR, [0x79]@0x1E4 <- +0x1E0 limitL, [0x7a]@0x1E8 <- +0x1E8 speed) linkedEntity = 0; // @0x234 + rotationOfTorsoHorizontal = 0.0f; torsoLimitRight = 0.0f; torsoLimitLeft = 0.0f; - rangeToTarget = 0.0f; torsoSpeed = 0.0f; Check_Fpu(); @@ -203,14 +209,17 @@ Logical } //########################################################################### -// SetCompassHeading -- @004b7810 +// SetThreatVector -- @004b7810 // -// Copies a Point3D into compassHeading (@0x208). +// Copies a Point3D into threatVector (@0x208). Renamed 2026-07-19 (Gitea +// #10): the re-dumped attr table @005110b8 binds "ThreatVector" (attr 12) to +// @0x208 -- the old SetCompassHeading label was the one-slot transcription +// shift (the compass is the Scalar @0x214). No port caller. // void - HUD::SetCompassHeading(const Point3D &heading) + HUD::SetThreatVector(const Point3D &threat) { - compassHeading = heading; // FUN_00408440(this+0x208, &heading) + threatVector = threat; // FUN_00408440(this+0x208, &threat) } //########################################################################### @@ -299,11 +308,12 @@ Logical // 0 -> set HUD flag bit 0x1 on the graphic, clear bit 0x2 // 3 -> set bit 0x2, clear bit 0x1 (and gate on mech +0x410) // writes the resulting "active" flag to *(mech +0x374). -// 5. Target line-of-sight: if a target entity (mech +0x388) exists and is -// not over the heat limits, computes targetRange (@0x1EC) as the distance -// between the HUD anchor (mech +0x37C) and the target (+0x100); sets -// visible (@0x1F8); otherwise blanks. A default range 1200.0 is used -// when there is no target. +// 5. Target lock + range: if a target entity (mech +0x388) exists and the +// own/target zone-damage limits pass (:5619-5634, the Lock rule), sets +// lockFlag (@0x1F8, "Lock"); rangeToTarget (@0x1EC, "RangeToTarget") is +// the distance between the HUD anchor (mech +0x37C) and the target +// (+0x100), sliding at 500 m/s (:5652-5670), default 1200.0 with no +// target; the compass Scalar @0x214 = yaw euler[0] + torso twist (:5676). // 6. Torso-horizon slew: horizontalTorsoOffset (@0x294) is moved toward the // commanded torso heading at up to MaxTorsoSlew (500/sec), clamped to // +/- horizontalLimit (@0x29C), then written to the graphic at diff --git a/game/reconstructed/hud.hpp b/game/reconstructed/hud.hpp index f586390..20d9ec7 100644 --- a/game/reconstructed/hud.hpp +++ b/game/reconstructed/hud.hpp @@ -96,18 +96,30 @@ // // AttributePointers IndexEntry table @005110c0 (each record binds a name // to an attribute id and an object offset, the offset encoded as - // byte_offset|1 -- the same scheme used by sensor.hpp). Recovered: - // id 4 "RotationOfTorsoHorizontal" -> @0x1D8 - // id 5 "HorizontalTorsoLimitRight" -> @0x1DC - // id 6 "HorizontalTorsoLimitLeft" -> @0x1E0 - // id 7 "SpeedOfTorsoHorizontal" -> @0x1E4 - // id 8 "RangeToTarget" -> @0x1E8 - // id 9 "Visible" -> @0x1EC - // id 10 "Lock" -> @0x1F4 - // id 11 "HotBoxVector" -> @0x1F8 - // id 12 "ThreatVector" -> @0x1FC (Vec3) - // id 13 "CompassHeading" -> @0x208 (Vec3) - // (FlickerRate name ptr @005110bc heads the table as the category label.) + // byte_offset|1 -- the same scheme used by sensor.hpp). + // + // RE-DUMPED 2026-07-19 (Gitea #10 audit; section_dump.txt @5110b8, 16-byte + // records {id, namePtr, offset|1, 0}, terminator @511160). The table + // actually STARTS at @005110b8 with a FULL id-3 FlickerRate record (the old + // transcription had misread it as a "category label" and shifted every + // subsequent offset down one slot): + // id 3 "FlickerRate" -> @0x1D8 + // id 4 "RotationOfTorsoHorizontal" -> @0x1DC + // id 5 "HorizontalTorsoLimitRight" -> @0x1E0 + // id 6 "HorizontalTorsoLimitLeft" -> @0x1E4 + // id 7 "SpeedOfTorsoHorizontal" -> @0x1E8 + // id 8 "RangeToTarget" -> @0x1EC + // id 9 "Visible" -> @0x1F4 (init 1; @0x1F0 is unbound) + // id 10 "Lock" -> @0x1F8 (init 0) + // id 11 "HotBoxVector" -> @0x1FC (Point3D) + // id 12 "ThreatVector" -> @0x208 (Point3D) + // id 13 "CompassHeading" -> @0x214 (Scalar: yaw + torso twist) + // CROSS-CHECKED against the ctor @004b7f94 (inits @0x1F4=1 Visible-on / + // @0x1F8=0 Lock-off; @0x1F0 uninitialized) and HudSimulation @004b7830 + // (per-frame @0x1DC <- linkedTorso+0x1D8 = Torso::currentTwist; @0x1EC + // range default 1200.0 + the 500 m/s display slide; the Lock rule writes + // @0x1F8 at :5622/:5633; @0x214 = EulerAngles(mech)[0] + @0x1DC at :5676; + // the hotbox Point3D copy @0x1FC at :5680). All consistent. // // CONFIRMED (task #37): the recovered reticle Execute @004cdcf0 binds and // USES ids 4/5/6/8/0xA/0xB/0xC/0xD exactly per these names -- @@ -115,8 +127,7 @@ // HorizontalTorsoLimit full-scales, RangeToTarget the range caret, Lock // the spinning lock ring, HotBoxVector the target box (eye-space vec3), // ThreatVector the compass threat trail, CompassHeading the rose - // rotation. (The earlier note about @0x1D8/@0x1EC/@0x1F8 usage remains - // for the ctor-side seeds only.) + // rotation. // public: enum { @@ -142,20 +153,25 @@ // Local data. Offsets are byte offsets into the shipped object; names are // best-effort from the attribute pool + ctor/HudSimulation usage. // + // Member names re-based 2026-07-19 (Gitea #10) against the RE-DUMPED attr + // table @005110b8 -- the old names were shifted one slot (see the table + // comment above). protected: - Scalar flickerRate; // @0x1D8 resource +0xF4 (flicker / blink period threshold) - // Torso / target read-outs sampled each construction from the linked - // targeting entity (owner Mech +0x438 -> this->linkedEntity @0x234): - Scalar torsoLimitRight; // @0x1DC <- linkedEntity +0x1D8 - Scalar torsoLimitLeft; // @0x1E0 <- linkedEntity +0x1DC - Scalar torsoSpeed; // @0x1E4 <- linkedEntity +0x1E0 - Scalar rangeToTarget; // @0x1E8 <- linkedEntity +0x1E8 (and recomputed in sim @0x1EC) - Scalar targetRange; // @0x1EC computed line-of-sight distance to the target - int lockFlag; // @0x1F4 init 1 - int visible; // @0x1F8 set 0/1 from heat + line-of-sight each frame - Point3D threatVector; // @0x1FC init (0,0,0) ("ThreatVector") - Point3D compassHeading; // @0x208 init (0,0,0) ("CompassHeading"); see SetCompassHeading - Scalar hotBoxScalar; // @0x214 init 0 + Scalar flickerRate; // @0x1D8 resource +0xF4 ("FlickerRate", attr id 3) + // Torso / target read-outs sampled at construction AND per-frame in + // HudSimulation from the linked Torso subsystem (owner Mech +0x438 -> + // this->linkedEntity @0x234): + Scalar rotationOfTorsoHorizontal; // @0x1DC <- linkedEntity +0x1D8 (Torso::currentTwist; attr 4) + Scalar torsoLimitRight; // @0x1E0 <- linkedEntity +0x1DC (attr 5) + Scalar torsoLimitLeft; // @0x1E4 <- linkedEntity +0x1E0 (attr 6) + Scalar torsoSpeed; // @0x1E8 <- linkedEntity +0x1E8 (attr 7 "SpeedOfTorsoHorizontal") + Scalar rangeToTarget; // @0x1EC computed target distance; 1200.0 default; 500 m/s display slide (attr 8) + int scratch1F0; // @0x1F0 unbound / not ctor-initialized in the binary + int visible; // @0x1F4 init 1 ("Visible", attr 9; heat/page gating) + int lockFlag; // @0x1F8 init 0 ("Lock", attr 10; the :5619-5634 lock rule) + Point3D hotBoxVector; // @0x1FC init (0,0,0) ("HotBoxVector", attr 11) + Point3D threatVector; // @0x208 init (0,0,0) ("ThreatVector", attr 12); see SetThreatVector + Scalar compassHeading; // @0x214 init 0 ("CompassHeading", attr 13; yaw euler[0] + torso twist @:5676) Scalar blinkTimer; // @0x218 accumulates time, reset against flickerRate Scalar flickerTimer; // @0x21C int blinkState; // @0x224 init 1 (visible phase of the blink) @@ -193,8 +209,13 @@ void HudSimulation(Scalar time_slice); // @004b7830 (Performance, PTR @00511170) + // @004b7810 writes the Point3D @0x208 -- which the RE-DUMPED attr table + // (2026-07-19) proves is the THREAT vector, not the compass (the compass + // is the Scalar @0x214). Renamed from the old mislabel SetCompassHeading; + // no port caller exists (the port threat-trail feed goes through the + // reticle globals, btl4vid.cpp). void - SetCompassHeading(const Point3D &heading); // @004b7810 (writes compassHeading @0x208) + SetThreatVector(const Point3D &threat); // @004b7810 (writes threatVector @0x208) //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Subsystem virtual overrides (vtable @00511510) diff --git a/game/reconstructed/mech.cpp b/game/reconstructed/mech.cpp index 9cf6a76..0722b05 100644 --- a/game/reconstructed/mech.cpp +++ b/game/reconstructed/mech.cpp @@ -563,7 +563,8 @@ Logical // Logical Mech::WorldStructurePick(const Point3D &start, const Vector3D &dir, - Scalar max_range, Point3D *hit_world) + Scalar max_range, Point3D *hit_world, + Entity **hit_owner) { Scalar dlen = (Scalar)Sqrt(dir.x*dir.x + dir.y*dir.y + dir.z*dir.z); if (dlen < 1e-6f) @@ -574,6 +575,20 @@ Logical return False; if (hit_world != 0) ray.FindEnd(hit_world); // clipped length -> entry point + if (hit_owner != 0) + { + // The struck solid's owning ENTITY (issue #3, item b). Every static solid + // carries its owner Simulation in the tag pointer (BoxedSolid ctor; + // TERRAIN.cpp:107/246 pass the Terrain/CulturalIcon `this`, DOOR.cpp:395 + // the Door) and Entity derives from Simulation at offset 0, so this is the + // struck map entity. The binary designates/damages the struck ENTITY: + // the weapon dispatch @004b9728 (part_013.c:6765) gates ONLY "target not + // derived from Mech@0x50bdb4 OR aimed zone set" -- a non-Mech target is + // damaged unconditionally -- and the missile contact @004be078 dispatches + // a zone=-1 TakeDamageMessage at the struck solid's owner with no class + // test at all. + *hit_owner = (Entity *)solid->GetOwningSimulation(); + } return True; } diff --git a/game/reconstructed/mech.hpp b/game/reconstructed/mech.hpp index 7e08268..5923d68 100644 --- a/game/reconstructed/mech.hpp +++ b/game/reconstructed/mech.hpp @@ -699,9 +699,16 @@ public: // tree (garages/walls/props -- the geometry that blocks the walk) along the // boresight and return the entry point of the closest structure hit. The // authentic non-mech world pick (Mover::FindStaticSolidHitBy, the static-world - // half of the engine's FindBoxedSolidHitBy). Defined in mech.cpp. + // half of the engine's FindBoxedSolidHitBy). hit_owner (optional) receives 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, DOOR.cpp:395): the binary targets/damages the struck + // ENTITY, not a sentinel (weapon dispatch @004b9728 sends to any non-Mech + // target; missile contact @004be078 dispatches at the struck solid's owner). + // Defined in mech.cpp. Logical WorldStructurePick(const Point3D &start, const Vector3D &dir, - Scalar max_range, Point3D *hit_world); + Scalar max_range, Point3D *hit_world, + Entity **hit_owner = 0); // Per-weapon beam render walk (task #33; extracted task #51 so it runs for // EVERY mech -- player, dummy masters, and MP replicants whose emitters diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index f003700..ea5dad5 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -652,6 +652,8 @@ static int gBTPPCKey = 0; static int gBTMissileKey = 0; static int gBTPinkyKey = 0; // key '4' = the pod's 4th fire button (Pinky 0x45) int gBTModeCycle = 0; // 'M' edge: cycle the control mode (mapper consumes) +int gBTDisplayCycle = 0; // 'N' edge: cycle the secondary schematic (Gitea #6, mapper consumes) +int gBTPresetCycle[3] = {0,0,0}; // J/K/L edges: cycle an upper-MFD preset page (Gitea #9, L4 mapper consumes) int gBTLookBehind = 0; // 'V' held: the pod's rear-view button (task #68) float gBTTwistAxis = 0.0f; // Q/E torso-twist deflection (assisted-mode stick X) float gBTElevAxis = 0.0f; // R/F torso-elevation (pitch aim, stick Y) @@ -661,6 +663,7 @@ static int gBTConfigKey = 0; // task #6: HOLD 'G' = the weapon-con static int gBTGenSelKey = 0; // task #12: F5..F8 = SelectGeneratorA..D, F9 = mode toggle // (0 idle; else the message id 4..8) static int gBTValveKey = 0; // task #13: 'C' = MoveValve on the selected condenser +static int gBTFlushKey = 0; // Gitea #7: 'H' HELD = InjectCoolant (the coolant flush) // Damage: each shot dispatches a REAL Entity::TakeDamageMessage to the target. Now // that the damage zones are constructed (mech.cpp Pass-3 zone build), the engine base @@ -899,6 +902,40 @@ void << " at(" << mx << "," << my << "," << mz << ")\n" << std::flush; } +//########################################################################### +// COOLANT-FLUSH CONDENSATION CLOUD (Gitea #7) -- the flush visual. +// +// BTDPL.INI psfx 19 = FLUSH.PFX, commented "Coolant flush" (both day and +// night tables): a bluish (0.7/0.9/1.6 tint) firesmoke burst, ~3.5 s window. +// The binary drives it with the mode-1 "coolant/reservoir" effect renderable +// (FUN_00456a68, alloc 0x138), built for every classID-0xBC0 subsystem on +// its "ReservoirState" attribute (BTL4VID.CPP build loop, part_014.c:5439; +// missing attr = Verify "Reservoir did not have a ReservoirState...") and +// DCS-parented to the mech; its per-frame tick (part_007.c:8780) starts the +// effect exactly when the watched state CHANGES to 1. Manual p24: "other +// pilots will see steam rise from your 'Mech on the battlefield". +// +// The port consolidates the BT effect renderables in the psfx layer, so this +// helper is called on the same 0->1 alarm edge (Reservoir:: +// InjectCoolantMessageHandler) and rides the mech via the attached-emitter +// follower (seg -1 = the torso-height entity fallback in +// BTResolveSegmentWorld -- the cloud rises FROM the mech and trails it). +//########################################################################### +void + BTSpawnFlushCloud(void *ownerMech) +{ + if (ownerMech == 0) + return; + float pos[3], rows[9]; + if (!BTResolveSegmentWorld(ownerMech, -1, pos, rows)) + return; + extern void BTStartPfxAttached(int, void *, int, float, float, float, const float *); + BTStartPfxAttached(19, ownerMech, -1, pos[0], pos[1], pos[2], rows); + if (getenv("BT_FLUSH_LOG")) + DEBUG_STREAM << "[flush] condensation cloud (psfx 19 FLUSH.PFX) at(" + << pos[0] << "," << pos[1] << "," << pos[2] << ")\n" << std::flush; +} + //########################################################################### // PER-ROUND DETONATION (the binary's Missile::MoveAndCollide @004bef78: every // round spawns ITS OWN ExplosionModelFile at its impact point, resource @@ -1461,6 +1498,32 @@ static void } } } + else if (tgt != 0 && !BTIsRegisteredMech(tgt) && p.damage > 0.0f + && tgt->damageZoneCount > 0) + { + // NON-MECH victim with damage zones (a CulturalIcon truck/prop + // designated by the world pick -- issue #3, item b). The binary + // missile dispatches a zone=-1 TakeDamageMessage DIRECTLY at + // whatever entity's solid the round struck, with NO class test + // (Missile::MoveAndCollide contact branch -> @004be078: msg id + // 100, zone 0xffffffff, invalid-zone flag 1 -> struck->Dispatch). + // The icon's own handler maps -1 -> 0 and runs the death + // transition (-> the FULL 'trkdead' explosion, res 32). + Damage dmg; + dmg.damageType = Damage::ExplosiveDamageType; + dmg.damageAmount = p.damage; + dmg.burstCount = 1; + dmg.impactPoint = p.pos; + Entity::TakeDamageMessage take_damage( + Entity::TakeDamageMessageID, sizeof(Entity::TakeDamageMessage), + (p.shooter != 0) ? p.shooter->GetEntityID() : EntityID::Null, + -1 /*unaimed; icon handler maps -1 -> zone 0*/, dmg); + tgt->Dispatch(&take_damage); + if (getenv("BT_PROJ_LOG") || getenv("BT_CULT_LOG")) + DEBUG_STREAM << "[projectile] IMPACT (non-mech entity class=" + << (int)tgt->GetClassID() << ") damage=" << p.damage + << " (@004be078 direct dispatch)\n" << std::flush; + } p.active = 0; } } @@ -2595,6 +2658,10 @@ void gBTGenSelKey = gBTInput.genSel; // task #13: the Valve action cycles the coolant valve. gBTValveKey = gBTInput.valve; + // Gitea #7: the Flush action HELD = the coolant MFD's + // top-right button (manual p24) -- InjectCoolant while + // down, stop on release. + gBTFlushKey = gBTInput.flush; // TORSO CONTROLS (2026-07-13): 'M' cycles the control mode // (Basic -> Standard -> Veteran -- the pod console button, // CycleControlModeMessageHandler); Q/E deflect the torso @@ -2605,6 +2672,28 @@ void const int mNow = gBTInput.modeCycle; if (mNow && !sPrevM) gBTModeCycle = 1; // edge -> one cycle sPrevM = mNow; + // Gitea #6: 'N' cycles the secondary screen's schematic + // (Damage -> Critical -> Heat) -- the pod's status-info- + // center button (manual p13), CycleDisplayModeNow. + { + static int sPrevN = 0; + const int nNow = gBTInput.displayCycle; + if (nNow && !sPrevN) gBTDisplayCycle = 1; + sPrevN = nNow; + } + // Gitea #9: J/K/L cycle an upper-MFD preset page + // (Quad -> populated Eng pages -> Quad) -- the desktop + // sender for the pod's per-MFD page-button banks + // (SetPresetMode, consumed by the L4 mapper). + { + static int sPrevMfd[3] = {0,0,0}; + for (int mg = 0; mg < 3; ++mg) + { + const int gNow = gBTInput.mfdCycle[mg]; + if (gNow && !sPrevMfd[mg]) gBTPresetCycle[mg] = 1; + sPrevMfd[mg] = gNow; + } + } // (task #68) LookBehind action HELD = the pod's rear-view // button (releases back to the forward view). Rear-mounted // weapons (blackhawk/owens back racks) fire only in it. @@ -2690,6 +2779,18 @@ void sElev = 0.0f; // X recenters pitch too } gBTElevAxis = sElev; + // DIAG (BT_FORCE_ELEV=<-1..1>): pin the elevation + // axis for headless pitch verification (no focus). + { + static float s_fe = -2.0f; + if (s_fe < -1.5f) + { + const char *fe = getenv("BT_FORCE_ELEV"); + s_fe = fe ? (float)atof(fe) : -3.0f; + } + if (s_fe >= -1.0f) + gBTElevAxis = s_fe; + } if (getenv("BT_INPUT_LOG")) { static float s_eacc = 0.0f; s_eacc += dt; @@ -4607,10 +4708,11 @@ void Point3D structPoint; float structDist = 1e30f; bool haveStruct = false; + Entity *structOwner = 0; // the struck solid's owning map entity if (gBTTerrainEntity != 0) { Point3D sp; - if (WorldStructurePick(rayStart, rayDir, 1200.0f, &sp)) + if (WorldStructurePick(rayStart, rayDir, 1200.0f, &sp, &structOwner)) { float dx = sp.x-rs[0], dy = sp.y-rs[1], dz = sp.z-rs[2]; structDist = dx*dx + dy*dy + dz*dz; @@ -4623,6 +4725,8 @@ void DEBUG_STREAM << "[wpick] structure hit at (" << sp.x << "," << sp.y << "," << sp.z << ") dist=" << (float)Sqrt(structDist) + << " owner=" << (void *)structOwner + << " ownerClass=" << (structOwner ? (int)structOwner->GetClassID() : -1) << (hotTarget ? " (mech also under boresight)" : "") << "\n" << std::flush; } @@ -4637,11 +4741,21 @@ void } else if (haveStruct) { - // a world structure is the closest hit (garage/wall) -- or it - // occludes a mech behind it. Designate it as the non-mech world - // pick (the geometry rides in the POINT; the sentinel entity has - // no damage-zone table -> no lock ring, HudSim part_013.c:5620). - pickTarget = gBTTerrainEntity; + // a world structure is the closest hit (garage/wall/prop) -- or + // it occludes a mech behind it. Designate the struck solid's + // OWNING ENTITY (issue #3, item b): the binary's target slot + // 0x388 holds the struck entity itself, and its weapon damage + // dispatch @004b9728 (part_013.c:6765-6794) sends a zone=-1 + // TakeDamageMessage to ANY non-Mech target unconditionally -- + // which is what makes CulturalIcons (trucks/props) weapon- + // killable (-> the FULL 'trkdead' explosion, res 32, not just + // the ram 'stephit' crunch). A plain terrain owner ignores it + // (Entity::TakeDamageMessageHandler returns on zone==-1 with no + // zones, ENTITY.cpp:885); an icon's own handler maps -1 -> 0. + // Fall back to the world sentinel only if the solid carried no + // owner. No lock ring either way (hotTarget stays mech-only; + // HudSim part_013.c:5620). + pickTarget = (structOwner != 0) ? structOwner : gBTTerrainEntity; pickPoint = structPoint; ++gAimGround; } @@ -4744,6 +4858,63 @@ void } } + // DETERMINISTIC FIRE-AT-ICON TEST (BT_FIRE_AT_ICON, issue #3 item b + // verify): designate the NEAREST live (DefaultState) CulturalIcon as the + // weapon target every frame, bypassing the flaky headless aim ray, so + // BT_AUTOFIRE exercises the production weapon-vs-icon dispatch + // (SendDamageMessage -> icon TakeDamage -> res-32 'trkdead' explosion). + // Diag only, off by default. + if (getenv("BT_FIRE_AT_ICON")) + { + extern Entity *gBTCultIcons[256]; + extern int gBTCultIconCount; + // Prefer icons AHEAD (toward the BT_GOTO goal when set) so the kill + // happens in the chase camera's field of view -- pure capture-harness + // convenience, the dispatch path is identical either way. + float fwdX = 0.0f, fwdZ = 0.0f; int haveFwd = 0; + { + const char *gv = getenv("BT_GOTO"); + float gx, gz; + if (gv != 0 && sscanf(gv, "%f %f", &gx, &gz) == 2) + { + fwdX = gx - localOrigin.linearPosition.x; + fwdZ = gz - localOrigin.linearPosition.z; + haveFwd = 1; + } + } + Entity *bestIcon = 0; float bestR2 = 1e30f; + for (int ii = 0; ii < gBTCultIconCount; ++ii) + { + CulturalIcon *ic = (CulturalIcon *)gBTCultIcons[ii]; + if (ic == 0 || ic->GetSimulationState() != CulturalIcon::DefaultState) + continue; // already dying/burning + Point3D ip = ic->localOrigin.linearPosition; + float dx = ip.x - localOrigin.linearPosition.x; + float dz = ip.z - localOrigin.linearPosition.z; + if (haveFwd && (dx*fwdX + dz*fwdZ) <= 0.0f) + continue; // behind the walk direction -> off-camera + float r2 = dx*dx + dz*dz; + if (r2 < bestR2) { bestR2 = r2; bestIcon = (Entity *)ic; } + } + if (bestIcon != 0) + { + Point3D aimPt = ((CulturalIcon *)bestIcon)->localOrigin.linearPosition; + aimPt.y += 3.0f; // aim into the hull, not the base + MECH_TARGET_ENTITY(this) = bestIcon; + MECH_TARGET_SUBIDX(this) = -1; + MECH_TARGET_POS(this) = aimPt; + static float s_fai = 0.0f; s_fai += dt; + if (s_fai >= 1.0f) + { + s_fai = 0.0f; + DEBUG_STREAM << "[fireicon] designating icon entity=" + << bestIcon->GetEntityID() << " r=" << (float)Sqrt(bestR2) + << " (" << aimPt.x << "," << aimPt.y << "," << aimPt.z << ")\n" + << std::flush; + } + } + } + // HUD feeds: the range caret + the hotbox (world point + state) + the // recovered-Execute instruments (compass, twist tape, group mask). // The range caret tracks the PICK (authentic: :5639 computes it from @@ -4837,6 +5008,34 @@ void extern float gBTEyeTwist; gBTEyeTwist = gBTHudTwist; } + + // TORSO-ELEVATION AIM -> EYE PITCH (issue #3 follow-up; user + + // tester report "pitch does not work"): the Torso sim integrated + // R/F into currentElevation (authored limits/rates) but NOTHING + // consumed it -- the view and the aim ray (camera basis) never + // pitched. The pod's stick-Y aim pitches the EYE (the torso + // geometry does not tilt); compose it per frame with the look-state + // angles into eyepointRotation, which DPLEyeRenderable reads every + // frame (VIEW = baseOffset * R). BTCommitLookState stores the + // look component in gBTLookPitch/Yaw (below); the elevation rides + // the pitch term. Pitch sign PIXEL-CALIBRATED (BT_FORCE_ELEV + // screenshots): −elev pitched the view DOWN for a positive axis; + // R (axis +1) must aim UP -> +elev. + { + extern float gBTLookPitch, gBTLookYaw; + extern Scalar BTGetTorsoElevation(Subsystem *); + const float elev = (float)BTGetTorsoElevation(GetTorsoSubsystem()); + eyepointRotation = EulerAngles( + Radian(Radian::Normalize(gBTLookPitch + elev)), + Radian(Radian::Normalize(gBTLookYaw)), + Radian(0.0f)); + // ...and the AIM RAY: the boresight publisher (L4VIDRND task + // #48) LEVELS the view direction (drops terrain body pitch) -- + // publish the deliberate elevation so it re-applies onto the + // leveled boresight (else shots fire level over low targets). + extern float gBTEyeElev; + gBTEyeElev = elev; + } } // task #6 ORDER FIX: the scripted block must run BEFORE the fire-push @@ -4974,6 +5173,119 @@ void gBTValveKey = (s_vtFrame >= 600 && s_vtFrame < 610) ? 1 : 0; } + // Gitea #6 scripted verify (BT_VIEWCYCLE_TEST=): pulse one + // secondary-schematic cycle (Damage -> Critical -> Heat -> Damage) at + // the given frame and every 300 frames after -- with + // BT_DEV_GAUGES_DOCK + BT_SHOT the docked sec panel's schematic layer + // swap is pixel-verifiable. BT_MODECYCLE_TEST= does the same + // for the M-key CONTROL-mode cycle (the empirical proof that a + // control-mode change does NOT touch the secondary view). + if ((Entity *)this == application->GetViewpointEntity() + && getenv("BT_VIEWCYCLE_TEST")) + { + static int s_vcFrame = 0; + ++s_vcFrame; + int t0 = atoi(getenv("BT_VIEWCYCLE_TEST")); + if (t0 < 1) t0 = 600; + if (s_vcFrame >= t0 && (s_vcFrame - t0) % 300 == 0) + { + gBTDisplayCycle = 1; + } + } + if ((Entity *)this == application->GetViewpointEntity() + && getenv("BT_MODECYCLE_TEST")) + { + static int s_mcFrame = 0; + ++s_mcFrame; + int t0 = atoi(getenv("BT_MODECYCLE_TEST")); + if (t0 < 1) t0 = 600; + if (s_mcFrame >= t0 && (s_mcFrame - t0) % 300 == 0) + { + gBTModeCycle = 1; + } + } + + // Gitea #9 scripted verify (BT_PRESET_TEST=): from the given + // frame, pulse a preset-page cycle on ALL THREE upper MFDs every 120 + // frames (Quad -> populated Eng pages -> Quad, per MFD) -- with + // BT_DEV_GAUGES_DOCK + BT_SHOT the Mfd1/Mfd2/Mfd3 page swaps are + // pixel-verifiable ([mode] preset log lines under BT_MODE_LOG). + if ((Entity *)this == application->GetViewpointEntity() + && getenv("BT_PRESET_TEST")) + { + static int s_ptFrame = 0; + static int s_ptPulses = 0; + ++s_ptFrame; + int t0 = atoi(getenv("BT_PRESET_TEST")); + if (t0 < 1) t0 = 600; + // issue #11: BT_PRESET_HOLD= stops after n pulses so a page can + // be HELD indefinitely (steady-state ghost/graph verification -- + // the 120-frame cycle otherwise phase-locks with BT_SHOT's 90-frame + // cadence, so a fixed fraction of shots lands on transition frames). + int holdAfter = getenv("BT_PRESET_HOLD") ? atoi(getenv("BT_PRESET_HOLD")) : 0; + if (s_ptFrame >= t0 && (s_ptFrame - t0) % 120 == 0 + && (holdAfter <= 0 || s_ptPulses < holdAfter)) + { + ++s_ptPulses; + gBTPresetCycle[0] = 1; + gBTPresetCycle[1] = 1; + gBTPresetCycle[2] = 1; + } + } + + // Gitea #7 scripted verify (BT_FLUSH_TEST=): hold the coolant + // flush for ~60 sim frames starting at the given frame -- the + // reservoir drains ([flush] log), the coolant vertBar drops, and the + // FLUSH.PFX condensation cloud spawns on the 0->1 edge. + if ((Entity *)this == application->GetViewpointEntity() + && getenv("BT_FLUSH_TEST")) + { + static int s_ftFrame = 0; + ++s_ftFrame; + int t0 = atoi(getenv("BT_FLUSH_TEST")); + if (t0 < 1) t0 = 600; + gBTFlushKey = (s_ftFrame >= t0 && s_ftFrame < t0 + 60) ? 1 : 0; + } + + // Gitea #7: the COOLANT FLUSH button (the coolant MFD's top-right + // button, manual p24 -- "punch (or hold down) the coolant button to + // flush fresh coolant through your 'Mech's loops"). Press AND + // release edges both dispatch: InjectCoolant (id 4, the Reservoir + // table @0x50e680) starts the flush on data>=1 and stops it on + // data<1 -- the same held-button payload the pod's streamed MFD + // EventMapping delivers. + if ((Entity *)this == application->GetViewpointEntity()) + { + static int s_prevFlush = 0; + if (gBTFlushKey != s_prevFlush) + { + int pressed = (gBTFlushKey != 0); + s_prevFlush = gBTFlushKey; + Subsystem *reservoir = 0; + for (int s = 1; s < GetSubsystemCount(); ++s) + { + Subsystem *sub = GetSubsystem(s); + if (sub != 0 && (int)sub->GetClassID() == 0xBC0) // Reservoir + { + reservoir = sub; + break; + } + } + if (reservoir != 0) + { + if (getenv("BT_FLUSH_LOG")) + DEBUG_STREAM << "[flush-tx] InjectCoolant " + << (pressed ? "PRESS" : "RELEASE") << " -> " + << reservoir->GetName() << std::endl; + ReceiverDataMessageOf msg( + 4 /*Reservoir::InjectCoolantMessageID*/, + sizeof(ReceiverDataMessageOf), + (ControlsButton)(pressed ? 1 : 0)); + reservoir->Dispatch(&msg); + } + } + } + // task #13 dev harness: the COOLANT VALVE lever. 'C' edge -> dispatch // MoveValve (id 4, the Condenser table @0x50E52C) to the selected // condenser -- the same press payload the pod's engineering-screen aux @@ -6272,6 +6584,13 @@ int BTWeaponMountIsRear(void *ownerMech, int segIndex) return rear; } +// The current look-state eye component (pitch/yaw) -- stored so the per-frame +// eyepoint compose (the HUD tick above) can combine it with the live torso +// elevation. Single-viewpoint state (BTCommitLookState runs for the local +// player's mapper only), same pattern as the other viewpoint globals. +float gBTLookPitch = 0.0f; +float gBTLookYaw = 0.0f; + void BTCommitLookState(void *mech_v, int look_state) { Mech *m = (Mech *)mech_v; @@ -6291,6 +6610,8 @@ void BTCommitLookState(void *mech_v, int look_state) case 4: /*LookDown*/ pitch = m->lookFrontAngle; break; default: break; // LookNone: identity } + gBTLookPitch = (float)pitch; // per-frame compose adds elevation + gBTLookYaw = (float)yaw; m->eyepointRotation = EulerAngles( Radian(Radian::Normalize(pitch)), Radian(Radian::Normalize(yaw)), diff --git a/game/reconstructed/mechmppr.cpp b/game/reconstructed/mechmppr.cpp index 1668d53..715e182 100644 --- a/game/reconstructed/mechmppr.cpp +++ b/game/reconstructed/mechmppr.cpp @@ -521,7 +521,13 @@ void //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // -// @004afcac -- cycle the HUD display mode (0 -> 1 -> 2 -> 0). +// @004afcac -- cycle the DISPLAY mode (0 -> 1 -> 2 -> 0). Gitea #6: this is +// the secondary screen's Damage/Critical/Heat schematic cycle -- the L4 +// NotifyOfDisplayModeChange override (vtbl+0x4C, @004d1ae4) swaps the +// ModeSecondary* mask bits 18..20. The pod input was the "'Mech status Info +// center" button (manual p13, bottom left of the secondary screen); the DOS +// keyboard fallback was extended-F4 (Keypress 0x13e), dead under the WinTesla +// VK map. // void MechControlsMapper::CycleDisplayModeMessageHandler( @@ -533,16 +539,30 @@ void if (message->dataContents > 0) { - displayMode = displayMode + 1; - if (displayMode > 2) - { - displayMode = 0; - } - NotifyOfDisplayModeChange(displayMode); // vtable+0x4c + CycleDisplayModeNow(); } Check_Fpu(); } +// +// The display-cycle body, shared by the console-button message handler above +// and the desktop 'N' key (mech4 key poll -> gBTDisplayCycle) -- the same +// split as CycleControlModeNow. +// +void + MechControlsMapper::CycleDisplayModeNow() +{ + displayMode = displayMode + 1; + if (displayMode > 2) + { + displayMode = 0; + } + NotifyOfDisplayModeChange(displayMode); // vtable+0x4c + DEBUG_STREAM << "[mode] display mode -> " << displayMode + << " (0=Damage 1=Critical 2=Heat)" << std::endl; + Check_Fpu(); +} + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // @004afce8 -- toggle voice assist on the Mech's pilot subsystem. @@ -759,6 +779,7 @@ void // CONTROL MODE gauge tracks). { extern int gBTModeCycle; + extern int gBTDisplayCycle; extern float gBTTwistAxis; extern int gBTTorsoRecenter; if (gBTModeCycle) @@ -766,6 +787,14 @@ void gBTModeCycle = 0; CycleControlModeNow(); } + // Gitea #6: 'N' cycles the secondary screen's schematic + // (Damage -> Critical -> Heat) -- the same body the pod's + // status-info-center button message drives. + if (gBTDisplayCycle) + { + gBTDisplayCycle = 0; + CycleDisplayModeNow(); + } // TORSO ELEVATION (pitch): the pod stick's Y axis -- every // control mode routes stickPosition.y into // Torso::SetAnalogElevationAxis below, so the bridge feeds diff --git a/game/reconstructed/mechmppr.hpp b/game/reconstructed/mechmppr.hpp index f0bb261..d3f05f8 100644 --- a/game/reconstructed/mechmppr.hpp +++ b/game/reconstructed/mechmppr.hpp @@ -113,6 +113,13 @@ class Pilot; CycleDisplayModeMessageHandler( // @004afcac ReceiverDataMessageOf *message ); + + // The shared display-cycle body (Gitea #6: the pod console button + // message above + the desktop 'N' key both drive it) -- cycles the + // secondary screen's Damage -> Critical -> Heat schematic via the + // NotifyOfDisplayModeChange override (vtbl+0x4C, @004d1ae4). + void + CycleDisplayModeNow(); void ToggleVoiceAssistMessageHandler( // @004afce8 ReceiverDataMessageOf *message diff --git a/game/reconstructed/mechweap.cpp b/game/reconstructed/mechweap.cpp index 10db641..0dfc93e 100644 --- a/game/reconstructed/mechweap.cpp +++ b/game/reconstructed/mechweap.cpp @@ -118,10 +118,15 @@ static_assert((int)PoweredSubsystem::NextAttributeID const MechWeapon::IndexEntry MechWeapon::AttributePointers[]= { - // task #16: the FULL binary table @0x511890 (eleven entries) + the - // port-extra OutputVoltage alias. Every member offset matches the - // binary's (layout static_assert-locked); TriggerState stays the streamed - // fire-button binding (id 0x13 PINNED). + // task #16: the FULL binary table @0x511890 (eleven entries). Every member + // offset matches the binary's (layout static_assert-locked); TriggerState + // stays the streamed fire-button binding (id 0x13 PINNED). + // issue #11: the port-extra 0x1D "OutputVoltage" alias (-> rechargeLevel) is + // RETIRED -- the binary MechWeapon table ends at 0x1C, and the alias SHADOWED + // the authentic Emitter-level OutputVoltage (currentLevel@0x414, RAW volts -- + // the SeekVoltageGraph cursor feed) because AttributeIndexSet::Find walks + // lowest-id-first. Energy weapons now publish their own table (emitter.cpp + // ids 0x1D-0x25); no ballistic-weapon consumer ever bound the alias. // // The PADS fill our chain-vs-binary id gap (now 0x0F..0x11) with valid, // named, never-bound entries (the binary's own ids here are the powered/ @@ -142,8 +147,7 @@ const MechWeapon::IndexEntry ATTRIBUTE_ENTRY(MechWeapon, PipExtendedRange, pipExtendedRange), // 0x19 @0x348 ATTRIBUTE_ENTRY(MechWeapon, EstimatedReadyTime, estimatedReadyTime),// 0x1A @0x330 ATTRIBUTE_ENTRY(MechWeapon, RearFiring, rearFiring), // 0x1B @0x334 (task #68: real semantics) - ATTRIBUTE_ENTRY(MechWeapon, WeaponState, weaponAlarm), // 0x1C @0x350 - ATTRIBUTE_ENTRY(MechWeapon, OutputVoltage, rechargeLevel) // 0x1D (port alias) + ATTRIBUTE_ENTRY(MechWeapon, WeaponState, weaponAlarm) // 0x1C @0x350 (binary table END) }; MechWeapon::AttributeIndexSet& diff --git a/game/reconstructed/mechweap.hpp b/game/reconstructed/mechweap.hpp index 875f8a8..4109023 100644 --- a/game/reconstructed/mechweap.hpp +++ b/game/reconstructed/mechweap.hpp @@ -140,13 +140,14 @@ class CockpitHud; //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Attribute Support (gauge data-binding wave) // - // The energy/ballistic weapon engineering clusters read OutputVoltage (the - // charge/ready level -- animates as the weapon recharges after firing) and - // PercentDone (the recharge dial), both onto rechargeLevel (1.0 == ready). - // GetAttributeIndex() chains to PoweredSubsystem's so InputVoltage + the - // HeatSink temps also resolve; Emitter/PPC/ProjectileWeapon/MissileLauncher - // point their DefaultData at MechWeapon::GetAttributeIndex() to inherit it - // (they carried an EMPTY default-constructed index -> resolved NOTHING). + // The weapon engineering clusters read PercentDone (the recharge dial) onto + // rechargeLevel (1.0 == ready). GetAttributeIndex() chains to + // PoweredSubsystem's so InputVoltage + the HeatSink temps also resolve; + // ProjectileWeapon/MissileLauncher point their DefaultData at + // MechWeapon::GetAttributeIndex() to inherit it (they carried an EMPTY + // default-constructed index -> resolved NOTHING). Emitter/PPC now chain + // their OWN authentic table on top (emitter.cpp, ids 0x1D-0x25: Laser*/ + // Seek*/OutputVoltage=currentLevel -- issue #11). // public: enum { @@ -203,10 +204,12 @@ class CockpitHud; PipExtendedRangeAttributeID, // 0x19 @0x348 EstimatedReadyTimeAttributeID, // 0x1A @0x330 RearFiringAttributeID, // 0x1B @0x334 - WeaponStateAttributeID, // 0x1C @0x350 weaponAlarm - OutputVoltageAttributeID, // 0x1D PORT-EXTRA alias -> rechargeLevel (the - // engineering clusters bind it by name) - NextAttributeID + WeaponStateAttributeID, // 0x1C @0x350 weaponAlarm (binary table END) + // issue #11: the 0x1D "OutputVoltage" PORT-EXTRA alias is RETIRED -- + // it shadowed the authentic Emitter table's OutputVoltage + // (currentLevel@0x414 RAW volts, ids 0x1D-0x25 chain here exactly + // as in the binary; Find walks lowest-id-first). + NextAttributeID // 0x1D == the binary chain value }; private: static const IndexEntry AttributePointers[]; diff --git a/game/reconstructed/myomers.cpp b/game/reconstructed/myomers.cpp index 5eced4c..99f3370 100644 --- a/game/reconstructed/myomers.cpp +++ b/game/reconstructed/myomers.cpp @@ -40,8 +40,9 @@ // ConnectToMover/DisconnectFromMover bodies. // best-effort : MyomersSimulation (@004b8d18) is the per-tick Performance // (only reachable via function pointer; SetPerformance call -// site not captured -- see note); GetSpeedReading (@004b8f94, -// slot 15) purpose; ToggleSeekVoltage (@004b8a48) not present +// site not captured -- see note); [slot 15 @004b8f94 RESOLVED +// issue #11: SeekVoltageResponse -- the SeekVoltageGraph +// sampler, sqrt not fabs]; ToggleSeekVoltage (@004b8a48) not present // in the decompiled shards; the heat-base field @0x150 and // graphic-state block (this[7..9]) inherit-pattern stores. // excluded : @0x4b7f94..@0x4b881c (gimbal / torso-horizon display, base @@ -315,7 +316,7 @@ int Myomers::CreateStreamedSubsystem( //*************************************************************************** // // Core drive computation: returns the locomotion drive available for a given -// input voltage. (helper; called by RegisterMaxOutput and GetSpeedReading.) +// input voltage. (helper; called by RegisterMaxOutput and SeekVoltageResponse.) // // base = ownerMech[0x34C] * (input_voltage / seekVoltage[recommendedIndex]) // ^ Mech base-speed scalar ^ normalise to the recommended gear @@ -372,19 +373,40 @@ void Myomers::RegisterMaxOutput() //*************************************************************************** -// Myomers::GetSpeedReading slot 15 @004b8f94 +// Myomers::SeekVoltageResponse slot 15 @004b8f94 //*************************************************************************** // -// PoweredSubsystem virtual slot 15 (base @004b1780). Best-effort: converts -// AvailableOutput(input_voltage) into a normalised gauge reading -- output is -// scaled by 3.6 (m/s -> km/h) and divided by 350 (gauge full scale), then run -// through FUN_004dd138 (fp magnitude helper / sqrt). Drives a cockpit -// speed gauge. (Decompiled body discards the result -> returned via FPU.) +// PoweredSubsystem virtual slot 15 (base @004b1780) == the shared subsystem +// vtbl+0x3C sampler the SeekVoltageGraph's Execute (@004c6934) plots -- THIS +// is what draws the myomer engineering page's POWER curve (issue #11; the +// old "drives a cockpit speed gauge" guess and the GetSpeedReading name are +// retired). Converts AvailableOutput(input_voltage) into the graph's 0..1 +// x-response: scaled by 3.6 (m/s -> km/h), normalised to a 350 km/h full +// scale, then FUN_004dd138 == SQRT (part_015.c:4026 -- the old "fp magnitude +// / fabs" reading was wrong; sqrt is also what Emitter's analog @004bb42c +// applies to its power ratio). (Decompiled body discards the result -> +// returned via FPU.) // -Scalar Myomers::GetSpeedReading(Scalar input_voltage) +Scalar Myomers::SeekVoltageResponse(Scalar input_voltage) { Scalar output = AvailableOutput(input_voltage); // @004b8ac0 - return (Scalar)fabs((double)((output * 3.6f - 0.0f) / 350.0f)); // FUN_004dd138 (fp magnitude) + Scalar r = (output * 3.6f - 0.0f) / 350.0f; + // (FUN_004dd138 routes a negative operand to the matherr handler; output + // >= 0 in practice -- guarded for port safety.) + return (r > 0.0f) ? (Scalar)sqrt((double)r) : 0.0f; // FUN_004dd138 (sqrt) +} + +//*************************************************************************** +// issue #11 bridge -- the Myomers leg of the SeekVoltageGraph sample dispatch +// (see emitter.cpp BTSeekVoltageSample). Guarded: a non-Myomers subsystem +// samples 0 (only emitter/myomer pages own a graph in the shipped config). +//*************************************************************************** +Scalar BTMyomersSeekSample(void *subsystem, Scalar voltage) +{ + Entity *entity = (Entity *)subsystem; + if (entity == 0 || !entity->IsDerivedFrom(Myomers::ClassDerivations)) + return 0.0f; + return ((Myomers *)entity)->SeekVoltageResponse(voltage); // @004b8f94 } diff --git a/game/reconstructed/myomers.hpp b/game/reconstructed/myomers.hpp index 60ca9cd..0ef5416 100644 --- a/game/reconstructed/myomers.hpp +++ b/game/reconstructed/myomers.hpp @@ -245,10 +245,15 @@ class Mech; HandleMessage(int message); // slot 9, @004b8a8c (-> PoweredSubsystem @004b0efc) void ResetToInitialState(Logical powered = True); // slot 10, @004b8aa4 (-> PoweredSubsystem @004b0e6c) - // slot 15 (PoweredSubsystem base @004b1780) -- normalised speed/gauge readout - // (output km/h scaled to 350): GetSpeedReading @004b8f94 + // slot 15 (PoweredSubsystem base @004b1780) -- the SeekVoltageGraph's + // voltage-response sampler (issue #11: the shared subsystem + // vtbl+0x3C slot the graph's Execute @004c6934 calls; Emitter's + // analog is @004bb42c): sqrt(AvailableOutput(v)*3.6 / 350) + // -- normalised sqrt of the drive speed in km/h against a + // 350 km/h full scale. (Renamed from the best-effort + // "GetSpeedReading"; FUN_004dd138 is sqrt, not fabs.) Scalar - GetSpeedReading(Scalar input_voltage); // slot 15, @004b8f94 (name best-effort) + SeekVoltageResponse(Scalar input_voltage); // slot 15, @004b8f94 // slot 16 (PoweredSubsystem::IsSourceShorted base @004b0b5c) -- Myomers // requires the source to supply at least the selected seek voltage // AND be in the Ready (==2) state. diff --git a/game/reconstructed/powersub.cpp b/game/reconstructed/powersub.cpp index fdf9041..4bd0696 100644 --- a/game/reconstructed/powersub.cpp +++ b/game/reconstructed/powersub.cpp @@ -1449,3 +1449,18 @@ bool BTGetSubsystemAuxScreen(Subsystem *sub, int *screen, int *placement, char * if (label64 != NULL) strcpy(label64, ps->auxScreenLabel); // +0x10C return true; } + +// +// issue #11 bridge -- the SeekVoltageGraph's destroyed test: +// *(subsystem+0x40) == 1 in the binary (@004c6934) == MechSubsystem:: +// simulationState == 1 (Destroyed). Named-member read through the real type +// (databinding rule); every subsystem that owns a graph is MechSubsystem- +// derived (Emitter/PPC/Myomers), so the PoweredSubsystem gate is sufficient +// and a non-match reads "not destroyed". +// +int BTSubsystemDestroyed(Subsystem *sub) +{ + if (!BTIsPoweredSubsystem(sub)) + return 0; + return (((PoweredSubsystem *)sub)->GetSimulationState() == 1) ? 1 : 0; // +0x40 == 1 +} diff --git a/game/reconstructed/torso.cpp b/game/reconstructed/torso.cpp index b03191a..bd304d3 100644 --- a/game/reconstructed/torso.cpp +++ b/game/reconstructed/torso.cpp @@ -1040,6 +1040,18 @@ Scalar BTGetTorsoTwist(Subsystem *torso) return torso ? ((Torso *)torso)->CurrentTwist() : 0.0f; } +// +// Live torso ELEVATION (pitch aim, rad) -- Torso::currentElevation == the +// binary torso+0x1E4. Same complete-type-TU bridge pattern as +// BTGetTorsoTwist above. Consumed by the per-frame eyepoint compose +// (mech4.cpp): the R/F / stick-Y aim pitches the EYE only -- the torso +// geometry does not tilt, exactly like the pod. +// +Scalar BTGetTorsoElevation(Subsystem *torso) +{ + return torso ? ((Torso *)torso)->CurrentElevation() : 0.0f; +} + // // Task #56 bridge -- ADDRESS of the live torso twist for the gyro's external- // pitch pointer (binary bt_mech stream tail: gyro+0x258 = torso+0x1D8; the gyro