BT410 Phase 5.3.9: ballistic fire path -- SRM launchers fire salvos + run dry
The SRM6 MissileLaunchers now run the authentic ballistic fire cycle against their linked AmmoBins, wire-verified end to end. - PROJWEAP ProjectileWeaponSimulation (PARTIAL, binary @004bbd04 -- fully recovered in the BT411 RE): one trigger-edge sample; the dry-bin gate pins NoAmmo(7) every frame; Loaded(2) -- the AMMO PULL LIVES IN THE CALLER (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 the 1995 "denied shot fakes a full firing cycle" defect class); granted -> Firing -> FireWeapon -> Loading (or NoAmmo on the emptying pull), recoil=rechargeRate; Loading bleeds recoil -> Loaded when a round is chambered, dial animates; Jammed(5) held; NoAmmo(7) = the roach-motel (re-asserts unconditionally). Deferred: electrical step, gate 1, eject, jam roll, HasActiveTarget. - AMMOBIN: FeedAmmo() + GetAmmoState() (1 round-ready / 2 EMPTY). - ammoBinLink wired from the resource's ammoBinIndex (the bin's roster slot). - WIRE-VERIFIED RESOURCE ALIGNMENT: a raw-stream dump pinned the ProjectileWeapon field block +3 ints past our staged struct (the MechWeapon resource ancestry runs 3 ints short -- the pip-family fields). Fixed with resourceAlignPad[3]; phantom muzzleVelocity/missileCount tail dropped. Confirmed on-wire: ammoBinIndex=27/29 (the exact bin slots), tracerInterval=1, minTOF=0.5, minVolt%=0.3, minJam=0.05, missileCount=6 (an SRM6!). - MECHWEAP: weapon-state enum extended to the full 1995 set (DryTrigger 1, Jammed 5, TriggerDuringJam 6, NoAmmo 7; count 8). - MISLANCH FireWeapon PARTIAL (@004bcc60: heat + salvo 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 at the authored 1s cadence, rounds 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. Zero Fail. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
# include <mech.hpp>
|
||||
#endif
|
||||
|
||||
#if !defined(AMMOBIN_HPP)
|
||||
# include <ammobin.hpp>
|
||||
#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 << "<no bin>";
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user