Add [ammo] fire trace (BT_AMMO_LOG); reproduces the missile FailureHeat->NoAmmo brick

Playtest: "lost missiles mid-fight, no pips, sad noise -- out of ammo?"  The default
build didn't trace ammo, so it couldn't be answered from the log.  Added a [ammo]
trace (env BT_AMMO_LOG) at the four ProjectileWeaponSimulation decision points --
FIRED+rounds-left, gate1/gate2 dry-transition edge, dry-fire blip, and denial -- plus
a public AmmoBin::GetAmmoCount() const getter.

Reproduced the actual cause: NOT ammo depletion.  The SRM6 trips gate 1 FailureHeat
(heatLvl=2) after ~5 volleys and latches into the permanent NoAmmo roach-motel with
19 rounds still in the bin:
  [ammo] SRM6_2 FIRED, rounds left=19
  [ammo] SRM6_2 -> NoAmmo (gate1): destroyed=0 failHeat=1 heatLvl=2 mechDisabled=0
Logged as the concrete instance of the heat-economy open question (open-questions.md).
Trace-only; no behavior change (all output gated on BT_AMMO_LOG).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-20 21:59:46 -05:00
co-authored by Claude Opus 4.8
parent 7052d9e5bb
commit 89ddd49283
3 changed files with 77 additions and 6 deletions
+54 -6
View File
@@ -618,19 +618,36 @@ void
// @4bbd28: advance any in-progress magazine eject.
UpdateEject(time_slice); // call 0x4bbb50
// [ammo] fire trace (env BT_AMMO_LOG): surfaces the round count + the exact
// dry transition / dry-fire blip / denial, so "did I run out of missiles?" is
// answerable from the log instead of inferred. Cached once.
static int s_ammoLog = -1;
if (s_ammoLog < 0) s_ammoLog = getenv("BT_AMMO_LOG") ? 1 : 0;
// GATE 1 @4bbd36-4bbd6e: weapon DESTROYED (`simulationState@0x40 == 1` --
// NOT simulationFlags, the historic mis-transcription of gotcha #20), own
// heatAlarm at FailureHeat (this+0x184 == 2 -- THE consumer of the authored
// FailureTemperature for the ballistic family), or the owning mech disabled
// (FUN_0049fb54) -> pin recoil at the full rechargeRate + latch alarm 7.
// No return: the frame continues into the state machine.
if (simulationState == 1 // [this+0x40] == 1 (destroyed)
|| heatAlarm.GetLevel() == HeatSink::FailureHeat // [this+0x184] == 2
|| (owner != 0 && owner->IsDerivedFrom(*Mech::GetClassDerivations())
&& ((Mech *)owner)->IsDisabled())) // FUN_0049fb54(owner)
{
recoil = rechargeRate; // [this+0x3E8] = [this+0x3DC]
weaponAlarm.SetLevel(7); // call 0x41bbd8(this+0x350, 7)
int gate1Destroyed = (simulationState == 1);
int gate1FailHeat = (heatAlarm.GetLevel() == HeatSink::FailureHeat);
int gate1Disabled = (owner != 0 && owner->IsDerivedFrom(*Mech::GetClassDerivations())
&& ((Mech *)owner)->IsDisabled());
if (gate1Destroyed || gate1FailHeat || gate1Disabled)
{
// Edge log: the FIRST frame gate 1 bricks the weapon into the NoAmmo
// roach-motel (destroyed / FailureHeat / mech disabled) -- distinguishes
// a true dry bin (gate 2) from a HEAT trip (the open heat-economy question).
if (s_ammoLog && weaponAlarm.GetState() != 7)
DEBUG_STREAM << "[ammo] " << GetName() << " -> NoAmmo (gate1): destroyed="
<< gate1Destroyed << " failHeat=" << gate1FailHeat
<< " heatLvl=" << heatAlarm.GetLevel()
<< " mechDisabled=" << gate1Disabled << "\n" << std::flush;
recoil = rechargeRate; // [this+0x3E8] = [this+0x3DC]
weaponAlarm.SetLevel(7); // call 0x41bbd8(this+0x350, 7)
}
}
// GATE 2 @4bbd71-4bbda6: the linked AmmoBin. Feed alarm Empty(2)/level-3 or
@@ -642,6 +659,12 @@ void
&& (bin->GetAmmoState() == 2 || bin->GetAmmoState() == 3 // [bin+0x1A8]
|| bin->GetSimulationState() == 1)) // [bin+0x40] (destroyed)
{
// Edge: log the FIRST frame the weapon is pinned dry (not the per-frame re-pin).
if (s_ammoLog && weaponAlarm.GetState() != 7)
DEBUG_STREAM << "[ammo] " << GetName() << " -> NoAmmo (gate2): rounds="
<< bin->GetAmmoCount() << " binState=" << bin->GetAmmoState()
<< " binDestroyed=" << (int)(bin->GetSimulationState() == 1)
<< "\n" << std::flush;
weaponAlarm.SetLevel(7);
}
@@ -668,6 +691,11 @@ void
weaponAlarm.SetLevel(0); // @4bbef4 -> Firing
ForceUpdate(); // @4bbf05 `or word [this+0x18],1` (updateModel)
FireWeapon(); // @4bbf0d vtbl+0x48 (slot 18)
if (s_ammoLog)
DEBUG_STREAM << "[ammo] " << GetName() << " FIRED, rounds left="
<< bin->GetAmmoCount()
<< (bin->GetAmmoState() == 2 ? " (bin EMPTY -> NoAmmo)" : "")
<< "\n" << std::flush;
if (bin->GetAmmoState() == 2) // @4bbf11 bin went Empty on the pull
weaponAlarm.SetLevel(7); // -> unavailable (gate 2 re-pins it)
else if (CheckForJam()) // @4bbf2a call 0x4bbfcc (heat-scaled roll)
@@ -677,9 +705,21 @@ void
ForceUpdate(); // @4bbf4c second updateModel mark
recoil = rechargeRate; // @4bbf51 begin the recharge cooldown
}
else if (s_ammoLog)
DEBUG_STREAM << "[ammo] " << GetName()
<< " trigger but FeedAmmo not ready: rounds="
<< (bin != 0 ? bin->GetAmmoCount() : -1)
<< " binState=" << (bin != 0 ? bin->GetAmmoState() : -1)
<< "\n" << std::flush;
}
else
{
if (s_ammoLog)
DEBUG_STREAM << "[ammo] " << GetName()
<< " DENIED (no target / view gate): viewFire=" << (int)viewFireEnable
<< " hasTarget=" << (int)HasActiveTarget()
<< " rounds=" << (bin != 0 ? bin->GetAmmoCount() : -1)
<< " (pips stay, no ammo pulled)\n" << std::flush;
// @4bbf5f THE DENIAL BLIP (Gitea #12 fix): a shot denied by the
// look-view gate or by having NO TARGET does SetLevel(4);
// SetLevel(2) -- a one-frame audio blip, the pip STAYS LOADED and
@@ -738,7 +778,15 @@ void
// whether FailureHeat is reachable at authentic play intensity is
// the open heat-economy question (open-questions.md, MP section).
if (trigger)
{
if (s_ammoLog)
DEBUG_STREAM << "[ammo] " << GetName()
<< " DRY-FIRE blip (NoAmmo roach-motel): rounds="
<< (bin != 0 ? bin->GetAmmoCount() : -1)
<< " binState=" << (bin != 0 ? bin->GetAmmoState() : -1)
<< "\n" << std::flush;
weaponAlarm.SetLevel(1); // @4bbe54 dry-trigger blip
}
weaponAlarm.SetLevel(7); // @4bbe65 re-assert
recoil = rechargeRate; // @4bbe76
ComputeOutputVoltage(); // @4bbe85 vtbl+0x44 -- dial pinned at 0