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>
163 lines
10 KiB
Markdown
163 lines
10 KiB
Markdown
# 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` /
|
||
`viewFireEnable` members (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::CommitLookState` re-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.
|
||
- ~~`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
|
||
`MechWeapon::DefaultData` binds 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
|
||
from `target->localOrigin` every FSM tick (both weapon families call it
|
||
right after the trigger sample). The Loaded→Firing gate is now the authentic
|
||
`viewFireEnable && HasActiveTarget()` — a denied pull is the one-frame blip.
|
||
- `SendDamage(target, damage)`: builds the engine 5-arg
|
||
`Entity::TakeDamageMessage` (inflictor = our mech's EntityID) and Dispatches
|
||
AT the target; the engine handler routes `damageZones[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).
|