From 092408041c88b0bc3736b271ace7750202523075 Mon Sep 17 00:00:00 2001 From: arcattack Date: Sat, 11 Jul 2026 19:39:35 -0500 Subject: [PATCH] Cockpit: the CONFIG-MODE weapon-regrouping session + the live-play fire fixes (tasks #6+#11) TASK #6 -- the pod's in-cockpit weapon regrouping, fully reconstructed and live-verified (hold-configure -> tap fire button -> toggle membership -> release-commit): - MechWeapon handlers id 9 ConfigureMappables @004b9550 / id 10 ChooseButton @004b95b8 (binary table @0x511860; the old "Myomers fns" mislabel swept) + the GetMessageHandlers() accessor chain through Emitter/ProjectileWeapon/ MissileLauncher/GAUSS/PPC (empty per-class sets swallowed dispatch). - Mapper vtable truth: +0x38 EnterConfiguration / +0x3C ExitConfiguration / +0x40,+0x44 AddOrErase evt/dir. No "secondary vtable @0050f498", no "CreateTemporaryEventMappings" virtual (RP-name drift; swept incl CLASSMAP). L4 Enter/Exit rebuilt complete: StartMappableButtonsConfigure (the NonMapping 0x10000 <-> Mapping 0x8000 mode flip + the gauge's active-weapon latch) + the held-button re-arm + the 4 fire-button temp maps; only the RIO mapper implements the toggle (Thrustmaster no-ops = can't regroup). - MechSubsystem +0xE8/+0xEC corrected to controlDestination/controlMessageID (ex hostEntity/subsystemId2 mislabel); MechWeapon ctor defaults the destination to &fireImpulse (@004b99a8). - ConfigMapGauge state loop reconstructed (PE-recovered DAT_00518eb4 table; buttonGroup GetMapState sampler -- the "needs ModeManager" guard rationale was wrong). Finding [T1]: the shipped binary NEVER enables this gauge (no SetColor caller) -- authentically dormant; BT_CONFIGMAP=1 is the dev enable. - Dev harness: HOLD 'G' opens the session (BT_CONFIG_SLOT picks the weapon); BT_CONFIG_TEST scripts a headless verify. @004afbc4 corrected to its real Fail-trap body (the guessed AddOrErase(NULL) body would corrupt groups). - Bonus [T1]: the binary MechWeapon ATTRIBUTE table has ELEVEN entries (PercentDone..WeaponState) -- "TriggerState is the only one" was wrong. TASK #11 -- the user-reported live-play regressions (both real bugs): - PHANTOM FIRE: the mech4 bring-up shot block painted an explosion at the victim on its OWN 0.3s cadence whenever fire was held -- desynced once the authentic recharges landed. Retired (the real impact visual flows from each discharge via the messmgr SubmitExplosion). - WEAPON BRICKING (the "cuts out" bug): the Loading->Loaded snap window (+-0.01 around seekV) assumes the pod's LOCKED 60 fps; one port dt-spike jumps it, the byte-verified >1.0 clamp (_DAT_004ba830=0) zeroes readiness, and the weapon sticks in Loading at level ~10000 forever (observed live). Fixed with pod-frame (1/60s) sub-stepping of the binary's own Loading tick -- also fixes the I^2R integral over-heating generators on big steps. NEW GOTCHA CLASS recorded: reconstruction-gotchas #12 (frame-pacing trap). - Duty-cycle measured (max-rate autofire): generators equilibrate in the degradation band (recharge stretch ~3.5-4.4x), the FailureHeat breaker never trips solo, PPCs sustain ~11.8-damage full-charge fire. The slow kill pacing and recharge gaps are the AUTHENTIC heat economy; the pilot's counters (this regrouping UI, generator reassignment, coolant valves) are the reconstruction queue. Co-Authored-By: Claude Fable 5 --- context/decomp-reference.md | 23 ++++ context/gauges-hud.md | 10 ++ context/open-questions.md | 68 ++++++++++- context/reconstruction-gotchas.md | 19 ++- docs/VEHICLE_SUBSYSTEMS.md | 20 +++- game/original/BT/GAUSS.CPP | 3 +- game/original/BT/PPC.CPP | 3 +- game/reconstructed/CLASSMAP.md | 6 +- game/reconstructed/btl4gau2.cpp | 80 +++++++++++-- game/reconstructed/btl4mppr.cpp | 97 ++++++++++++---- game/reconstructed/btl4mppr.hpp | 49 +++++--- game/reconstructed/emitter.cpp | 64 +++++++--- game/reconstructed/emitter.hpp | 2 +- game/reconstructed/mech4.cpp | 187 ++++++++++++++++++------------ game/reconstructed/mechmppr.cpp | 50 ++++---- game/reconstructed/mechmppr.hpp | 46 +++++--- game/reconstructed/mechsub.cpp | 32 ++++- game/reconstructed/mechsub.hpp | 13 ++- game/reconstructed/mechweap.cpp | 108 ++++++++++++++++- game/reconstructed/mechweap.hpp | 41 ++++++- game/reconstructed/mislanch.cpp | 11 +- game/reconstructed/mislanch.hpp | 2 +- game/reconstructed/myomers.cpp | 2 +- game/reconstructed/powersub.cpp | 8 ++ game/reconstructed/projweap.cpp | 11 +- game/reconstructed/projweap.hpp | 2 +- 26 files changed, 748 insertions(+), 209 deletions(-) diff --git a/context/decomp-reference.md b/context/decomp-reference.md index b058a7d..baa3000 100644 --- a/context/decomp-reference.md +++ b/context/decomp-reference.md @@ -151,6 +151,29 @@ From the weapon `.SUB` records + the charge-curve `.data` constants (PE-parsed a --- +### The CONFIG-MODE session chain (task #6) [T1] +- MechWeapon handler table @0x511860: {9,"ConfigureMappables"→@004b9550}, {10,"ChooseButton"→@004b95b8} + (strings @0x511994/@0x5119A7). PoweredSubsystem table @0x50F4EC: ids 4-8 = SelectGeneratorA-D / + ToggleGeneratorMode → @004b099c/@004b09e4/@004b0a2c/@004b0a74/@004b0abc. +- MechWeapon ATTRIBUTE table @0x511890: ELEVEN entries — 0x12 PercentDone@0x320, 0x13 TriggerState@0x31C, + 0x14 DistanceToTarget@0x324, 0x15 TargetWithinRange@0x34C, 0x16 WeaponRange@0x328, 0x17 PipPosition, + 0x18 PipColor, 0x19 PipExtendedRange, 0x1A EstimatedReadyTime@0x330, 0x1B RearFiring@0x334, + 0x1C WeaponState@0x350. +- Mapper vtables: base @0050F45C (+0x38 Enter Fail@004b0280 / +0x3C Exit Fail@004b029c / +0x40 + AddOrErase-evt Fail@004b02b8 / +0x44 AddOrErase-dir Fail@004b02d4 / +0x48,+0x4C Notify @004b048c/@004b0494); + L4 @0051E440 (+0x38=@004d1840, +0x3C=@004d18dc, +0x40/+0x44 = RET no-ops @004d195c/@004d1964, + +0x48/+0x4C=@004d1acc/@004d1ae4); Thrustmaster @0051E3F0 (inherits the no-ops); RIO @0051E3A0 + (+0x40=@004d25e8, +0x44=@004d262c). @004afbc4 = a pure Fail thunk ("Unhandled button mapping!", 0x7a). +- ControlsManager vtbl: +0x20 StartMappableButtonsConfigure / +0x24 Stop / +0x28 AddOrEraseMappableButton + (T0 CONTROLS.cpp:291-351). Group vtbl: +0x0C Remove, +0x10 Update, +0x14 Add(evt), +0x18 Add(dir), + +0x1C AddOrErase(evt), +0x20 AddOrErase(dir), +0x24 GetMapState (unmapped=0/others=1/mine=2). +- Mode masks (T0 BTL4MODE.HPP): ModeMapping=0x8000 (bit 15) / ModeNonMapping=0x10000 (bit 16), mutually + exclusive. MechSubsystem +0xE8/+0xEC = controlDestination/controlMessageID (ctor pass-throughs; + MechWeapon defaults destination=&fireImpulse@0x31C per @004b99a8). Subsystem+0x110: 0/-1 config-session + flag written by handler 9 (reader unknown). +- ConfigMapGauge @004c6d80/@004c6f1c: table DAT_00518eb4 = {y,btn} {0x0d,0x45}{0x25,0x46}{0x3d,0x40} + {0x55,0x47}, blit x=0xc; SetColor @004c6ee0 has NO caller (gauge dormant in the shipped build). + ## 6. Env-var gates (runtime; default OFF unless noted) The authentic stack is DEFAULT-ON (`BTEnvOn`, mechrecon.hpp): `BT_GAIT_CUTOVER`, `BT_GAIT_SM`, diff --git a/context/gauges-hud.md b/context/gauges-hud.md index 7f7c314..1680758 100644 --- a/context/gauges-hud.md +++ b/context/gauges-hud.md @@ -68,6 +68,16 @@ 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] +## ConfigMapGauge (the weapon panel's regroup lamp column) — authentically DORMANT +The per-weapon btjoy.pcc joystick image + 4 cm_* state lamps (off/other/only/both) showing, +for each mappable fire button (Pinky/ThumbLow/Trigger/ThumbHigh), whether THIS panel's weapon +is bound to it. **The shipped binary never enables it [T1, task #6]:** no caller of SetColor +@004c6ee0 exists, so color==0 and Execute early-outs. The state loop is reconstructed +(btl4gau2.cpp; table DAT_00518eb4 PE-recovered; sampler = LBE4ControlsManager:: +buttonGroup[btn].GetMapState — NOT the ModeManager, the old guard note was wrong) behind the +PORT dev enable `BT_CONFIGMAP=1`. The regroup MECHANISM itself (ConfigureMappables/ +ChooseButton, task #6) is live regardless of the gauge. + ## Authentically-static (do NOT "fix") Degradation/Failure temps are fixed markers; AmbientTemperature 300; CoolantMassLeakRate 0 on a pristine mech (damage-gated); cmArmor/cmCrit all-green (undamaged solo player — BT is PvP-only); diff --git a/context/open-questions.md b/context/open-questions.md index c7139f3..eef5aea 100644 --- a/context/open-questions.md +++ b/context/open-questions.md @@ -133,12 +133,50 @@ authentic path scoped. ThumbHigh; BT_AUTOFIRE pulses the Trigger); the gBT*Trigger bypasses + pulse hack are retired. VERIFIED 2-node at the BINDING level (⚠ honest correction, task #8: those kills still flowed through the retired mech4 bring-up block -- the emitter DISCHARGE chain was latched shut by - the NaN edge bug below until 2026-07-11; the full weapon-fire chain is now verified solo). **Remaining (the config-mode session):** handlers - id 9/10 (@4b9550/@4b95b8 — corrected: they were mislabeled as Myomers fns), the L4 mapper - Enter/ExitConfiguration completion (StartMappableButtonsConfigure + mode 0x8000), the - ConfigMapGauge unguard — the pod's in-cockpit REGROUPING UI (pilot heat-management choice); - mapper vtable corrections: +0x38 Enter / +0x3c Exit / +0x44 RIO AddOrErase (Thrustmaster - no-ops it). + the NaN edge bug below until 2026-07-11; the full weapon-fire chain is now verified solo). +- **✅ THE CONFIG-MODE SESSION — RECONSTRUCTED (task #6, 2026-07-11) [T1 anchors].** The + pod's in-cockpit weapon-REGROUPING UI, fully decoded from the binary + T0 engine: + * **The flow**: the pilot HOLDS a weapon's configure button (a per-weapon type-6 + EventMapping, id 9, on the MFD-quadrant aux panels; e.g. BTL4.RES @0xAA3DC) → + MechWeapon handler id 9 "ConfigureMappables" @004b9550 (table @0x511860; the old + "Myomers fns" label swept) → mapper(slot 0)->EnterConfiguration @004d1840 (vtbl+0x38): + StartMappableButtonsConfigure (T0 CONTROLS.cpp:291 -- latches the active weapon for the + gauge, flips the GLOBAL mode NonMapping 0x10000 → Mapping 0x8000, so every normal fire + mapping goes dormant), re-arms the held button (+0x8000, id 9), temp-maps the four fire + buttons 0x40/0x45/0x46/0x47 → id 10. Tapping a fire button → handler id 10 + "ChooseButton" @004b95b8 → RIO AddOrErase(direct) @004d262c: buttonGroup[v-1].AddOrErase + (0x10000, &fireImpulse) — a TOGGLE; the permanent 0x10000 instances ARE the commit. + Release → ExitConfiguration @004d18dc: StopMappableButtonsConfigure + strip the 0x8000 + layer. Only the RIO (pod) mapper implements AddOrErase; L4/Thrustmaster carry RET + no-ops @004d195c/@004d1964 (a joystick cockpit cannot regroup). + * **Vtable truth (swept)**: base @0050F45C +0x38 Enter(Fail @004b0280) / +0x3C Exit(Fail + @004b029c) / +0x40 AddOrErase-event(Fail @004b02b8) / +0x44 AddOrErase-direct(Fail + @004b02d4) / +0x48,+0x4C Notify pair. There is NO "secondary vtable @0050f498" and NO + "CreateTemporaryEventMappings" virtual (RP-name drift). @004afbc4 (the mapper's shared + aux/zoom handler) is a pure Fail thunk ("Unhandled button mapping!", line 0x7a), not an + AddOrErase router. + * **Port landed**: MechWeapon handlers 9/10 + the GetMessageHandlers() accessor chain + (MechWeapon → Emitter/ProjectileWeapon → MissileLauncher, + GAUSS/PPC.CPP; dispatch + resolves through the CONCRETE class's SharedData — the empty per-class sets silently + swallowed the messages); the 6-arg Enter/1-arg Exit reshape; the L4 Enter/Exit full + bodies; MechSubsystem +0xE8/+0xEC corrected to controlDestination/controlMessageID (the + old hostEntity/subsystemId2 labels were wrong; MechWeapon ctor defaults the destination + to &fireImpulse per @004b99a8); dev harness = HOLD 'G' (BT_CONFIG_SLOT selects the + weapon; BT_CONFIG_TEST scripts a headless session). + * **ConfigMapGauge finding [T1]**: the shipped binary NEVER enables it — no caller of + SetColor @004c6ee0 exists (the only child accesses are BecameActive/SetEnable forwards), + so color stays 0 and Execute early-outs: the gauge is authentically DORMANT in BTL4OPT. + The state loop is now reconstructed anyway (@004c6f1c: 4 {y,button} pairs from + DAT_00518eb4 = {13,Pinky}{37,ThumbLow}{61,Trigger}{85,ThumbHigh}, x=0xc; + buttonGroup[btn].GetMapState(controlDestination, subsys, controlMessageID, 0x10000) → + cm_off/cm_other/cm_only/cm_both), behind the PORT dev enable BT_CONFIGMAP=1. + * **Bonus [T1]**: the binary MechWeapon ATTRIBUTE table @0x511890 has ELEVEN entries + (0x12 PercentDone .. 0x1C WeaponState) — future gauge-wave feeds; the "TriggerState is + the only MechWeapon attribute" claim was wrong (fixed in mechweap.hpp). + * Tails: weapon+0x110 (write 0/-1 on session open/close; port name vitalSubsystemIndex) + has NO confirmed reader yet; @004d1acc (+0x48 L4 Notify override) is unexported — + re-export from Ghidra; the PoweredSubsystem generator-select handlers (ids 4-8, + @004b099c..@004b0abc) are decoded but not yet registered in the port. - **Myomers authentic coupling** — the structural un-stub is INERT (mover feed + heat-gen no-op). Real coupling needs the advanced-damage gate (`OwnerAdvancedDamage`/`FUN_004ad7d4` → the owning **BTPlayer** `mech+0x190`+0x260, NOT 0xBD3 — see the mech+0x190 item above) + `MoverAttach` routing @@ -215,6 +253,24 @@ authentic path scoped. handlers (@004b099c..@004b0abc: assign weapon to Generator 1-4 + auto/manual toggle via modeAlarm@0x2CC -- wired to buttons, not yet reachable). +- **Task #11 -- the live-play regressions (2026-07-11) [T2].** User-reported after the + electrical model landed. THREE distinct causes, all resolved: + (1) *Phantom fire on the enemy*: the mech4 bring-up shot block painted an explosion at the + victim on its OWN 0.3s cadence whenever fire was held -- desynced once real recharges landed. + RETIRED (the authentic impact visual flows from each real discharge via the messmgr's + SubmitExplosion). (2) *Weapons bricking one by one* ("fire ~1s then cuts out"): the + FRAME-PACING TRAP -- see [[reconstruction-gotchas]] §12: the Loading->Loaded snap window + (+-0.01 around seekV) assumes the pod's locked 60 fps; a dt spike jumps it, the overshoot + clamp zeroes rechargeLevel, and the weapon sticks in Loading at level ~10000 forever. + Fixed with pod-frame (1/60s) sub-stepping of the binary's own Loading tick (also fixes the + I^2R integral overfeeding generator heat on big steps). (3) *Slow kills / short fire + windows*: AUTHENTIC -- beams last the authored DischargeTime, recharges take the authored + 2-5s stretched by hot generators (ChargeTimeScale), and max-rate autofire equilibrates + generators in the degradation band (1100-1500 K; the FailureHeat breaker never trips in + solo max-abuse -- duty-cycle measured). The pilot's authentic counters are the systems + under reconstruction: config-mode regrouping (task #6), generator reassignment (ids 4-8), + coolant valves. + ## Locomotion / combat polish (non-gating) - Authentic per-mech TURN-RATE constant (currently a bring-up constant rate). - Body-callback gimp handlers (states 16-19, targets 0x70b2/0x7161 undecoded → fall back to stand); diff --git a/context/reconstruction-gotchas.md b/context/reconstruction-gotchas.md index 80efd24..6908198 100644 --- a/context/reconstruction-gotchas.md +++ b/context/reconstruction-gotchas.md @@ -176,7 +176,24 @@ published attribute table MUST be a **dense prefix** from the parent's `NextAttr AVs. Fill gaps with a shared read-only pad member. Same for a class's `AttributeID` enum. [T2] -## 12. Verification gotchas (don't fool yourself) +## 12. Frame-pacing trap — the binary assumes a LOCKED 60 fps (task #11) + +The 1995 pod ran frame-locked; reconstructed per-frame logic can carry HIDDEN frame-rate +assumptions that variable dt violates. Archetype: the Emitter Loading tick — the charge +integrates toward the generator's 10000V and the Loading→Loaded transition only fires while +`rechargeLevel` crosses the ±0.01 snap window around seekV (~0.25s of travel ≈ 15 pod frames — +never missed at 60 fps). One port dt-spike (0.24s observed live) jumps the whole window; the +byte-verified >1.0 overshoot clamp (`_DAT_004ba830` = 0.0) then zeroes readiness and the weapon +is PERMANENTLY bricked in Loading at level ~10000 (user-visible: "weapons cut out one by one"). +Big steps also corrupt integrals evaluated at stale state (the I²R generator feed overheated). + +**Fix pattern: pod-frame sub-stepping** — run the binary's own tick verbatim inside a +`while (remaining) { slice = min(remaining, 1/60); … }` loop (bounded; leftover time resumes +next frame). This reproduces the pod's exact trajectory instead of redesigning the logic. +Suspect ANY reconstructed per-frame code with narrow equality/window tests or `x == 1.0f` +state transitions: charge/seek loops, snap comparisons, timers compared with `==`. + +## 13. Verification gotchas (don't fool yourself) - **Lazy gauge build:** `GaugeRenderer::BuildConfigurationFile` runs LAZILY. A too-early process kill shows `[gskip]=0` / "not built" even though the widget is fine — **wait for the gauge diff --git a/docs/VEHICLE_SUBSYSTEMS.md b/docs/VEHICLE_SUBSYSTEMS.md index c1684e4..0554bc0 100644 --- a/docs/VEHICLE_SUBSYSTEMS.md +++ b/docs/VEHICLE_SUBSYSTEMS.md @@ -84,9 +84,15 @@ except `SeekVoltageGraph`) (VertNormalSlider-ish) + connection `FUN_004c3288` (vt `0x518e90`) at `+0x2d`. 2nd ctor `0x4c72ac` (uses the engine `FUN_00474855` scalar-attr connection). dtor `0x4c733c`. - **`ConfigMapGauge`** @`0x4c6d80` (vt `0x51a1b8`) : GraphicGauge — 5 pcc names (btjoy/cm_off/cm_other/ - cm_only/cm_both) interned into the warehouse; Execute `0x4c6f1c` reads the control-mapper state from - `app->GetModeManager()` (`DAT_004efc94+0x3c`, group `0x1c0 + i*0x20`) and blits the matching frame. - SetColor `0x4c6ee0` (writes `this+0x94`). dtor `0x4c6e54`. + cm_only/cm_both) interned into the warehouse; Execute `0x4c6f1c` reads the mapping state from + `app->GetControlsManager()` (`DAT_004efc94+0x3c` == the LBE4ControlsManager — NOT the ModeManager, + which is `+0x50`): `buttonGroup[btn]` (array base `+0x1c0`, stride `0x20`) `->GetMapState(*(subsys+0xE8), + subsys, *(subsys+0xEC), 0x10000)` [T0 CONTROLS.h `ControlsUpdateManager::GetMapState`, returns + 0 unmapped / 1 mappedByOthers / 2 mappedByMe / 3 both == the cm_off/other/only/both frame index]. + The 4-slot table `DAT_00518eb4..d0` is RECOVERED from the image ({y,button} pairs, blit x=0xc): + {0x0d,0x45 Pinky} {0x25,0x46 ThumbLow} {0x3d,0x40 Trigger} {0x55,0x47 ThumbHigh} [T1 PE bytes]. + SetColor `0x4c6ee0` (writes `this+0x94`; Execute is inert while 0 — the activation caller is + unrecovered in the export). dtor `0x4c6e54`. Engine primitives it calls (ALREADY in `munga_engine.lib`, callable — do NOT reconstruct): `TwoState` (`FUN_004739d8`, vt `0x4fb740`), `NumericDisplayScalar` (`0x470888`, vt `0x4fbb40`), @@ -142,8 +148,12 @@ the resource parse, verifying combat/heat un-regressed. **This is the final, sig are affected. The authentic fix is the heat-leaf-branch re-base (the same §10 deferred item for the Sensor RadarPercent gauge) — a large, regression-risky change to the working combat/heat subsystem code — or a set of per-field bridges like the aux-screen one. -- **`ConfigMapGauge`'s control-mapper state overlay** — guarded off (raw App ModeManager offsets, `DAT_00518eb8` - unrecovered); the base joystick bitmap draws. +- **`ConfigMapGauge`'s control-mapper state overlay** — ✅ RECONSTRUCTED (task #6): the state loop is + live in btl4gau2.cpp (recovered `DAT_00518eb4` table; `buttonGroup[btn].GetMapState(...,0x10000)` via + the complete-type bridge `BTSubsystemControlFeed`; `MechSubsystem` +0xE8/+0xEC fixed to + controlDestination/controlMessageID, MechWeapon ctor defaults the destination to `&fireImpulse`). + The gauge stays authentically DORMANT (the shipped binary never calls SetColor `0x4c6ee0` — no caller + exists); `BT_CONFIGMAP=1` is the port's dev enable. - The **separate** dev-gauge window (960×384) didn't appear in the headless test run (the panels render in `BT_DEV_GAUGES_DOCK=1` docked mode); the separate-window path is existing dev-gauge infra, unrelated to this reconstruction. diff --git a/game/original/BT/GAUSS.CPP b/game/original/BT/GAUSS.CPP index cbc026f..c81674a 100644 --- a/game/original/BT/GAUSS.CPP +++ b/game/original/BT/GAUSS.CPP @@ -26,7 +26,8 @@ GaussRifle::SharedData GaussRifle::DefaultData( &GaussRifle::ClassDerivations, - GaussRifle::MessageHandlers, + 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) GaussRifle::StateCount ); diff --git a/game/original/BT/PPC.CPP b/game/original/BT/PPC.CPP index 1838f79..f067a69 100644 --- a/game/original/BT/PPC.CPP +++ b/game/original/BT/PPC.CPP @@ -32,7 +32,8 @@ PPC::SharedData PPC::DefaultData( &PPC::ClassDerivations, - PPC::MessageHandlers, + 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) PPC::StateCount ); diff --git a/game/reconstructed/CLASSMAP.md b/game/reconstructed/CLASSMAP.md index 8a42c92..595d59e 100644 --- a/game/reconstructed/CLASSMAP.md +++ b/game/reconstructed/CLASSMAP.md @@ -147,10 +147,10 @@ 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). SetControlMode@4d1ae4 (mode bits18-20, +│ 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]). -│ CreateTemporaryEventMappings@4d1840 / RemoveTemporaryEventMappings@4d18dc (buttons 0x40..0x47, hat-skip 0x41->0x45, -│ mode 0x8000=ConfigReady). NotifyOfControl/ConfigModeChange = no-ops @4d195c/@4d1964. KeypressMessageHandler@4d1bf0 +│ 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). │ file-scope helpers: SendFakeButtonEvent@4d13f0, SendFakeButtonEventPair@4d133c, DispatchDeveloperFakeEvent@4d141c (GeneratorA-D/Condenser1/LRM15/AmmoBinLRM15/Avionics/Hud test keys). │ ├─ MechThrustmasterMapper vtable 0051e3f0 ctor @4d21d0 dtor @4d22b4 TestInstance @4d22e0 ClassDerivations @0051dc78 diff --git a/game/reconstructed/btl4gau2.cpp b/game/reconstructed/btl4gau2.cpp index 2efbc25..d943ac0 100644 --- a/game/reconstructed/btl4gau2.cpp +++ b/game/reconstructed/btl4gau2.cpp @@ -66,6 +66,10 @@ #define BT_DEBUGSTREAM_DEFINED #include +// task #6 (ConfigMapGauge state loop): the LBE4 buttonGroup sampler. +#include +#include +#include #pragma hdrstop #if !defined(BTL4GAU2_HPP) @@ -385,6 +389,13 @@ ConfigMapGauge::ConfigMapGauge( localView.SetOrigin(x, y); // vtbl+0x10 (this+0x48) color = 0; // @0x94 this[0x25] + // task #6 finding [T1]: NO caller of SetColor (@004c6ee0) exists in the + // shipped binary -- the only accesses to the WeaponCluster's configMap + // child (@0xD4) are BecameActive forwards and SetEnable -- so the gauge is + // authentically DORMANT in BTL4OPT (color stays 0; Execute early-outs). + // BT_CONFIGMAP=1 is the PORT's dev enable to watch the state loop live. + if (getenv("BT_CONFIGMAP")) + color = 0xff; subsystem = subsystem_in; // @0x90 this[0x24] L4Warehouse *warehouse = (L4Warehouse *)renderer_in->warehousePointer; @@ -432,28 +443,35 @@ void // // @004c6f1c -- Execute. When enabled (color != 0): on the dirty flag, blit the -// base joystick bitmap; then for each of the 4 config-map slots read the control- -// mapper state and, if changed, stretch-blit the matching cm_* pixmap. +// base joystick bitmap; then for each of the 4 config-map slots read the fire +// button's map state and, if changed, blit the matching cm_* pixmap. // -// BRING-UP (marked): the per-slot control-mapper state comes from the App's -// ModeManager sampled through a .data port/colour table (DAT_00518eb8) at raw -// offsets (app+0x3c -> +0x1c0 + slot*0x20, sampler vtbl+0x24). That is the same -// raw-App-offset read class that AVs before the ModeManager layout is -// reconstructed (see the FillPilotArray fix, §10), and DAT_00518eb8 was not -// recovered from the optimised image -- so the mapper-state loop is guarded off -// and only the base-bitmap blit runs. The full loop lands with the ModeManager -// reconstruction. +// task #6 UNGUARD: the old deferral rationale was wrong on both counts -- +// (a) the sampler is NOT the App ModeManager; it is +// LBE4ControlsManager::buttonGroup[btn].GetMapState (group vtbl+0x24, all +// T0 engine, callable by name -- the same pattern btl4mppr.cpp compiles), +// (b) DAT_00518eb4 IS recoverable from the PE image: 4 {y, button} pairs +// {0x0d,0x45 Pinky} {0x25,0x46 ThumbLow} {0x3d,0x40 Trigger} +// {0x55,0x47 ThumbHigh}, blit x = 0xc. +// GetMapState(direct_destination, event_receiver, message_id, 0x10000) returns +// unmapped=0 / mappedByOthers=1 / mappedByMe=2 / both=3 == the cm_off/cm_other/ +// cm_only/cm_both frame index -- the per-weapon lamp column over the btjoy +// joystick image. The feed (subsys+0xE8/+0xEC = &fireImpulse/0 for weapons) +// comes through the complete-type bridge BTSubsystemControlFeed (mechsub.cpp). // +extern void BTSubsystemControlFeed(void *subsystem_in, void **destination_out, + int *message_id_out, void **receiver_out); + void ConfigMapGauge::Execute() { if (color == 0) return; + L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer; if (dirty) { dirty = False; - L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer; BitMap *base = warehouse->bitMapBin.Get(joystickImage); // FUN_00442aec if (base != NULL) { @@ -462,7 +480,45 @@ void } warehouse->bitMapBin.Release(joystickImage); } - // (control-mapper state loop deferred -- see note above.) + + // The 4-slot state loop (DAT_00518eb4, PE-recovered {y, button} pairs). + static const struct { int y; int button; } kSlot[4] = + { + { 0x0d, 0x45 }, // ButtonJoystickPinky + { 0x25, 0x46 }, // ButtonJoystickThumbLow + { 0x3d, 0x40 }, // ButtonJoystickTrigger + { 0x55, 0x47 }, // ButtonJoystickThumbHigh + }; + LBE4ControlsManager *controls = + (LBE4ControlsManager *)application->GetControlsManager(); // DAT_004efc94+0x3c + if (controls == NULL || subsystem == NULL) + return; + + void *destination = 0; + int messageID = 0; + void *receiver = 0; + BTSubsystemControlFeed(subsystem, &destination, &messageID, &receiver); + + for (int slot = 0; slot < 4; slot++) + { + int state = controls->buttonGroup[kSlot[slot].button].GetMapState( // group vtbl+0x24 + (ControlsButton *)destination, // *(subsys+0xE8) + (Receiver *)receiver, // the subsystem itself + messageID, // *(subsys+0xEC) + 0x10000 /*ModeNonMapping -- the persistent groups*/); + if (state != previousState[slot]) + { + previousState[slot] = state; + PixelMap8 *pix = + (PixelMap8 *)warehouse->pixelMap8Bin.Get(stateImage[state]); // FUN_00442d2b + if (pix != NULL) + { + localView.MoveToAbsolute(0xc, kSlot[slot].y); // vtbl+0x24 + localView.DrawPixelMap8(0 /*opaque*/, 0 /*rotation*/, pix); // vtbl+0x5c + } + warehouse->pixelMap8Bin.Release(stateImage[state]); // FUN_00442e51 + } + } } diff --git a/game/reconstructed/btl4mppr.cpp b/game/reconstructed/btl4mppr.cpp index d8ae2c5..66baf62 100644 --- a/game/reconstructed/btl4mppr.cpp +++ b/game/reconstructed/btl4mppr.cpp @@ -497,25 +497,45 @@ L4MechControlsMapper::MessageHandlerSet& // //############################################################################# -// CreateTemporaryEventMappings @004d1840 +// EnterConfiguration @004d1840 //############################################################################# // -// During configuration, route every mappable button (the 0x40..0x47 block, -// skipping the joystick hat 0x41..0x44) at the configuration receiver so the -// pilot can press a button to bind it. Mode mask 0x8000 == ModeConfigReady. +// THE CONFIG-MODE SESSION OPEN (task #6; the old reconstruction named this +// "CreateTemporaryEventMappings" and carried only the fire-button loop -- +// missing the mode flip, so config mode never actually ENGAGED). Called by +// MechWeapon handler id 9 (ConfigureMappables, @004b9550) when the pilot +// presses-and-HOLDS a weapon's configure button: +// 1. StartMappableButtonsConfigure (engine T0, ControlsManager vtbl+0x20): +// latches the active weapon (destination/receiver) for the ConfigMap +// gauge and flips the GLOBAL mode NonMapping(0x10000) -> Mapping(0x8000) +// -- every permanent fire mapping goes dormant for the session. +// 2. Re-arm the HELD configure button under 0x8000 so its RELEASE still +// reaches the weapon (-> ExitConfiguration). +// 3. Temp-map the four mappable fire buttons (0x40 Trigger, 0x45 Pinky, +// 0x46 ThumbLow, 0x47 ThumbHigh -- the 0x41..0x44 hat is skipped) to +// send ChooseButton (id 10) to the configuring weapon. // void - L4MechControlsMapper::CreateTemporaryEventMappings( - Receiver *receiver, - Receiver::MessageID config_message_id + L4MechControlsMapper::EnterConfiguration( + int held_element, + ControlsButton *destination, + Receiver *receiver, + Receiver::MessageID choose_message_id, + Receiver::MessageID configure_message_id, + Receiver::MessageID active_message_id ) { LBE4ControlsManager *controls = (LBE4ControlsManager*)application->GetControlsManager(); // DAT_004efc94+0x3c - // Route every mappable button (0x40..0x47, skipping the joystick hat - // 0x41..0x44) at the configuration receiver; mode 0x8000 == ModeConfigReady. - // (Loop bounds mirror the RP twin L4VTVControlsMapper.) + controls->StartMappableButtonsConfigure( // vtbl+0x20 (CONTROLS.cpp:291) + destination, receiver, receiver, active_message_id, + /*remove*/ 0x10000 /*ModeNonMapping*/, + /*add*/ 0x8000 /*ModeMapping*/); + + controls->buttonGroup[held_element].Add( // keep the held button live + 0x8000, receiver, configure_message_id, receiver); + for (int i = LBE4ControlsManager::FirstMappableButton; i <= LBE4ControlsManager::LastMappableButton; ++i) { @@ -524,22 +544,33 @@ L4MechControlsMapper::MessageHandlerSet& i = LBE4ControlsManager::ButtonJoystickPinky; // 0x45 } controls->buttonGroup[i].Add( - 0x8000, receiver, config_message_id, receiver); + 0x8000, receiver, choose_message_id, receiver); } Check_Fpu(); } // //############################################################################# -// RemoveTemporaryEventMappings @004d18dc +// ExitConfiguration @004d18dc //############################################################################# +// +// THE SESSION CLOSE (the configure button's RELEASE): restore the mode masks +// (Mapping -> NonMapping; the engine also clears the gauge's active* latch), +// then strip the 0x8000 temp layer off the held button + the four mappables. +// The regrouping itself needs no commit -- the RIO AddOrErase toggled the +// permanent 0x10000 mappings directly. // void - L4MechControlsMapper::RemoveTemporaryEventMappings() + L4MechControlsMapper::ExitConfiguration(int held_element) { LBE4ControlsManager *controls = (LBE4ControlsManager*)application->GetControlsManager(); + controls->StopMappableButtonsConfigure( // vtbl+0x24 (CONTROLS.cpp:319) + /*remove*/ 0x8000, /*add*/ 0x10000); + + controls->buttonGroup[held_element].Remove(0x8000); + for (int i = LBE4ControlsManager::FirstMappableButton; i <= LBE4ControlsManager::LastMappableButton; ++i) { @@ -554,20 +585,31 @@ L4MechControlsMapper::MessageHandlerSet& // //############################################################################# -// NotifyOfControlModeChange / NotifyOfConfigurationModeChange +// AddOrErase (event / direct) -- the L4/Thrustmaster no-ops // @004d195c / @004d1964 //############################################################################# // -// No-ops at the L4 layer (the gauges are driven directly off the mode masks), -// exactly like RP's L4VTVControlsMapper::NotifyOf* pair. +// task #6 CORRECTION: these two RET stubs occupy the +0x40/+0x44 AddOrErase +// vtable slots (section_dump @0051e440/@0051e3f0) -- the old reconstruction +// mislabeled them NotifyOfControlModeChange/NotifyOfConfigurationModeChange. +// A joystick (Thrustmaster) cockpit inherits them: pressing a fire button +// during a config session does NOTHING there. Only the pod RIO variant +// (@004d25e8/@004d262c) implements the group toggle. // void - L4MechControlsMapper::NotifyOfControlModeChange(int /*new_mode*/) + L4MechControlsMapper::AddOrErase( + unsigned int /*button_ID*/, + Receiver * /*receiver*/, + Receiver::MessageID /*message_ID*/ + ) { } void - L4MechControlsMapper::NotifyOfConfigurationModeChange(Logical /*new_state*/) + L4MechControlsMapper::AddOrErase( + unsigned int /*button_ID*/, + ControlsButton * /*destination*/ + ) { } @@ -969,9 +1011,13 @@ RIO_AUX_HANDLER(Aux3Eng4, 2, 4) // @004d24bc // AddOrErase (event / direct) @004d25e8 / @004d262c //############################################################################# // -// During configuration these bind the just-pressed button (1-based ID) either -// to a receiver+message or to a direct ControlsButton sink, under the base -// (ModeBasic, 0x10000) mode mask. Mirrors RP VTVRIOMapper::AddOrErase. +// THE REGROUP EDIT (task #6): during a config session these TOGGLE the +// just-pressed fire button's (1-based ID) permanent mapping under the +// ModeNonMapping (0x10000) mask -- the engine ControlsUpdateManager AddOrErase +// (CONTROLS.h:431-597) deletes a matching instance or creates one, so the +// weapon JOINS the button's fire group if absent and LEAVES it if present. +// The toggled 0x10000 instances ARE the commit; there is no save step. +// Mirrors RP VTVRIOMapper::AddOrErase. // void MechRIOMapper::AddOrErase( @@ -997,6 +1043,15 @@ RIO_AUX_HANDLER(Aux3Eng4, 2, 4) // @004d24bc (LBE4ControlsManager*)application->GetControlsManager(); controls->buttonGroup[button_ID - 1] .AddOrErase(0x10000, destination, this); // vtbl+0x20 + if (getenv("BT_FIRE_LOG")) + { + int state = controls->buttonGroup[button_ID - 1].GetMapState( + destination, 0, 0, 0x10000); + DEBUG_STREAM << "[rio-map] toggle btn=0x" << std::hex << (button_ID - 1) + << std::dec << " dest=" << (void *)destination + << " -> " << (((state & 2) != 0) ? "JOINED" : "LEFT") + << " (state=" << state << ")" << std::endl; + } Check_Fpu(); } diff --git a/game/reconstructed/btl4mppr.hpp b/game/reconstructed/btl4mppr.hpp index bed1236..ebf81b4 100644 --- a/game/reconstructed/btl4mppr.hpp +++ b/game/reconstructed/btl4mppr.hpp @@ -112,20 +112,39 @@ void SetPresetMode(int group, int item); // @004d1b24 (preset mode-mask) - void - NotifyOfControlModeChange(int new_mode), // @004d195c (no-op) - NotifyOfConfigurationModeChange(Logical new_state); // @004d1964 (no-op) - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Configuration support (overrides MechControlsMapper traps) + // Configuration support (overrides the MechControlsMapper traps; task #6 + // slot truth: @004d1840/+0x38 Enter, @004d18dc/+0x3C Exit, and the two + // RET-stub AddOrErase no-ops @004d195c/+0x40 / @004d1964/+0x44 that the + // 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.) // public: - void - CreateTemporaryEventMappings( // @004d1840 - Receiver *receiver, - Receiver::MessageID config_message_id - ), - RemoveTemporaryEventMappings(); // @004d18dc + virtual void + EnterConfiguration( // @004d1840 (+0x38) + int held_element, + ControlsButton *destination, + Receiver *receiver, + Receiver::MessageID choose_message_id, + Receiver::MessageID configure_message_id, + Receiver::MessageID active_message_id + ); + virtual void + ExitConfiguration(int held_element); // @004d18dc (+0x3C) + virtual void + AddOrErase( // @004d195c (+0x40, ret-stub no-op) + unsigned int button_ID, + Receiver *receiver, + Receiver::MessageID message_ID + ); + virtual void + AddOrErase( // @004d1964 (+0x44, ret-stub no-op) + unsigned int button_ID, + ControlsButton *destination + ); //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Construction and Destruction Support @@ -285,14 +304,14 @@ activePerformance = (Simulation::Performance)performance; } - void - AddOrErase( // @004d25e8 + virtual void + AddOrErase( // @004d25e8 (+0x40 override) unsigned int button_ID, Receiver *target, Receiver::MessageID message_ID ); - void - AddOrErase( // @004d262c + virtual void + AddOrErase( // @004d262c (+0x44 override) unsigned int button_ID, ControlsButton *destination ); diff --git a/game/reconstructed/emitter.cpp b/game/reconstructed/emitter.cpp index 13d85b5..3ff6c12 100644 --- a/game/reconstructed/emitter.cpp +++ b/game/reconstructed/emitter.cpp @@ -143,13 +143,22 @@ Derivation "Emitter" ); -Receiver::MessageHandlerSet Emitter::MessageHandlers; +// task #6: inherit MechWeapon's handler set (ids 9/10 ConfigureMappables/ +// ChooseButton) -- dispatch resolves through the CONCRETE class's SharedData, +// so an empty own-set here silently swallowed the config messages. +Receiver::MessageHandlerSet& + Emitter::GetMessageHandlers() +{ + static Receiver::MessageHandlerSet messageHandlers( + MechWeapon::GetMessageHandlers()); // copy-inherit + return messageHandlers; +} Simulation::AttributeIndexSet Emitter::AttributeIndex; Emitter::SharedData Emitter::DefaultData( &Emitter::ClassDerivations, - Emitter::MessageHandlers, + Emitter::GetMessageHandlers(), MechWeapon::GetAttributeIndex(), // gauge wave: inherit temps/InputVoltage/OutputVoltage/PercentDone (was empty) Emitter::StateCount ); @@ -390,21 +399,44 @@ void weaponAlarm.SetLevel(4); // TriggerDuringLoad blip weaponAlarm.SetLevel(3); } - // E7 RETIRED (task #10): the authentic charge path is live -- the - // PoweredSubsystem ctor attaches the authored generator, the Emitter - // ctor calibrates voltageScale/energyCoefficient, and TrackSeekVoltage - // integrates the charge over the authored RechargeRate seconds. The - // old unconditional snap-to-seek recharged every emitter in ONE frame - // (~0.3s cycle vs the authored 2-6s), multiplying the heat input ~16x - // (the "runaway": the routing itself was verified link-correct). - if (GetVoltageState() == 4) // this+0x278 == Ready + // E7 RETIRED (task #10): the authentic charge path is live (authored + // generator + calibrated voltageScale/EC; TrackSeekVoltage integrates + // the charge over the authored RechargeRate seconds). + // + // POD-FRAME SUB-STEPPING (task #11, THE WEAPON-BRICK FIX): the binary's + // Loading tick assumes the pod's LOCKED 60 fps -- the charge integrates + // toward the generator's 10000V and the Loaded transition only fires + // while rechargeLevel crosses the +-0.01 snap window around seekV + // (level in [7920, 8080]; ~15 pod frames wide). Under the port's + // variable dt, one spike frame (dt >= ~0.25s) jumps the whole window: + // the level overshoots, ComputeOutputVoltage's byte-verified >1.0 + // clamp (_DAT_004ba830 = 0.0) zeroes rechargeLevel, and the weapon is + // PERMANENTLY stuck Loading at level ~10000 (observed live: + // "level=9999.68 alarm=3 edge=1"). Fix: run the binary's own + // tick -- TrackSeekVoltage + ComputeOutputVoltage + the Loaded test -- + // at the binary's own frame rate. This also keeps the I^2R generator + // feed integral accurate (big steps evaluate seekRate at a stale level + // and OVER-heat the generators). Sub-step count is bounded; leftover + // time under extreme throttling just resumes next frame. { - TrackSeekVoltage(time_slice); // @004ba838 - } - ComputeOutputVoltage(); // (*vtable+0x44)() - if (rechargeLevel == 1.0f) // _DAT_004bac04 -- fully charged (== inherited @0x320) - { - weaponAlarm.SetLevel(2); // -> Loaded + static const Scalar kPodFrame = 1.0f / 60.0f; + Scalar remaining = time_slice; + int guard = 600; // bound: 10s of sim per frame + while (remaining > 0.0f && guard-- > 0) + { + Scalar slice = (remaining < kPodFrame) ? remaining : kPodFrame; + remaining -= slice; + if (GetVoltageState() == 4) // this+0x278 == Ready + { + TrackSeekVoltage(slice); // @004ba838 + } + ComputeOutputVoltage(); // (*vtable+0x44)() + if (rechargeLevel == 1.0f) // _DAT_004bac04 -- fully charged + { + weaponAlarm.SetLevel(2); // -> Loaded + break; + } + } } break; diff --git a/game/reconstructed/emitter.hpp b/game/reconstructed/emitter.hpp index 3b8afe0..15754e2 100644 --- a/game/reconstructed/emitter.hpp +++ b/game/reconstructed/emitter.hpp @@ -88,7 +88,7 @@ class NotationFile; // public: static Derivation ClassDerivations; - static Receiver::MessageHandlerSet MessageHandlers; + static Receiver::MessageHandlerSet& GetMessageHandlers(); // inherits MechWeapon's (task #6) static AttributeIndexSet AttributeIndex; static SharedData DefaultData; diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 62d79eb..5478484 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -644,6 +644,7 @@ int gBTMissileTrigger = 0; // channel 3: missiles (key 3 / CTRL) static int gBTLaserKey = 0; // raw key states (set by the keyboard poll) static int gBTPPCKey = 0; static int gBTMissileKey = 0; +static int gBTConfigKey = 0; // task #6: HOLD 'G' = the weapon-configure button // 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 @@ -1524,6 +1525,10 @@ void gBTLaserKey = focused && ((pAsync('1') | pAsync(0x20 /*VK_SPACE*/)) & dn) ? 1 : 0; gBTPPCKey = focused && (pAsync('2') & dn) ? 1 : 0; gBTMissileKey = focused && ((pAsync('3') | pAsync(0x11 /*VK_CONTROL*/)) & dn) ? 1 : 0; + // task #6: HOLD 'G' opens the config session on the selected + // weapon (BT_CONFIG_SLOT, default: the first weapon); while + // held, the fire keys TOGGLE its group membership. + gBTConfigKey = focused && (pAsync('G') & dn) ? 1 : 0; // gBTDrive.fire = "any weapon trigger down" (feeds the bring-up // damage dispatcher + the beam-visual keepalive) gBTDrive.fire = (gBTLaserKey || gBTPPCKey || gBTMissileKey) ? 1 : 0; @@ -3001,6 +3006,34 @@ void gBTHudPrimary = ((int)targetReticle.reticleElementMask & 0x20) != 0; } + // task #6 ORDER FIX: the scripted block must run BEFORE the fire-push + // block below -- it writes gBTLaserKey/gBTConfigKey, which the earlier + // keyboard poll zeroes each frame while the window is unfocused; the + // old placement AFTER the push block meant the push never saw the + // scripted press. + // task #6 scripted verify (BT_CONFIG_TEST=1, headless): drives the same + // gBTConfigKey/gBTLaserKey states the keyboard would -- + // t=[8,11) hold the configure button (session open) + // t=[9.5,9.7) one Trigger press edge -> ChooseButton toggle + // t>=13 0.2s Trigger pulses -> does the weapon fire now? + if ((Entity *)this == application->GetViewpointEntity() + && getenv("BT_CONFIG_TEST")) + { + // frame-count driven (this env throttles background windows to a + // few fps -- wall/sim seconds are useless for scripting) + static int s_cfgFrame = 0; + ++s_cfgFrame; + (void)dt; + // Schedule LATE (frame 2000+): the sim ticks many frames per second + // and the first-run schedule (30-120) fired before the enemy spawn + // completed -- targetInArc false -> the fire press never pushed. + gBTConfigKey = (s_cfgFrame >= 2000 && s_cfgFrame < 2600) ? 1 : 0; + int fireOn = (s_cfgFrame >= 2300 && s_cfgFrame < 2320) ? 1 : 0; // ONE press edge + if (s_cfgFrame >= 3000) + fireOn = ((s_cfgFrame / 60) & 1); // fire pulses after exit + gBTLaserKey = fireOn; + } + // E8: pulse the three fire channels per frame (1,0,1,0...) so each // weapon sim's CheckFireEdge sees clean rising edges. UNCONDITIONAL -- // NOT inside the enemy block: firing needs only the world PICK (task @@ -3067,6 +3100,77 @@ void } } + + // task #6 dev harness: the config-session BRACKET. 'G' edge -> + // dispatch ConfigureMappables (id 9) press/release to the selected + // weapon -- the exact ReceiverDataMessageOf payload + // (+-(element+1)) the pod's streamed aux-panel EventMapping delivers. + // Everything downstream is the AUTHENTIC chain: EnterConfiguration + // flips the global mode to Mapping(0x8000) (normal fire mappings go + // dormant for the hold), the fire keys deliver ChooseButton (id 10) + // through the temp 0x8000 event mappings pushed by the SAME live-mode + // button path above, and the release restores NonMapping(0x10000). + if ((Entity *)this == application->GetViewpointEntity()) + { + static int s_prevCfg = 0; + if (gBTConfigKey != s_prevCfg) + { + s_prevCfg = gBTConfigKey; + Subsystem *weapon = 0; + const char *slotEnv = getenv("BT_CONFIG_SLOT"); + int wantSlot = (slotEnv != 0) ? atoi(slotEnv) : -1; + for (int s = 1; s < GetSubsystemCount(); ++s) // slot 0 = the MAPPER, never a weapon + { + Subsystem *sub = GetSubsystem(s); + if (sub == 0) + continue; + // Pick by WEAPON classID (0xBC8 Emitter / 0xBCA ProjectileWeapon / + // 0xBCE GaussRifle / 0xBD0 MissileLauncher / 0xBD4 PPC). The old + // (simulationFlags & 0x8) heuristic matched the MAPPER at slot 0: + // its MakeViewpointEntity resource is stack-built with only + // name/classID/size set, so its flags are STACK GARBAGE -- and a + // config message dispatched to the mapper lands in the mapper's + // OWN id space (the aux-preset Fail trap) = the abort popup. + int cid = (int)sub->GetClassID(); + if (wantSlot >= 0 ? (s == wantSlot) + : (cid == 0xBC8 || cid == 0xBCA || cid == 0xBCE + || cid == 0xBD0 || cid == 0xBD4)) + { + weapon = sub; + break; + } + } + if (weapon == 0 && getenv("BT_FIRE_LOG")) + { + DEBUG_STREAM << "[config] NO WEAPON matched (count=" + << GetSubsystemCount() << ")" << std::endl; + for (int s2 = 0; s2 < GetSubsystemCount() && s2 < 33; ++s2) + { + Subsystem *p = GetSubsystem(s2); + if (p != 0) + DEBUG_STREAM << " [cfg-scan] " << s2 << " " << p->GetName() + << " flags=0x" << std::hex << (int)p->simulationFlags + << std::dec << std::endl; + } + } + if (weapon != 0) + { + const int kElem = 0x0e; // an aux-panel element (as authored) + ReceiverDataMessageOf msg( + 9 /*MechWeapon::ConfigureMappablesMessageID (mechweap.hpp)*/, + sizeof(ReceiverDataMessageOf), + (ControlsButton)(gBTConfigKey ? (kElem + 1) : -(kElem + 1))); + weapon->Dispatch(&msg); + if (getenv("BT_FIRE_LOG")) + DEBUG_STREAM << "[config] " << (gBTConfigKey ? "ENTER" : "EXIT") + << " session on " << weapon->GetName() + << " mode now=0x" << std::hex + << (int)application->GetModeManager()->GetModeMask() + << std::dec << std::endl; + } + } + } + { // The VICTIM under the boresight (task #46: any peer mech, not just // the solo gEnemyMech). Range/log/fire all key off THIS mech. @@ -3115,11 +3219,6 @@ void gAimHits = 0; gAimNoRay = 0; gAimGround = 0; } - if (gFireCooldown > 0.0f) - gFireCooldown -= dt; - - const int fireWanted = gBTDrive.fireForced || gBTDrive.fire; - // Resolve the "explode" effect resource once. if (gExplodeReady == 0) { @@ -3143,72 +3242,18 @@ void } } - // FIRE + DAMAGE: only vs a MECH under the boresight (terrain/sky picks - // draw beams but deal no damage). Dispatch routes to the picked victim - // -- for a REPLICANT this is a cross-pod hit: Entity::Dispatch reroutes - // the TakeDamageMessage to the owning master over the wire (task #46). - if (fireWanted && victim != 0 && targetInArc && gFireCooldown <= 0.0f - && anyWeaponInRange && gExplodeReady == 1) - { - gFireCooldown = kFireCooldown; - ++gShotCount; - - // Impact = the PICKED HULL POINT -> the STEP-6 cylinder lookup - // resolves the zone under the boresight (aimed fire). - Point3D impact = hotPoint; - - Origin exp_origin = ((Mech *)victim)->localOrigin; - exp_origin.linearPosition = impact; // at the hit point - // IMPACT FRAME: -Z aimed from the victim at the shooter (the .PFX - // hit spray convention; yaw = atan2(ddx, ddz)). [T0] - exp_origin.angularPosition = - EulerAngles(0.0f, (Scalar)atan2((double)ddx, (double)ddz), 0.0f); - - Explosion::MakeMessage exp_message( - Explosion::MakeMessageID, - sizeof(Explosion::MakeMessage), - (Entity::ClassID)RegisteredClass::ExplosionClassID, - EntityID::Null, - gExplodeRes, - Explosion::DefaultFlags, - exp_origin, - victim->GetEntityID(), // entity hit - GetEntityID()); // shooter (this mech) - - Explosion *shot = Explosion::Make(&exp_message); - if (shot) - { - Register_Object(shot); - DEBUG_STREAM << "[fire] SHOT #" << gShotCount - << " -> explosion at target (range=" << range << ")\n" << std::flush; - } - - // --- DAMAGE (real, STEP 6): dispatch UNAIMED (zone == -1) with the - // beam's world entry point. Mech::TakeDamageMessageHandler - // resolves the zone from the cylinder table; the base handler - // routes it to Mech__DamageZone::TakeDamage. Dispatch on a - // REPLICANT is rerouted by Entity::Dispatch to the owning - // master over the wire -- the cross-pod damage path. - // DAMAGE DELIVERY MOVED (task #8): the AUTHENTIC submitter is - // each firing weapon's own SendDamageMessage (@004b9728, called - // from Emitter::FireWeapon with the authored per-shot - // damagePortion + damageForce + impact) into the - // SubsystemMessageManager. This block keeps only the - // presentation roles: the boresight pick feeds the target - // slots the weapons read (mech+0x388/0x37C/0x38C), the [fire] - // beam-entry explosion visual above, and the shot log. The - // flat kShotDamage build + shooter-side score moved with the - // submission (mechweap.cpp). - if (getenv("BT_FIRE_LOG") && ((Mech *)victim)->damageZoneCount > 0) - { - DEBUG_STREAM << "[damage] beam hit " << (void*)victim - << (((Mech *)victim)->GetInstance() == ReplicantInstance - ? " (REPLICANT -> cross-pod via the weapon submit)" : "") - << "\n" << std::flush; - } - // DEATH effects fire at the VICTIM's own death transition - // (UpdateDeathState, task #42) -- MP-correct: the master runs it. - } + // THE PHANTOM-SHOT BLOCK IS RETIRED (task #11, user-reported): this + // bring-up stand-in spawned an explosion at the victim on its OWN + // 0.3s cadence whenever the fire key was down -- matched the old + // one-frame recharge, but once the AUTHENTIC electrical model + // landed (task #10: 2-5s recharges + the generator thermal breaker) + // it kept painting hits while no weapon was discharging ("enemy + // shows fire even though I'm not producing lasers"). The authentic + // impact visual flows from each REAL discharge: Emitter::FireWeapon + // -> SendDamageMessage -> SubsystemMessageManager explosion + // bundling -> SubmitExplosion (messmgr.cpp). This block now keeps + // only its presentation roles above (boresight pick -> the target + // slots the weapons read, and the [target] log). } } diff --git a/game/reconstructed/mechmppr.cpp b/game/reconstructed/mechmppr.cpp index 3e2709d..e1acd0f 100644 --- a/game/reconstructed/mechmppr.cpp +++ b/game/reconstructed/mechmppr.cpp @@ -408,25 +408,21 @@ Logical //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // @004afbc4 -- shared handler for the auxiliary-equipment and zoom buttons. -// The decomp body for this thunk was not captured; per the VTVControlsMapper -// pattern it routes the press through the configuration / mapping interface. -// BEST-EFFORT. +// task #6 CORRECTION (disasm-verified): the base body is a pure FAIL TRAP -- +// push 0x7a / "Unhandled button mapping!" @0x50f24a / MECHMPPR.CPP @0x50f265 / +// call Fail -- i.e. "not overridden". The real aux/zoom behavior lives in the +// derived L4 layer's own handlers. The old guessed body called +// AddOrErase(value, NULL) -- with the RIO mapper live that would TOGGLE a +// NULL-destination direct mapping into the fire groups. // void MechControlsMapper::ConfigureMappableMessageHandler( - ReceiverDataMessageOf *message + ReceiverDataMessageOf * /*message*/ ) { - Check(this); - Check(message); - - if (message->dataContents > 0) - { - // TODO: verify against @004afbc4 -- expected to add or erase a - // temporary mapping for the pressed mappable button. - AddOrErase(message->dataContents, (ControlsButton *)0); - } - Check_Fpu(); + DEBUG_STREAM << "[FAIL] Unhandled button mapping (base ConfigureMappableMessageHandler)" + << std::endl; + Fail("Unhandled button mapping!\n"); // MECHMPPR.CPP:0x7a } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -1149,39 +1145,46 @@ Pilot * // @004b029c // void - MechControlsMapper::ExitConfiguration() + MechControlsMapper::ExitConfiguration(int /*held_element*/) { - Fail("ExitConfiguration not overridden!\n"); // MECHMPPR.CPP:0x245 + DEBUG_STREAM << "[FAIL] ExitConfiguration not overridden (base mapper in slot 0)" << std::endl; + Fail("ExitConfiguration not overridden!\n"); // @004b029c MECHMPPR.CPP:0x245 } // -// (companion to the above; "EnterConfiguration not overridden!" @0050f370) +// @004b0280 (vtable +0x38; "EnterConfiguration not overridden!" @0050f370) // void MechControlsMapper::EnterConfiguration( + int /*held_element*/, ControlsButton * /*destination*/, Receiver * /*receiver*/, - Receiver::MessageID /*activation_message_id*/, - Receiver::MessageID /*configuration_message_id*/ + Receiver::MessageID /*choose_message_id*/, + Receiver::MessageID /*configure_message_id*/, + Receiver::MessageID /*active_message_id*/ ) { + DEBUG_STREAM << "[FAIL] EnterConfiguration not overridden (base mapper in slot 0)" << std::endl; Fail("EnterConfiguration not overridden!\n"); } // -// @004b02b8 +// @004b02b8 (vtable +0x40; the EVENT AddOrErase overload -- the old +// "CreateTemporaryEventMappings" name was RP drift, task #6) // void - MechControlsMapper::CreateTemporaryEventMappings( + MechControlsMapper::AddOrErase( + unsigned int /*button_ID*/, Receiver * /*receiver*/, - Receiver::MessageID /*config_message_id*/ + Receiver::MessageID /*message_ID*/ ) { + DEBUG_STREAM << "[FAIL] Unhandled mapping (base AddOrErase event)" << std::endl; Fail("Unhandled mapping!\n"); // MECHMPPR.CPP:0x24f } // -// @004b02d4 +// @004b02d4 (vtable +0x44; the DIRECT AddOrErase overload) // void MechControlsMapper::AddOrErase( @@ -1189,6 +1192,7 @@ void ControlsButton * /*destination*/ ) { + DEBUG_STREAM << "[FAIL] Unhandled mapping (base AddOrErase direct)" << std::endl; Fail("Unhandled mapping!\n"); // MECHMPPR.CPP:0x25a } diff --git a/game/reconstructed/mechmppr.hpp b/game/reconstructed/mechmppr.hpp index d2b9079..b848e87 100644 --- a/game/reconstructed/mechmppr.hpp +++ b/game/reconstructed/mechmppr.hpp @@ -259,27 +259,43 @@ class Pilot; FillPilotArray(); // @004b06cc // - // Configuration / event-mapping interface (the secondary vtable @0050f498). - // Platform-specific derived mappers override these; the base mapper traps - // any unrouted call. + // Configuration / event-mapping interface -- the CONFIG-MODE session + // virtuals (task #6, vtable slots verified from the raw tables in + // section_dump: base @0050F45C, L4 @0051E440, Thrustmaster @0051E3F0, + // RIO @0051E3A0). There is NO "secondary vtable @0050f498" -- that + // address is simply slot 15 INSIDE the primary base vtable. Slot map: + // +0x38 EnterConfiguration (base Fail stub @004b0280; L4 @004d1840) + // +0x3C ExitConfiguration (base Fail stub @004b029c; L4 @004d18dc) + // +0x40 AddOrErase(event) (base Fail @004b02b8; L4/TM no-op @004d195c; RIO @004d25e8) + // +0x44 AddOrErase(direct) (base Fail @004b02d4; L4/TM no-op @004d1964; RIO @004d262c) + // +0x48/+0x4C NotifyOf* pair (base no-ops @004b048c/@004b0494) + // The old reconstruction here ("CreateTemporaryEventMappings", a 4-arg + // Enter, a 0-arg Exit) was RP-name drift; the binary signatures come from + // the ONLY callers -- MechWeapon handlers id 9/10 @004b9550/@004b95b8: + // Enter(|v|-1, &weapon.fireImpulse, weapon, /*choose*/10, /*configure*/9, /*active*/0) + // Exit(|v|-1) + // AddOrErase(v /*1-based*/, &weapon.fireImpulse) // public: virtual void - EnterConfiguration( - ControlsButton *destination, - Receiver *receiver, - Receiver::MessageID activation_message_id, - Receiver::MessageID configuration_message_id - ); - virtual void - ExitConfiguration(); // @004b029c - virtual void - CreateTemporaryEventMappings( // @004b02b8 + EnterConfiguration( // +0x38 @004b0280 + int held_element, + ControlsButton *destination, Receiver *receiver, - Receiver::MessageID config_message_id + Receiver::MessageID choose_message_id, + Receiver::MessageID configure_message_id, + Receiver::MessageID active_message_id ); virtual void - AddOrErase( // @004b02d4 + ExitConfiguration(int held_element); // +0x3C @004b029c + virtual void + AddOrErase( // +0x40 @004b02b8 (event overload) + unsigned int button_ID, + Receiver *receiver, + Receiver::MessageID message_ID + ); + virtual void + AddOrErase( // +0x44 @004b02d4 (direct overload) unsigned int button_ID, ControlsButton *destination ); diff --git a/game/reconstructed/mechsub.cpp b/game/reconstructed/mechsub.cpp index b4e054c..c5dfbe5 100644 --- a/game/reconstructed/mechsub.cpp +++ b/game/reconstructed/mechsub.cpp @@ -124,8 +124,8 @@ MechSubsystem::MechSubsystem( { refCount = NewRefCount(0x10); // this[0x3d] this->owner = owner; // this[0x34] -- the owning Mech (canonical) - hostEntity = owner; // this[0x3a] = param_7 - subsystemId2 = subsystem_ID; // this[0x3b] = param_8 + controlDestination = 0; // this[0x3a] = param_7 (ctor pass-through; + controlMessageID = 0; // this[0x3b] = param_8 weapons override -- task #6) vitalSubsystem = 0; // this[0x39] BindName(refCount, "None"); // FUN_00402a98(.,this+0x3d,"None") @0x50df7a alarmModel = 0; // identity ResourceID // FUN_00408440(this+0x3e, identity) @@ -159,8 +159,8 @@ MechSubsystem::MechSubsystem( // ctor only set hostEntity; subclasses read // `owner` widely -> must be set or it is garbage.) statusAlarm.SetLevel(0); // FUN_0041bbd8(this+0xb, 0) - hostEntity = (Mech *)subsystem_resource->segmentIndex; // this[0x3a]=param_6 - subsystemId2 = subsystem_ID; // this[0x3b]=param_7 + controlDestination = 0; // this[0x3a]=param_6 (ctor pass-through; + controlMessageID = 0; // this[0x3b]=param_7 weapons override -- task #6) resource = subsystem_resource; // this[0x3c] vitalSubsystemIndex = -1; // this[0x44] vitalSubsystem = (subsystem_resource->vitalSubsystemIndex == 1); // res+0x48==1 @@ -487,3 +487,27 @@ int Check_Fpu(); return True; } + + +//############################################################################# +// BTSubsystemControlFeed -- complete-type bridge (task #6) +// +// ConfigMapGauge::Execute (@004c6f1c) reads the subsystem's controls feed +// (*(subsys+0xE8) destination, *(subsys+0xEC) message id) for GetMapState. +// The gauge TU has no complete MechSubsystem (databinding-trap rules: never +// raw-read a compiled object's offsets) -- this bridge reads the NAMED members +// and hands back the Receiver upcast alongside. +// +void + BTSubsystemControlFeed( + void *subsystem_in, + void **destination_out, + int *message_id_out, + void **receiver_out + ) +{ + MechSubsystem *sub = (MechSubsystem *)subsystem_in; + *destination_out = sub->controlDestination; // @0xE8 + *message_id_out = sub->controlMessageID; // @0xEC + *receiver_out = (Receiver *)sub; +} diff --git a/game/reconstructed/mechsub.hpp b/game/reconstructed/mechsub.hpp index 8dbac82..c1c5bee 100644 --- a/game/reconstructed/mechsub.hpp +++ b/game/reconstructed/mechsub.hpp @@ -289,8 +289,17 @@ class Damage; // subsystem KILLS the mech (ctor: res+0x48 // "VitalSubsystem"==1; was misnamed // videoObjectFlag) [T1 part_012.c:15776] - Mech *hostEntity; // @0xE8 this[0x3a] - int subsystemId2; // @0xEC this[0x3b] + // task #6 CORRECTION: these two are the CONTROLS feed, not identity -- + // the binary ctor chain stores its destination/message ctor pass-throughs + // here (FUN_004ac644 part_012.c:15772-15773), and ConfigMapGauge::Execute + // @004c6f1c reads BOTH for buttonGroup GetMapState. MechWeapon's ctor + // defaults the destination to &fireImpulse (@004b99a8 part_013.c:6886-9). + // The old names hostEntity/subsystemId2 (populated with owner/subsystem_ID) + // were a mislabel -- any reader would have classified every weapon as + // "mappedByOthers". (No port reader existed; verified before the rename.) + void *controlDestination; // @0xE8 this[0x3a] (ControlsButton* the direct maps write) + int controlMessageID; // @0xEC this[0x3b] (event-mapping message id; weapons 0) + friend void BTSubsystemControlFeed(void *, void **, int *, void **); // gauge bridge (task #6) SubsystemResource *resource; // @0xF0 this[0x3c] void *refCount; // @0xF4 this[0x3d] ResourceDescription::ResourceID diff --git a/game/reconstructed/mechweap.cpp b/game/reconstructed/mechweap.cpp index 4ad7f90..3f945dc 100644 --- a/game/reconstructed/mechweap.cpp +++ b/game/reconstructed/mechweap.cpp @@ -59,6 +59,7 @@ #include #include // SubsystemMessageManager (task #8 damage submission) +#include // MechControlsMapper -- the config-session virtuals (task #6) #pragma hdrstop #if !defined(MECHWEAP_HPP) @@ -81,7 +82,27 @@ Derivation "MechWeapon" ); -Receiver::MessageHandlerSet MechWeapon::MessageHandlers; +// task #6 -- the binary's MechWeapon handler table @0x511860 (section_dump: +// {9, "ConfigureMappables"@0x511994, @004b9550}, {10, "ChooseButton"@0x5119A7, +// @004b95b8}); chained onto the PoweredSubsystem set. +const Receiver::HandlerEntry + MechWeapon::MessageHandlerEntries[]= +{ + MESSAGE_ENTRY(MechWeapon, ConfigureMappables), // id 9 @004b9550 + MESSAGE_ENTRY(MechWeapon, ChooseButton) // id 10 @004b95b8 +}; + +Receiver::MessageHandlerSet& + MechWeapon::GetMessageHandlers() +{ + static Receiver::MessageHandlerSet messageHandlers( + ELEMENTS(MechWeapon::MessageHandlerEntries), + MechWeapon::MessageHandlerEntries, + PoweredSubsystem::GetMessageHandlers() + ); + return messageHandlers; +} + Simulation::AttributeIndexSet MechWeapon::AttributeIndex; // legacy empty member (superseded by GetAttributeIndex) // gauge data-binding wave: OutputVoltage/PercentDone -> rechargeLevel (1.0 == @@ -112,12 +133,89 @@ MechWeapon::AttributeIndexSet& MechWeapon::SharedData MechWeapon::DefaultData( &MechWeapon::ClassDerivations, - MechWeapon::MessageHandlers, + MechWeapon::GetMessageHandlers(), MechWeapon::GetAttributeIndex(), MechWeapon::StateCount ); +//############################################################################# +// The CONFIG-MODE session handlers (task #6) +// + +// +// @004b9550 -- id 9 "ConfigureMappables": the press-and-HOLD session bracket. +// dataContents = +-(element+1) (press/release, the L4 button value convention). +// Press: open the session on THIS weapon -- the roster-slot-0 mapper flips the +// global mode NonMapping->Mapping, latches &fireImpulse as the active configure +// target, and temp-maps the fire buttons to send ChooseButton (id 10) here. +// Release: close the session (mode restored, temp layer stripped). +// The +0x110 write (0 open / -1 idle) is the binary's config-session flag +// (ctor default -1, part_012.c:15775); the port field carries the older name +// vitalSubsystemIndex -- no confirmed reader yet (read-site survey pending). +// +void + MechWeapon::ConfigureMappablesMessageHandler( + ReceiverDataMessageOf *message + ) +{ + Check(this); + Check_Pointer(message); + + int value = message->dataContents; // +-(element+1) + int element = ((value < 0) ? -value : value) - 1; + + MechControlsMapper *mapper = ((Mech *)owner)->MappingMapper(); // **(mech+0x128) + if (mapper == 0) + { + return; // (binary assumes slot 0 is live) + } + + if (value < 1) + { + mapper->ExitConfiguration(element); // vtbl+0x3C + vitalSubsystemIndex = -1; // this+0x110 -> idle + } + else + { + vitalSubsystemIndex = 0; // this+0x110 -> session open + mapper->EnterConfiguration( // vtbl+0x38 + element, + (ControlsButton *)&fireImpulse, // this+0x31C (TriggerState) + this, + ChooseButtonMessageID, // 10 + ConfigureMappablesMessageID, // 9 + 0); // active_message_id (direct bind) + } +} + +// +// @004b95b8 -- id 10 "ChooseButton": while the session is open, a tapped fire +// button sends its RAW 1-based value here; the mapper's direct AddOrErase +// TOGGLES buttonGroup[value-1]'s permanent (0x10000) mapping onto +// &fireImpulse -- join the group if absent, leave it if present. Press only. +// +void + MechWeapon::ChooseButtonMessageHandler( + ReceiverDataMessageOf *message + ) +{ + Check(this); + Check_Pointer(message); + + if (message->dataContents > 0) + { + MechControlsMapper *mapper = ((Mech *)owner)->MappingMapper(); + if (mapper != 0) + { + mapper->AddOrErase( // vtbl+0x44 (direct overload) + (unsigned int)message->dataContents, // RAW 1-based + (ControlsButton *)&fireImpulse); + } + } +} + + //############################################################################# // Firing -- abstract base body // @@ -195,6 +293,12 @@ MechWeapon::MechWeapon( usesExternalModel = (strstr(GetName(), "EXT") != 0) ? True : False; fireImpulse = 0.0f; // 0x31C + // task #6: the binary MechWeapon ctor defaults its controls-destination + // pass-through to &fireImpulse when the caller passes NULL (@004b99a8 + // `if (param_7==0) param_7 = this+0x31C`); the chain stores it at + // MechSubsystem+0xE8/+0xEC where ConfigMapGauge's GetMapState reads it. + controlDestination = (void *)&fireImpulse; // 0xE8 + controlMessageID = 0; // 0xEC rechargeLevel = 1.0f; // 0x320 (fully charged) previousFireImpulse = 0.0f; // 0x3A4 recoil = 0.0f; // 0x3E8 diff --git a/game/reconstructed/mechweap.hpp b/game/reconstructed/mechweap.hpp index c7eef4b..0813bc9 100644 --- a/game/reconstructed/mechweap.hpp +++ b/game/reconstructed/mechweap.hpp @@ -105,10 +105,39 @@ class CockpitHud; // public: static Derivation ClassDerivations; - static Receiver::MessageHandlerSet MessageHandlers; + // task #6: the binary registers TWO handlers ({9,"ConfigureMappables"}, + // {10,"ChooseButton"} -- table @0x511860); the old empty static set + // meant no pilot input could ever reach the config session. The + // accessor idiom (function-local static) dodges cross-TU init order. + static const Receiver::HandlerEntry MessageHandlerEntries[]; + static Receiver::MessageHandlerSet& GetMessageHandlers(); static AttributeIndexSet AttributeIndex; static SharedData DefaultData; + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Messaging Support -- the CONFIG-MODE session (task #6) + // + // Ids PINNED to the binary table @0x511860 (PoweredSubsystem owns 4-8, + // the generator-select handlers). The pilot HOLDS a weapon's authored + // configure button (a type-6 EventMapping, id 9, under the MFD-quadrant + // aux-panel mode masks) to open the session; taps fire buttons to toggle + // this weapon's group membership (id 10, temp-mapped by + // EnterConfiguration); releases to close. + // + public: + enum { + ConfigureMappablesMessageID = 9, // @004b9550 (was mislabeled a Myomers fn) + ChooseButtonMessageID = 10 // @004b95b8 + }; + void + ConfigureMappablesMessageHandler( // @004b9550 + ReceiverDataMessageOf *message + ); + void + ChooseButtonMessageHandler( // @004b95b8 + ReceiverDataMessageOf *message + ); + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Attribute Support (gauge data-binding wave) // @@ -122,8 +151,14 @@ class CockpitHud; // public: enum { - // TriggerState is the BINARY's only MechWeapon attribute - // ({0x13, "TriggerState", +0x31d} @0x511890 -- fireImpulse@0x31C). + // TriggerState ({0x13, "TriggerState"} @0x511890 -- fireImpulse + // @0x31C). task #6 CORRECTION: the binary MechWeapon attribute + // table has ELEVEN entries, not one -- 0x12 PercentDone@0x320, + // 0x14 DistanceToTarget@0x324, 0x15 TargetWithinRange@0x34C, + // 0x16 WeaponRange@0x328(=effectiveRange), 0x17 PipPosition, + // 0x18 PipColor, 0x19 PipExtendedRange, 0x1A EstimatedReadyTime + // @0x330, 0x1B RearFiring@0x334, 0x1C WeaponState@0x350 (future + // gauge-wave feeds). The pinning below still holds: // The streamed per-mech controls maps (type-6 resources) bind the // fire buttons DIRECTLY onto it by this NUMERIC id, so it is // PINNED to the binary value: the binary's subsystem chain reaches diff --git a/game/reconstructed/mislanch.cpp b/game/reconstructed/mislanch.cpp index 956be96..ad25f6c 100644 --- a/game/reconstructed/mislanch.cpp +++ b/game/reconstructed/mislanch.cpp @@ -86,13 +86,20 @@ Derivation "MissileLauncher" ); -Receiver::MessageHandlerSet MissileLauncher::MessageHandlers; +// task #6: inherit ProjectileWeapon's handler set (see emitter.cpp note). +Receiver::MessageHandlerSet& + MissileLauncher::GetMessageHandlers() +{ + static Receiver::MessageHandlerSet messageHandlers( + ProjectileWeapon::GetMessageHandlers()); // copy-inherit + return messageHandlers; +} Simulation::AttributeIndexSet MissileLauncher::AttributeIndex; MissileLauncher::SharedData MissileLauncher::DefaultData( &MissileLauncher::ClassDerivations, - MissileLauncher::MessageHandlers, + MissileLauncher::GetMessageHandlers(), MechWeapon::GetAttributeIndex(), // gauge wave: inherit temps/InputVoltage/OutputVoltage/PercentDone (was empty) MissileLauncher::StateCount ); diff --git a/game/reconstructed/mislanch.hpp b/game/reconstructed/mislanch.hpp index d21782b..3456ba5 100644 --- a/game/reconstructed/mislanch.hpp +++ b/game/reconstructed/mislanch.hpp @@ -75,7 +75,7 @@ class Missile; // public: static Derivation ClassDerivations; - static Receiver::MessageHandlerSet MessageHandlers; + static Receiver::MessageHandlerSet& GetMessageHandlers(); // inherits ProjectileWeapon's (task #6) static AttributeIndexSet AttributeIndex; static SharedData DefaultData; diff --git a/game/reconstructed/myomers.cpp b/game/reconstructed/myomers.cpp index 145ab60..18ac5c0 100644 --- a/game/reconstructed/myomers.cpp +++ b/game/reconstructed/myomers.cpp @@ -497,7 +497,7 @@ void Myomers::ConnectToMover(SubsystemMessage &message) //*************************************************************************** -// Myomers::DisconnectFromMover @004b95b8 +// Myomers::DisconnectFromMover (stand-in; no binary address) //*************************************************************************** // Counterpart of ConnectToMover: detaches the SpeedEffect feed. // if (message->value > 0) diff --git a/game/reconstructed/powersub.cpp b/game/reconstructed/powersub.cpp index 695fdb3..228aa87 100644 --- a/game/reconstructed/powersub.cpp +++ b/game/reconstructed/powersub.cpp @@ -999,6 +999,11 @@ void } if (heatAlarm.GetLevel() == HeatSink::FailureHeat) // this+0x184 == 2 { + // The THERMAL BREAKER: output collapses to 0 until the generator cools + // past NormalHeat and re-spins (task #11 diag below shows the duty cycle). + if (stateAlarm.GetLevel() != GeneratorRecovered && getenv("BT_HEAT_LOG")) + DEBUG_STREAM << "[gen] " << GetName() << " TRIPPED (T=" + << currentTemperature << ")" << std::endl; stateAlarm.SetLevel(GeneratorRecovered); // SetLevel 4 } @@ -1038,6 +1043,9 @@ void } else { + if (getenv("BT_HEAT_LOG")) + DEBUG_STREAM << "[gen] " << GetName() << " RESTARTING (T=" + << currentTemperature << ")" << std::endl; startTimer = 0.0f; stateAlarm.SetLevel(GeneratorStarting); // 0 } diff --git a/game/reconstructed/projweap.cpp b/game/reconstructed/projweap.cpp index 5ef7011..96860d3 100644 --- a/game/reconstructed/projweap.cpp +++ b/game/reconstructed/projweap.cpp @@ -123,13 +123,20 @@ Derivation "ProjectileWeapon" // DAT_00512280 ); -Receiver::MessageHandlerSet ProjectileWeapon::MessageHandlers; +// task #6: inherit MechWeapon's handler set (see emitter.cpp note). +Receiver::MessageHandlerSet& + ProjectileWeapon::GetMessageHandlers() +{ + static Receiver::MessageHandlerSet messageHandlers( + MechWeapon::GetMessageHandlers()); // copy-inherit + return messageHandlers; +} Simulation::AttributeIndexSet ProjectileWeapon::AttributeIndex; ProjectileWeapon::SharedData ProjectileWeapon::DefaultData( &ProjectileWeapon::ClassDerivations, - ProjectileWeapon::MessageHandlers, + ProjectileWeapon::GetMessageHandlers(), MechWeapon::GetAttributeIndex(), // gauge wave: inherit temps/InputVoltage/OutputVoltage/PercentDone (was empty) ProjectileWeapon::StateCount ); diff --git a/game/reconstructed/projweap.hpp b/game/reconstructed/projweap.hpp index 6d54876..17f371c 100644 --- a/game/reconstructed/projweap.hpp +++ b/game/reconstructed/projweap.hpp @@ -102,7 +102,7 @@ class NotationFile; public: static Derivation ClassDerivations; // parent = MechWeapon::ClassDerivations, // name = "ProjectileWeapon" (DAT_00512280) - static Receiver::MessageHandlerSet MessageHandlers; + static Receiver::MessageHandlerSet& GetMessageHandlers(); // inherits MechWeapon's (task #6) static AttributeIndexSet AttributeIndex; static SharedData DefaultData;