BT411 task-#62 truth: the arcade fires ONE cluster Missile per salvo and its zone damage lands EXACTLY ONCE (burstCount feeds only the gyro bounce + splash falloff, never the zone armor formula). The 5.3.16 per-missile delivery loop was ~6x too lethal. MISLANCH now sends a single salvo-split damage message (verified 1:1 FIRED-to-damage in the mutual fight; zero exceptions). The cluster splash joins the Missile entity wave. Also: BT_FORCE_ZONE=n dev hook (pin every hit -- the cascade verification knob) landed with 5.3.17's runs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
10 KiB
MECHWEAP.CPP / .HPP — reconstruction notes
MechWeapon (: PoweredSubsystem) is the weapon base: PPC/GaussRifle bind it via
Emitter, the SRMs via ProjectileWeapon/MissileLauncher. The resource streams
rechargeRate / weaponRange / damage / heatCostToFire (+ pip fields).
Reconstructed
- Statics (ClassDerivations, DefaultData on Subsystem::MessageHandlers/ AttributeIndex), ctor streaming the tuning fields, damageData prime.
- View-fire gating (2026-07-21, the look-commit wave): real
rearFiring/viewFireEnablemembers (1995 binary @0x334/@0x3E0) +IsRearFiring()/GetViewFireEnable()/SetViewFireEnable(). The ctor resolves REAR-FIRING authentically (binary ctor tail @004b99a8): the mount SEGMENT site name (owner->GetSegment(GetSegmentIndex())) is tested for the 'b' (back) marker — only the back gun ports (sitelbgunport/siterbgunport) carry it. Spawn state = armed for the forward view.Mech::CommitLookStatere-arms on every 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.)
Ballistic fire path (2026-07-21, Phase 5.3.9)
- ProjectileWeaponSimulation (PARTIAL, binary @004bbd04 — FULLY RECOVERED in the BT411 RE) installed on every non-replicant ballistic weapon. The authentic FSM shape: one trigger-edge sample before the gates; gate 2 (the linked AmmoBin dry → pin NoAmmo(7) every frame); Loaded(2): edge → viewFireEnable gate → the ammo pull happens in the CALLER (a failed pull stays Loaded silently; a denied shot does the 4→2 DENIAL BLIP with no ammo pulled — early-returning gates from FireWeapon while the caller cycles the alarm is exactly the 1995 "denied shot fakes a full firing cycle" defect); granted → Firing(0) → FireWeapon → Loading(3) (or NoAmmo if the pull emptied the bin), recoil = rechargeRate; Loading(3): recoil bleeds → 0 → Loaded when a round is chambered, ComputeOutputVoltage animates the dial; Jammed(5) held at full recoil; NoAmmo(7) is the roach-motel (re-asserts unconditionally; only a mission reset leaves it). Deferred inside: the leading electrical step, gate 1 (destroyed/FailureHeat/mech-disabled), magazine eject, the electrical-Ready recoil gate, the heat-scaled jam roll, HasActiveTarget.
- AmmoBin feed interface:
GetAmmoState()(1 round-ready / 2 EMPTY),FeedAmmo()(pull one round; 0 when dry). Feed pacing + cook-off join the heat wave. - ammoBinLink wiring: the launcher ctor resolves the bin from the
resource's
ammoBinIndex(the bin's roster slot) via the owner's subsystem table — WIRE-VERIFIED: the raw stream carries 27/29, exactly the AmmoBinSRM6_1/2 slots. - Resource alignment (wire-verified): a raw-stream dump showed the
ProjectileWeapon field block sits +3 ints past our staged struct — the
MechWeapon resource ancestry runs three ints short (the pip-family fields,
not yet broken out).
resourceAlignPad[3]re-aligns it; the phantom muzzleVelocity/missileCount tail fields were dropped (BT411's verified overlay ends at minJamChance; missileCount belongs to MissileLauncher's own extension and now reads its true 6). The full resource-overlay verification (every ancestor struct byte-checked) is its own wave. - MissileLauncher::FireWeapon PARTIAL (@004bcc60: authentically heat + the missileCount-salvo Missile spawn ONLY — spawn needs the entity/targeting waves).
VERIFIED headlessly (BT_FORCE_FIRE): bins resolve with the authored 24 rounds; SRM6s fire 6-missile salvos and reload at the authored 1 s cadence, rounds counting 24→0; after the 24th salvo the launcher goes DRY and stays silent across ~3,000 subsequent fire events while the energy weapons keep cycling — the NoAmmo roach-motel holds. Zero Fail.
Still staged
- The Missile entity spawn/seeker,
SendDamage(damage/networking),CreateStreamedSubsystem(tool-side model build), the jam roll (CheckForJam), magazine eject. — FIXED 2026-07-21: it is now a real defined set (MechWeapon::MessageHandlersdeclared but not defined(0, NULL, Subsystem::MessageHandlers)— no own handlers yet, inherits the chain), andMechWeapon::DefaultDatabinds it. This mattered because the surviving CODE PPC.CPP binds the inherited name (PPC::MessageHandlers) into PPC::DefaultData, and a declared-but-undefined static links as a zero-filled common block — the same silent-NULL trap as the Emitter::AttributeIndex crash. Real weapon handlers (fire/damage messages) come with the combat wave (grow the table in place).
5.3.16 — targeting + damage delivery
HasActiveTarget()= owner's target slot non-NULL (binary owner+0x388);UpdateTargeting()(@004b9bdc) refreshes rangeToTarget/targetWithinRange fromtarget->localOriginevery FSM tick (both weapon families call it right after the trigger sample). The Loaded→Firing gate is now the authenticviewFireEnable && HasActiveTarget()— a denied pull is the one-frame blip.SendDamage(target, damage): builds the engine 5-argEntity::TakeDamageMessage(inflictor = our mech's EntityID) and Dispatches AT the target; the engine handler routesdamageZones[zone]->TakeDamage. INTERIM zone pick =Random(target->damageZoneCount)— the authentic resolvers are the reticle aimed hit and the cylinder hit-location table (unaimed, zone −1 + the Mech TakeDamage override), both later waves.- Salvo semantics (BT411-verified): burstCount is NOT in the zone armor formula — one message = one amount×scale application. MissileLauncher therefore dispatches ONE message PER MISSILE (each carrying the ctor's salvo-split amount, each on its own random zone); a single split message would under-deliver the salvo by the round count. Interim all-rounds-hit until the Missile entity flight/seeker wave.
- Live-verified (two-mech fight): PPC 11.77/hit (12×chargeRatio², type 4 Energy), ER-M 3.43 (type 3 Laser), SRM6 5.83×6 (type 2 Explosive); repeat hits climb linearly; zones reach 1.0 Burning/Destroyed; no-target runs hold fire (0 FIRED across a 65s force-fire run without a target).
5.3.18a — salvo economy CORRECTED (BT411 task #62)
The 5.3.16 "one message per missile" loop was ~missileCount-times too lethal. The arcade fires ONE cluster Missile per trigger and its zone damage lands EXACTLY ONCE: damageData carries the ctor's salvo-split per-missile amount with burstCount=missileCount (burstCount feeds only gyro bounce + splash falloff — never the zone formula). MISLANCH now dispatches a single SendDamage per salvo (verified 1:1 FIRED:[dmg]). The cluster SPLASH component arrives with the Missile entity wave (proximity-fuse detonation).