diff --git a/restoration/source410/BT/AMMOBIN.HPP b/restoration/source410/BT/AMMOBIN.HPP index 5879ad9d..a541ac42 100644 --- a/restoration/source410/BT/AMMOBIN.HPP +++ b/restoration/source410/BT/AMMOBIN.HPP @@ -51,6 +51,33 @@ int GetAmmoCount() const { return ammoCount; } + // + // The feed interface the ballistic fire path consumes. Ammo states + // (the 1995 feed alarm): 1 = a round is chambered/ready, 2 = EMPTY. + // FeedAmmo pulls one round (the launcher calls it on a granted shot); + // returns 0 when dry. (Feed-rate pacing + the cook-off heat source + // join with the heat wave; the launcher's own recharge already paces + // shots.) + // + enum { + AmmoLoadedState = 1, + AmmoEmptyState = 2 + }; + int + GetAmmoState() const + { Check(this); return (ammoCount > 0) ? AmmoLoadedState : AmmoEmptyState; } + int + FeedAmmo() + { + Check(this); + if (ammoCount <= 0) + { + return 0; + } + --ammoCount; + return 1; + } + public: typedef AmmoBin__SubsystemResource SubsystemResource; diff --git a/restoration/source410/BT/MECHWEAP.HPP b/restoration/source410/BT/MECHWEAP.HPP index b71b25b9..85674dbc 100644 --- a/restoration/source410/BT/MECHWEAP.HPP +++ b/restoration/source410/BT/MECHWEAP.HPP @@ -169,10 +169,14 @@ // enum { FiringState = 0, + DryTriggerState = 1, LoadedState = 2, LoadingState = 3, TriggerDuringLoadState = 4, - WeaponStateCount = 5 + JammedState = 5, + TriggerDuringJamState = 6, + NoAmmoState = 7, + WeaponStateCount = 8 }; unsigned diff --git a/restoration/source410/BT/MECHWEAP.NOTES.md b/restoration/source410/BT/MECHWEAP.NOTES.md index f82a75c2..bed305b7 100644 --- a/restoration/source410/BT/MECHWEAP.NOTES.md +++ b/restoration/source410/BT/MECHWEAP.NOTES.md @@ -70,11 +70,53 @@ 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 ballistic fire path (ProjectileWeaponSimulation @004bbd04, missile - launch/seeker), `SendDamage` (damage/networking), `CreateStreamedSubsystem` - (tool-side model build). +- 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 diff --git a/restoration/source410/BT/MISLANCH.CPP b/restoration/source410/BT/MISLANCH.CPP index 8930db9f..875319ba 100644 --- a/restoration/source410/BT/MISLANCH.CPP +++ b/restoration/source410/BT/MISLANCH.CPP @@ -93,7 +93,21 @@ void void MissileLauncher::FireWeapon() { - Fail("MissileLauncher::FireWeapon -- mislanch.cpp not yet reconstructed"); + Check(this); + + // + // PARTIAL (binary @004bcc60): the authentic body launches missileCount + // Missile entities toward the owner's target (each carrying the salvo-split + // damageData) and dumps the firing heat. The Missile entity spawn needs + // the entity-spawn + targeting waves; the view/target gate, ammo pull and + // recoil all live in the CALLER (ProjectileWeaponSimulation's Loaded case). + // + if (getenv("BT_MECH_LOG")) + { + DEBUG_STREAM << "[fire] '" << GetName() + << "' FIRED (salvo of " << missileCount + << ", recharge=" << rechargeRate << "s)" << endl << flush; + } } // diff --git a/restoration/source410/BT/PROJWEAP.CPP b/restoration/source410/BT/PROJWEAP.CPP index 48d39e83..0cda83bc 100644 --- a/restoration/source410/BT/PROJWEAP.CPP +++ b/restoration/source410/BT/PROJWEAP.CPP @@ -19,6 +19,10 @@ # include #endif +#if !defined(AMMOBIN_HPP) +# include +#endif + Derivation ProjectileWeapon::ClassDerivations( MechWeapon::ClassDerivations, @@ -28,8 +32,8 @@ Derivation ProjectileWeapon::SharedData ProjectileWeapon::DefaultData( ProjectileWeapon::ClassDerivations, - Subsystem::MessageHandlers, - Subsystem::AttributeIndex, + MechWeapon::MessageHandlers, + MechWeapon::AttributeIndex, Subsystem::StateCount ); @@ -60,6 +64,50 @@ ProjectileWeapon::ProjectileWeapon( launchVelocity = Vector3D(0.0f, 0.0f, 0.0f); tracerOrigin = Vector3D(0.0f, 0.0f, 0.0f); + // + // Link the AmmoBin subsystem the resource references by roster index (the + // binary ctor resolves it from the owner's subsystem table and connects the + // embedded link @0x43C). Shipped content always links a bin; log when the + // data doesn't resolve one. + // + { + Subsystem *roster_bin = NULL; + if (subsystem_resource->ammoBinIndex >= 0 + && subsystem_resource->ammoBinIndex < owner->GetSubsystemCount()) + { + roster_bin = owner->GetSubsystem(subsystem_resource->ammoBinIndex); + } + if (roster_bin != NULL) + { + ammoBinLink.Add(roster_bin); + } + if (getenv("BT_MECH_LOG")) + { + DEBUG_STREAM << "[proj] '" << GetName() + << "' ammoBinIndex=" << subsystem_resource->ammoBinIndex + << " -> "; + if (roster_bin != NULL) + { + DEBUG_STREAM << roster_bin->GetName() + << " (rounds=" << ((AmmoBin *)roster_bin)->GetAmmoCount() << ")"; + } + else + { + DEBUG_STREAM << ""; + } + DEBUG_STREAM << endl << flush; + } + } + + // + // Install the ballistic fire state machine (a replicant copy is driven by + // console updates instead). + // + if (owner->GetInstance() != Entity::ReplicantInstance) + { + SetPerformance(&ProjectileWeapon::ProjectileWeaponSimulation); + } + Check_Fpu(); } @@ -80,11 +128,179 @@ Logical } // -// The ballistic discharge (ammo feed, jam roll, tracer, projectile spawn). -// Not yet reconstructed. +//############################################################################# +// FireWeapon The ballistic discharge (PARTIAL -- binary @004bc104). The +// authentic body is heat + projectile/tracer spawn ONLY: the view/target gate, +// the ammo pull and the recoil set all live in the CALLER (the Loaded case of +// ProjectileWeaponSimulation) -- early-returning any gate from here while the +// caller cycles the alarm anyway is exactly the 1995 "denied shot fakes a full +// firing cycle" defect class. The projectile spawn (Missile entities / +// tracer) needs the entity-spawn + targeting waves; MissileLauncher overrides +// this for the salvo launch. +//############################################################################# // void ProjectileWeapon::FireWeapon() { - Fail("ProjectileWeapon::FireWeapon -- projweap.cpp not yet reconstructed"); + Check(this); + + if (getenv("BT_MECH_LOG")) + { + DEBUG_STREAM << "[fire] '" << GetName() + << "' FIRED (ballistic, recharge=" << rechargeRate << "s)" + << endl << flush; + } +} + +// +//############################################################################# +// ProjectileWeaponSimulation The ballistic per-frame fire state machine +// (binary @004bbd04, FULLY RECOVERED in the BT411 RE). The weapon state is +// carried in the weapon alarm: 0/1/4/6 transient audio blips, 2 Loaded, +// 3 Loading, 5 Jammed, 7 unavailable/NoAmmo (the roach-motel: nothing in the +// machine ever leaves it -- only a mission reset does). +// +// PARTIAL: the leading PoweredSubsystemSimulation electrical step, the +// destroyed / FailureHeat / mech-disabled hard gate (gate 1), the magazine +// eject, the electrical-Ready recoil gate, the heat-scaled jam roll and the +// HasActiveTarget half of the fire gate are deferred with the power / heat / +// damage / targeting waves. What runs is authentic in shape: the single +// trigger-edge sample, the dry-bin gate (gate 2), the Loaded ammo-pull -> +// Firing -> Loading cycle with the denial blip, the Loading recoil bleed + +// recharge dial, and the NoAmmo dry-fire blip. +// +// DEV hook BT_FORCE_FIRE=1: pulses the trigger whenever the weapon is Loaded +// (same as the emitter hook) -- every armed weapon auto-fires at its authored +// cadence until its bin runs dry. +//############################################################################# +// +void + ProjectileWeapon::ProjectileWeaponSimulation(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; + } + } + + // + // The trigger edge is sampled ONCE, before the gates. + // + Logical trigger = CheckFireEdge(); + + // + // Gate 2: the linked AmmoBin. Empty (or missing) -> pin unavailable EVERY + // frame while dry. + // + AmmoBin *bin = (AmmoBin *)ammoBinLink.Resolve(); + if (bin != NULL && bin->GetAmmoState() == AmmoBin::AmmoEmptyState) + { + if (getenv("BT_MECH_LOG") && GetWeaponState() != NoAmmoState) + { + DEBUG_STREAM << "[fire] '" << GetName() + << "' -> NoAmmo (bin dry)" << endl << flush; + } + weaponAlarm.SetLevel(NoAmmoState); + } + + switch (GetWeaponState()) + { + case LoadedState: + if (trigger) + { + if (viewFireEnable) + { + // + // The ammo pull happens HERE, in the caller -- a failed pull + // just stays Loaded, silently. + // + if (bin != NULL && bin->FeedAmmo() != 0) + { + weaponAlarm.SetLevel(FiringState); + ForceUpdate(); + FireWeapon(); + if (bin->GetAmmoState() == AmmoBin::AmmoEmptyState) + { + weaponAlarm.SetLevel(NoAmmoState); + } + else + { + weaponAlarm.SetLevel(LoadingState); + } + ForceUpdate(); + recoil = rechargeRate; + } + } + else + { + // + // THE DENIAL BLIP: a shot denied by the look-view gate does + // SetLevel(4); SetLevel(2) -- a one-frame audio blip, the pip + // stays Loaded and NO ammo is pulled. + // + weaponAlarm.SetLevel(TriggerDuringLoadState); + weaponAlarm.SetLevel(LoadedState); + } + } + break; + + case LoadingState: + if (trigger) + { + weaponAlarm.SetLevel(TriggerDuringLoadState); + weaponAlarm.SetLevel(LoadingState); + } + // + // Recoil bleeds down; at zero (and a round chambered) -> Loaded. + // + recoil -= time_slice; + if (recoil < 0.0f) + { + recoil = 0.0f; + if (bin != NULL && bin->GetAmmoState() == AmmoBin::AmmoLoadedState) + { + weaponAlarm.SetLevel(LoadedState); + if (getenv("BT_MECH_LOG")) + { + DEBUG_STREAM << "[fire] '" << GetName() + << "' LOADED (rounds=" << bin->GetAmmoCount() << ")" + << endl << flush; + } + } + } + ComputeOutputVoltage(); + break; + + case JammedState: + if (trigger) + { + weaponAlarm.SetLevel(TriggerDuringJamState); + weaponAlarm.SetLevel(JammedState); + } + recoil = rechargeRate; + break; + + case NoAmmoState: + if (trigger) + { + weaponAlarm.SetLevel(DryTriggerState); // the dry-fire blip + } + weaponAlarm.SetLevel(NoAmmoState); // re-assert (the roach-motel) + recoil = rechargeRate; + ComputeOutputVoltage(); // dial pinned at 0 + break; + + default: // 0/1/4/6: transient audio-blip states, no case body + break; + } + + Check_Fpu(); } diff --git a/restoration/source410/BT/PROJWEAP.HPP b/restoration/source410/BT/PROJWEAP.HPP index 1b40a3fc..14bc196e 100644 --- a/restoration/source410/BT/PROJWEAP.HPP +++ b/restoration/source410/BT/PROJWEAP.HPP @@ -28,13 +28,22 @@ struct ProjectileWeapon__SubsystemResource: public MechWeapon::SubsystemResource { + // + // WIRE-VERIFIED alignment (raw-stream dump, TEST.EGG SRM6s): the + // MechWeapon resource ancestry above runs THREE ints short of the wire + // (the pip-family fields -- pipColor/pipExtendedRange -- are not yet + // broken out; the full overlay verification is its own wave). The pad + // re-aligns this struct's fields to their true stream offsets: + // tracerInterval reads 1, ammoBinIndex the true bin roster slot (27/29), + // minTimeOfFlight 0.5, minVoltagePercentToFire 0.3, minJamChance 0.05, + // and MissileLauncher's missileCount lands on 6 (an SRM6!). + // + int resourceAlignPad[3]; int tracerInterval; int ammoBinIndex; Scalar minTimeOfFlight; Scalar minVoltagePercentToFire; Scalar minJamChance; - Scalar muzzleVelocity; - int missileCount; }; //########################################################################### @@ -56,6 +65,22 @@ virtual void FireWeapon(); + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Per-frame simulation (the ballistic fire state machine). + // + public: + typedef void + (ProjectileWeapon::*Performance)(Scalar time_slice); + void + SetPerformance(Performance performance) + { + Check(this); + activePerformance = (Simulation::Performance)performance; + } + + void + ProjectileWeaponSimulation(Scalar time_slice); + public: typedef ProjectileWeapon__SubsystemResource SubsystemResource;