diff --git a/restoration/source410/BT/EMITTER.CPP b/restoration/source410/BT/EMITTER.CPP index 996c5236..d16a357a 100644 --- a/restoration/source410/BT/EMITTER.CPP +++ b/restoration/source410/BT/EMITTER.CPP @@ -27,13 +27,15 @@ Derivation // //############################################################################# -// Attribute Support. Emitter publishes the beam charge level (ChargeLevel): -// the HUD weapon-charge gauge binds to it, and -- load-bearing -- GaussRifle's -// AttributeIndex chains THIS index (GAUSS.CPP), and PPC::DefaultData binds it -// via the inherited PPC::AttributeIndex. It MUST be a real defined index -// chained to Subsystem::AttributeIndex, not the bare base (a declared-but- +// Attribute Support. Emitter publishes the beam charge level (ChargeLevel, at +// the authentic 0x1D past the MechWeapon table end): the HUD weapon-charge +// gauge binds it, and -- load-bearing -- GaussRifle's AttributeIndex chains +// THIS index (GAUSS.CPP), and PPC::DefaultData binds it via the inherited +// PPC::AttributeIndex. It MUST be a real defined index (a declared-but- // undefined Emitter::AttributeIndex leaves activeAttributeIndex NULL and -// faults the first GetAttributePointer on any PPC/Gauss). +// faults the first GetAttributePointer on any PPC/Gauss). Chained to the +// MechWeapon index so every energy weapon exposes the full weapon table +// (TriggerState / PercentDone / ...). //############################################################################# // const Emitter::IndexEntry @@ -46,13 +48,13 @@ Emitter::AttributeIndexSet Emitter::AttributeIndex( ELEMENTS(Emitter::AttributePointers), Emitter::AttributePointers, - Subsystem::AttributeIndex + MechWeapon::AttributeIndex ); Emitter::SharedData Emitter::DefaultData( Emitter::ClassDerivations, - Subsystem::MessageHandlers, + MechWeapon::MessageHandlers, Emitter::AttributeIndex, Subsystem::StateCount ); @@ -70,6 +72,16 @@ Emitter::Emitter( chargeLevel = 0.0f; dischargeTime = subsystem_resource->dischargeTime; + dischargeTimer = 0.0f; + + // + // Install the beam-weapon fire state machine (a replicant copy is driven by + // console updates / ServiceDischarge instead, still staged). + // + if (owner->GetInstance() != Entity::ReplicantInstance) + { + SetPerformance(&Emitter::EmitterSimulation); + } Check_Fpu(); } @@ -92,14 +104,148 @@ Logical // //############################################################################# -// FireWeapon The energy-beam discharge. Not yet reconstructed (the fire path -// is exercised in combat, past the current ctor frontier). +// FireWeapon The energy-beam discharge (PARTIAL -- binary @004bace8). The +// authentic body: re-arm the beam-on countdown, compute the per-shot damage / +// heat from the charge energy (0.5*V^2*EC closed forms), dump the heat into +// the inherited thermal accumulator, spend the charge, build the beam +// (muzzle -> target) and submit the Damage record at the owner's target. +// The energy/heat algebra needs the electrical charge model (TrackSeekVoltage +// / seekVoltage / generator -- the powersub wave), and the beam/damage need +// the targeting slot + renderer. This partial performs the DISCHARGE +// bookkeeping -- countdown re-arm, charge spent, recoil loaded so the recharge +// dial animates -- so the fire state machine runs end-to-end. //############################################################################# // void Emitter::FireWeapon() { - Fail("Emitter::FireWeapon -- emitter.cpp not yet reconstructed"); + Check(this); + + dischargeTimer = dischargeTime; + chargeLevel = 0.0f; + recoil = rechargeRate; // full recoil -> dial 0, decays in Loading + ComputeOutputVoltage(); + + if (getenv("BT_MECH_LOG")) + { + DEBUG_STREAM << "[fire] '" << GetName() + << "' FIRED (discharge=" << dischargeTime + << "s recharge=" << rechargeRate << "s)" << endl << flush; + } +} + +// +//############################################################################# +// ResetFiringState (binary @004ba9a8) -- the beam has finished: drop back to +// Loading so the recharge cycle begins. +//############################################################################# +// +void + Emitter::ResetFiringState() +{ + Check(this); + + weaponAlarm.SetLevel(LoadingState); +} + +// +//############################################################################# +// EmitterSimulation The beam-weapon per-frame fire state machine (binary +// @004baa88). The weapon state is carried in the weapon alarm level: +// 0 = Firing, 2 = Loaded (ready), 3 = Loading, 4 = the trigger-during-load +// blip. PARTIAL: the leading PoweredSubsystem electrical step and the +// destroyed / heat-failure / dead-mech hard gates are deferred with the +// power / heat / damage waves; the Loading charge uses the authored +// RechargeRate seconds directly (recoil decay -> ComputeOutputVoltage dial) +// instead of the TrackSeekVoltage generator integration; the Loaded->Firing +// gate honors viewFireEnable (the look-view arm) -- the HasActiveTarget gate +// joins it with the targeting wave. +// +// DEV hook BT_FORCE_FIRE=1: pulses the trigger whenever the weapon is Loaded +// (press while Loaded, release otherwise), so every armed weapon auto-fires at +// its authored recharge cadence -- the headless fire-cycle verification. +//############################################################################# +// +void + Emitter::EmitterSimulation(Scalar time_slice) +{ + Check(this); + + { + static int forceFire = -1; + if (forceFire < 0) + { + forceFire = (getenv("BT_FORCE_FIRE") != NULL) ? 1 : 0; + } + if (forceFire) + { + fireImpulse = (GetWeaponState() == LoadedState) ? 1.0f : 0.0f; + } + } + + Logical fireEdge = CheckFireEdge(); + + switch (GetWeaponState()) + { + case FiringState: + // + // Count the beam-on timer down; when it expires, drop to Loading. + // + dischargeTimer -= time_slice; + if (dischargeTimer <= 0.0f) + { + ResetFiringState(); + } + break; + + case LoadedState: + if (fireEdge) + { + if (viewFireEnable) + { + weaponAlarm.SetLevel(FiringState); + FireWeapon(); + } + else + { + weaponAlarm.SetLevel(TriggerDuringLoadState); + weaponAlarm.SetLevel(LoadedState); + } + } + break; + + case LoadingState: + if (fireEdge) + { + weaponAlarm.SetLevel(TriggerDuringLoadState); + weaponAlarm.SetLevel(LoadingState); + } + // + // Recharge: decay the recoil over the authored RechargeRate seconds; + // the dial (rechargeLevel) rises 0 -> 1 with it. + // + recoil -= time_slice; + if (recoil <= 0.0f) + { + recoil = 0.0f; + } + ComputeOutputVoltage(); + if (recoil == 0.0f) + { + weaponAlarm.SetLevel(LoadedState); + if (getenv("BT_MECH_LOG")) + { + DEBUG_STREAM << "[fire] '" << GetName() + << "' LOADED" << endl << flush; + } + } + break; + + default: + break; + } + + Check_Fpu(); } // diff --git a/restoration/source410/BT/EMITTER.HPP b/restoration/source410/BT/EMITTER.HPP index 94ccec70..b48bf7cd 100644 --- a/restoration/source410/BT/EMITTER.HPP +++ b/restoration/source410/BT/EMITTER.HPP @@ -48,11 +48,12 @@ static SharedData DefaultData; //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Attribute Support + // Attribute Support. The energy-weapon family publishes past the MechWeapon + // table end (0x1C) -- the binary's emitter IDs run 0x1D+. // public: enum { - ChargeLevelAttributeID = Subsystem::NextAttributeID, + ChargeLevelAttributeID = MechWeapon::NextAttributeID, // 0x1D NextAttributeID }; @@ -102,12 +103,31 @@ void FireWeapon(); + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Per-frame simulation (the beam-weapon fire state machine). + // + public: + typedef void + (Emitter::*Performance)(Scalar time_slice); + void + SetPerformance(Performance performance) + { + Check(this); + activePerformance = (Simulation::Performance)performance; + } + + void + EmitterSimulation(Scalar time_slice); + void + ResetFiringState(); + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Local Data // protected: Scalar chargeLevel; Scalar dischargeTime; + Scalar dischargeTimer; }; #endif diff --git a/restoration/source410/BT/MECHWEAP.CPP b/restoration/source410/BT/MECHWEAP.CPP index 108a6163..7851dbb0 100644 --- a/restoration/source410/BT/MECHWEAP.CPP +++ b/restoration/source410/BT/MECHWEAP.CPP @@ -48,11 +48,69 @@ MechWeapon::MessageHandlerSet Subsystem::MessageHandlers ); +// +//############################################################################# +// Attribute Support -- the 1995 binary table (@0x511890), IDs pinned so the +// streamed per-mech control mappings resolve: TriggerState (0x13) is what the +// authored fire buttons bind (the controls push then writes the trigger state +// straight into fireImpulse each frame). The pads bridge our chain gap +// 2..0x11 (parents publish nothing yet) -- REQUIRED, because Build leaves +// uncovered gap slots uninitialized; they target rechargeLevel, harmless if a +// stray mapping ever binds one (BT411 shipped the same). Pip*/WeaponRange +// pads-to-rechargeLevel where the member isn't reconstructed yet are noted. +//############################################################################# +// +#define MECHWEAP_PAD(n, name) \ + { (int)MechWeapon::MechWeaponPadFirstAttributeID + n, name, \ + (Simulation::AttributePointer)&MechWeapon::rechargeLevel } + +const MechWeapon::IndexEntry + MechWeapon::AttributePointers[]= +{ + MECHWEAP_PAD( 0, "MechWeaponPad02"), + MECHWEAP_PAD( 1, "MechWeaponPad03"), + MECHWEAP_PAD( 2, "MechWeaponPad04"), + MECHWEAP_PAD( 3, "MechWeaponPad05"), + MECHWEAP_PAD( 4, "MechWeaponPad06"), + MECHWEAP_PAD( 5, "MechWeaponPad07"), + MECHWEAP_PAD( 6, "MechWeaponPad08"), + MECHWEAP_PAD( 7, "MechWeaponPad09"), + MECHWEAP_PAD( 8, "MechWeaponPad0A"), + MECHWEAP_PAD( 9, "MechWeaponPad0B"), + MECHWEAP_PAD(10, "MechWeaponPad0C"), + MECHWEAP_PAD(11, "MechWeaponPad0D"), + MECHWEAP_PAD(12, "MechWeaponPad0E"), + MECHWEAP_PAD(13, "MechWeaponPad0F"), + MECHWEAP_PAD(14, "MechWeaponPad10"), + MECHWEAP_PAD(15, "MechWeaponPad11"), + ATTRIBUTE_ENTRY(MechWeapon, PercentDone, rechargeLevel), // 0x12 + ATTRIBUTE_ENTRY(MechWeapon, TriggerState, fireImpulse), // 0x13 + ATTRIBUTE_ENTRY(MechWeapon, DistanceToTarget, rangeToTarget), // 0x14 + ATTRIBUTE_ENTRY(MechWeapon, TargetWithinRange, targetWithinRange), // 0x15 + ATTRIBUTE_ENTRY(MechWeapon, WeaponRange, effectiveRange), // 0x16 + { (int)MechWeapon::PipPositionAttributeID, "PipPosition", // 0x17 (pip + (Simulation::AttributePointer)&MechWeapon::rechargeLevel }, // members + { (int)MechWeapon::PipColorAttributeID, "PipColor", // 0x18 not yet + (Simulation::AttributePointer)&MechWeapon::rechargeLevel }, // recon- + { (int)MechWeapon::PipExtendedRangeAttributeID, "PipExtendedRange", // 0x19 structed) + (Simulation::AttributePointer)&MechWeapon::rechargeLevel }, + ATTRIBUTE_ENTRY(MechWeapon, EstimatedReadyTime, estimatedReadyTime),// 0x1A + ATTRIBUTE_ENTRY(MechWeapon, RearFiring, rearFiring), // 0x1B + ATTRIBUTE_ENTRY(MechWeapon, WeaponState, weaponAlarm) // 0x1C +}; + +MechWeapon::AttributeIndexSet + MechWeapon::AttributeIndex( + ELEMENTS(MechWeapon::AttributePointers), + MechWeapon::AttributePointers, + Subsystem::AttributeIndex + ); + MechWeapon::SharedData MechWeapon::DefaultData( MechWeapon::ClassDerivations, MechWeapon::MessageHandlers, - Subsystem::AttributeIndex, + MechWeapon::AttributeIndex, Subsystem::StateCount ); @@ -77,6 +135,19 @@ MechWeapon::MechWeapon( damageData.damageType = damageType; damageData.damageAmount = damageAmount; + // + // Fire-machine state: spawn fully charged and Loaded (ready to fire). + // + fireImpulse = 0.0f; + previousFireImpulse = 0.0f; + rechargeLevel = 1.0f; + rangeToTarget = 0.0f; + effectiveRange = weaponRange; + estimatedReadyTime = 0; + recoil = 0.0f; + weaponAlarm.Initialize(WeaponStateCount); + weaponAlarm.SetLevel(LoadedState); + // // REAR-FIRING resolve (the binary ctor tail @004b99a8): a weapon is // rear-mounted when its MOUNT SEGMENT's site name carries the 'b' (back) @@ -129,6 +200,47 @@ Logical return True; } +// +//############################################################################# +// CheckFireEdge -- the rising-edge detector on fireImpulse (binary @004b9608). +// TriggerState (attr 0x13) carries the raw ControlsButton INT bit-copied by the +// streamed direct mapping (+(button+1) press / -(button+1) release), so the +// release value reads as a negative NaN in float terms; the binary's x87 +// compare effectively treated it as "released". Comparing the BIT PATTERNS as +// signed ints is sign-correct for the button ints AND for genuine float values +// (0.0f = 0x0, positives > 0, negatives/NaNs < 0), reproducing the binary's +// behavior exactly. +//############################################################################# +// +Logical + MechWeapon::CheckFireEdge() +{ + Check(this); + + int current = *(int *)&fireImpulse; + int previous = *(int *)&previousFireImpulse; + Logical edge = (current > 0 && previous <= 0) ? True : False; + previousFireImpulse = fireImpulse; + return edge; +} + +// +//############################################################################# +// ComputeOutputVoltage -- the recharge-dial writer (binary @004b9c9c, vtable +// slot the Emitter overrides with its own voltage form): +// rechargeLevel = (rechargeRate - recoil) / rechargeRate +// The Loading tick decays recoil from rechargeRate toward 0, so the dial rises +// 0 -> 1 over the authored RechargeRate seconds. +//############################################################################# +// +void + MechWeapon::ComputeOutputVoltage() +{ + Check(this); + + rechargeLevel = (rechargeRate - recoil) / rechargeRate; +} + Logical MechWeapon::TestInstance() const { diff --git a/restoration/source410/BT/MECHWEAP.HPP b/restoration/source410/BT/MECHWEAP.HPP index b04e8ecc..b71b25b9 100644 --- a/restoration/source410/BT/MECHWEAP.HPP +++ b/restoration/source410/BT/MECHWEAP.HPP @@ -23,6 +23,10 @@ # include # endif +# if !defined(ALARM_HPP) +# include +# endif + //##################### Forward Class Declarations ####################### class Mech; @@ -60,6 +64,37 @@ static SharedData DefaultData; static MessageHandlerSet MessageHandlers; + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Attribute Support. The real IDs are PINNED to the 1995 binary numbering + // (table @0x511890, ids 0x12..0x1C): the streamed per-mech control mappings + // reference them by NUMBER -- TriggerState (0x13) is the fire-button binding. + // Our parent chain publishes nothing past Subsystem::NextAttributeID (2) + // yet, so pads bridge the gap 2..0x11 (they shrink to the authentic + // 0x0F..0x11 once the mechsub/heat/powersub attribute waves publish -- + // SENSOR.HPP pins PoweredSubsystem::NextAttributeID at 0x0F). The pads are + // LOAD-BEARING: AttributeIndexSet::Build leaves unlisted gap slots as + // uninitialized garbage, so every ID up to the max must be covered. + // + public: + enum { + MechWeaponPadFirstAttributeID = Subsystem::NextAttributeID, // 2 + PercentDoneAttributeID = 0x12, + TriggerStateAttributeID, // 0x13 -- the streamed fire-button binding + DistanceToTargetAttributeID, // 0x14 + TargetWithinRangeAttributeID, // 0x15 + WeaponRangeAttributeID, // 0x16 + PipPositionAttributeID, // 0x17 + PipColorAttributeID, // 0x18 + PipExtendedRangeAttributeID, // 0x19 + EstimatedReadyTimeAttributeID, // 0x1A + RearFiringAttributeID, // 0x1B + WeaponStateAttributeID, // 0x1C (binary table end) + NextAttributeID // 0x1D (the Emitter family starts here) + }; + + static const IndexEntry AttributePointers[]; + static AttributeIndexSet AttributeIndex; + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Test Class Support // @@ -121,6 +156,32 @@ void SetViewFireEnable(Logical enable){ Check(this); viewFireEnable = enable; } + // + // Fire state machine. The weapon state is carried in the weapon alarm + // level: 0 = Firing, 2 = Loaded (ready), 3 = Loading, 4 = the + // trigger-during-load blip. CheckFireEdge is the rising-edge detector + // on fireImpulse (binary @004b9608): TriggerState carries the raw + // ControlsButton INT bit-copied by the streamed direct mapping, so the + // compare works on the BIT PATTERNS as signed ints (sign-correct for + // the button ints AND genuine floats), reproducing the binary's x87 + // semantics. ComputeOutputVoltage (@004b9c9c) is the recharge-dial + // writer. + // + enum { + FiringState = 0, + LoadedState = 2, + LoadingState = 3, + TriggerDuringLoadState = 4, + WeaponStateCount = 5 + }; + + unsigned + GetWeaponState() { Check(this); return weaponAlarm.GetLevel(); } + Logical + CheckFireEdge(); + void + ComputeOutputVoltage(); + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Local Data // @@ -134,6 +195,19 @@ Damage damageData; Logical rearFiring; Logical viewFireEnable; + // + // Fire-machine state (binary @0x31C/0x320/0x324/0x328/0x330/0x350/ + // 0x3A4/0x3E8): the trigger sample pair, the recharge dial, targeting + // ranges, and the weapon-state alarm. + // + Scalar fireImpulse; + Scalar previousFireImpulse; + Scalar rechargeLevel; + Scalar rangeToTarget; + Scalar effectiveRange; + int estimatedReadyTime; + Scalar recoil; + AlarmIndicator weaponAlarm; }; #endif diff --git a/restoration/source410/BT/MECHWEAP.NOTES.md b/restoration/source410/BT/MECHWEAP.NOTES.md index c19d3262..f82a75c2 100644 --- a/restoration/source410/BT/MECHWEAP.NOTES.md +++ b/restoration/source410/BT/MECHWEAP.NOTES.md @@ -18,14 +18,63 @@ rechargeRate / weaponRange / damage / heatCostToFire (+ pip fields). view change (see MECH.NOTES.md increment 7). Verified: the TEST.EGG mech's two rear lasers (ERMLaser_2/3) arm only in LOOK-BACK. +## Fire path (2026-07-21, Phase 5.3.8) + +- **The authentic attribute table IS published** (binary @0x511890, IDs pinned): + PercentDone 0x12 → rechargeLevel, **TriggerState 0x13 → fireImpulse** (the + streamed per-mech fire-button mappings bind this ID — the real trigger + wiring), DistanceToTarget 0x14, TargetWithinRange 0x15, WeaponRange 0x16 → + effectiveRange, EstimatedReadyTime 0x1A, RearFiring 0x1B, WeaponState 0x1C → + weaponAlarm. Pads bridge our chain gap 2..0x11 (parents publish nothing yet; + they shrink to the authentic 0x0F..0x11 when the mechsub/heat/powersub + attribute waves land — SENSOR.HPP pins PoweredSubsystem::NextAttributeID at + 0x0F). **Pads are LOAD-BEARING**: AttributeIndexSet::Build leaves uncovered + gap slots as UNINITIALIZED GARBAGE (`new IndexEntry[entryCount]`, only + inherited + listed entries written), so every ID up to the max must be + covered or Find() on a gap ID returns a wild pointer. Pip* (0x17-0x19) are + pads → rechargeLevel until the pip members are reconstructed. +- **Fire state machine members**: fireImpulse/previousFireImpulse (the trigger + sample pair), rechargeLevel, rangeToTarget, effectiveRange, + estimatedReadyTime, recoil, weaponAlarm (the state carrier: 0 Firing / + 2 Loaded / 3 Loading / 4 trigger-during-load). Spawn = charged + Loaded. +- **CheckFireEdge** (@004b9608): rising-edge on fireImpulse comparing BIT + PATTERNS as signed ints — TriggerState carries raw ControlsButton ints + (+(button+1) press / −(button+1) release); the release bit-pattern is a + negative NaN as float, and an IEEE float compare would latch the detector + shut after the first release (BT411 task #8). Int compare reproduces the + binary's x87 semantics exactly. +- **ComputeOutputVoltage** (@004b9c9c): `rechargeLevel = (rechargeRate − + recoil) / rechargeRate` — the recharge dial. +- **Emitter::EmitterSimulation** (PARTIAL, @004baa88) installed on every + non-replicant energy weapon: Firing → discharge countdown → + ResetFiringState(Loading); Loaded + trigger edge → viewFireEnable gate → + FireWeapon; Loading → recoil decays over the authored RechargeRate seconds → + Loaded. Deferred within it: the leading PoweredSubsystemSimulation electrical + step, the destroyed/heat-failure/dead-mech hard gates, the TrackSeekVoltage + generator charge integration (the partial uses the authored seconds + directly), and the HasActiveTarget gate (targeting wave). +- **Emitter::FireWeapon** (PARTIAL, @004bace8): discharge bookkeeping (beam + countdown re-arm, charge spent, recoil loaded). Deferred: the 0.5·V²·EC + energy/damage/heat closed forms (need the electrical model), the heat dump + (heat wave), the beam build + Damage submission (targeting + renderer). +- Emitter's index re-chained to MechWeapon's (ChargeLevel at the authentic + 0x1D past the table end); Emitter::DefaultData now binds + MechWeapon::MessageHandlers. +- DEV hook `BT_FORCE_FIRE=1`: pulses the trigger whenever a weapon is Loaded → + every armed weapon auto-fires at its authored cadence. + +**VERIFIED headlessly**: forward view — PPC_1/2 + ERMLaser_1 cycle +FIRED→LOADED→FIRED at the authored 0.6 s discharge / 1 s recharge, rear lasers +silent; `BT_FORCE_LOOK=3` (look-behind) — ONLY ERMLaser_2/3 fire. The full +view-fire × fire-FSM interlock holds in both directions. Zero Fail. (SRM6s are +MissileLaunchers — the ballistic ProjectileWeaponSimulation is a later +increment.) + ## Still staged -- `FireWeapon` (abstract here; the concrete fire paths in Emitter/PPC/Gauss/ - MissileLauncher are the combat wave), `SendDamage` (damage/networking), - `CreateStreamedSubsystem` (tool-side model build). -- The 1995 MechWeapon attribute table (RearFiring @0x1B, EstimatedReadyTime, - ...) is not yet published — our DefaultData still uses the base - Subsystem::AttributeIndex. Comes with the weapon-gauge/console wave. +- The ballistic fire path (ProjectileWeaponSimulation @004bbd04, missile + launch/seeker), `SendDamage` (damage/networking), `CreateStreamedSubsystem` + (tool-side model build). - ~~`MechWeapon::MessageHandlers` declared but not defined~~ — **FIXED 2026-07-21**: it is now a real defined set (`(0, NULL, Subsystem::MessageHandlers)` — no own handlers yet, inherits the chain), and