BT410 Phase 5.3.8: weapon fire path -- authentic attribute table + fire state machine

The energy weapons now run a live fire cycle, and the REAL trigger wiring is in
place: the 1995 MechWeapon attribute table (binary @0x511890) is published with
PINNED IDs, so the streamed per-mech fire-button mappings bind TriggerState
(0x13) -> fireImpulse and the controls push writes the trigger into the weapon.

- MECHWEAP: attribute enum + table (PercentDone 0x12, TriggerState 0x13,
  DistanceToTarget 0x14, TargetWithinRange 0x15, WeaponRange 0x16,
  EstimatedReadyTime 0x1A, RearFiring 0x1B, WeaponState 0x1C). Pads bridge the
  chain gap 2..0x11 -- LOAD-BEARING: AttributeIndexSet::Build leaves uncovered
  gap slots as uninitialized garbage, so every ID up to the max must be covered
  (they shrink to the authentic 0x0F..0x11 when the parent attribute waves land;
  SENSOR.HPP pins PoweredSubsystem::NextAttributeID at 0x0F).
- MECHWEAP: fire-machine members (fireImpulse/previousFireImpulse,
  rechargeLevel, rangeToTarget, effectiveRange, estimatedReadyTime, recoil,
  weaponAlarm 0/2/3/4); CheckFireEdge (@004b9608, BIT-PATTERN int compare --
  TriggerState carries raw ControlsButton ints whose release value is a float
  NaN; IEEE compare would latch the detector shut); ComputeOutputVoltage
  (@004b9c9c recharge dial).
- EMITTER: EmitterSimulation (PARTIAL @004baa88) -- Firing/Loaded/Loading FSM
  on the weapon alarm, viewFireEnable gate at Loaded->Firing, recoil-decay
  recharge over the authored RechargeRate seconds; FireWeapon (PARTIAL
  @004bace8) discharge bookkeeping; ChargeLevel re-pinned to the authentic
  0x1D; index re-chained to MechWeapon's. Deferred: electrical charge model
  (TrackSeekVoltage), heat dump, HasActiveTarget gate, beam/damage submission.
- BT_FORCE_FIRE dev hook: trigger pulse whenever Loaded (auto-fire cadence).

VERIFIED headlessly: forward view -- PPC_1/2 + ERMLaser_1 cycle FIRED->LOADED
at the authored 0.6s/1s cadence, rear lasers silent; BT_FORCE_LOOK=3 -- ONLY
the rear lasers (ERMLaser_2/3) fire. The view-fire x fire-FSM interlock holds
both directions. Zero Fail. (SRM6 ballistic FSM = a later increment.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-21 23:01:57 -05:00
co-authored by Claude Fable 5
parent d422dfffc0
commit 896bbbed4d
5 changed files with 421 additions and 20 deletions
+157 -11
View File
@@ -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();
}
//
+22 -2
View File
@@ -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
+113 -1
View File
@@ -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
{
+74
View File
@@ -23,6 +23,10 @@
# include <damage.hpp>
# endif
# if !defined(ALARM_HPP)
# include <alarm.hpp>
# 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
+55 -6
View File
@@ -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