From 89ddd492830044975d7049be0068e3279f7afa9f Mon Sep 17 00:00:00 2001 From: arcattack Date: Mon, 20 Jul 2026 21:59:46 -0500 Subject: [PATCH] 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 --- context/open-questions.md | 18 ++++++++++ game/reconstructed/ammobin.hpp | 5 +++ game/reconstructed/projweap.cpp | 60 +++++++++++++++++++++++++++++---- 3 files changed, 77 insertions(+), 6 deletions(-) diff --git a/context/open-questions.md b/context/open-questions.md index f227a5a..61832c3 100644 --- a/context/open-questions.md +++ b/context/open-questions.md @@ -788,3 +788,21 @@ and **"Peer motion: the 'random shakiness' is single-box packet jitter, NOT the `IntegrateMotion`'s 2-stage angular integrate is intended. - Speed model decomp-settled (`2c6db6a`): analog throttle, '5 speeds' false, `throttleState@0x4a4` is actually the fall-surface material (RENAME PENDING). + +## Missile launcher trips FailureHeat -> permanent NoAmmo brick (REPRODUCED 2026-07-20) + +Playtest: "lost the ability to fire missiles mid-fight, no pips, sad noise -- did I run out?" +Added the `[ammo]` fire trace (env `BT_AMMO_LOG`, projweap.cpp) and reproduced it: the SRM6 is +NOT out of ammo -- after ~5 volleys it trips **gate 1 FailureHeat** (`heatAlarm==2`) and latches +into the NoAmmo roach-motel (weaponAlarm 7, never released) 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 + +This is the heat-economy open question made concrete (gauges-hud.md "degenerate bring-up heat +scale"; projweap.cpp:730 "whether FailureHeat is reachable at authentic play intensity"): our +autofire test fires 60/s (extreme), but the user hit it in MANUAL play too -> either the per-shot +heat / FailureTemperature scaling is too aggressive, or the roach-motel (no recovery when heat +falls) is the wrong reading. NEXT: audit the SRM6 heatPerShot vs FailureTemperature vs the mech's +dissipation, and whether the binary's gate-1 FailureHeat is a LATCH or clears when heat drops. +Trace it live with `BT_AMMO_LOG=1` (+ `BT_PROJ_LOG=1` for per-tick heat/recoil). diff --git a/game/reconstructed/ammobin.hpp b/game/reconstructed/ammobin.hpp index e295d24..93f7af7 100644 --- a/game/reconstructed/ammobin.hpp +++ b/game/reconstructed/ammobin.hpp @@ -169,6 +169,11 @@ int GetAmmoState() const { return ammoAlarm.GetLevel(); } + // Rounds remaining (this+0x180) -- read-only, for the AMMO digits gauge and + // the [ammo] fire trace (named accessor per the databinding rule). + int + GetAmmoCount() const { return ammoCount; } + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Subsystem virtual overrides (vtable @0051286c) // diff --git a/game/reconstructed/projweap.cpp b/game/reconstructed/projweap.cpp index 343fb83..340be71 100644 --- a/game/reconstructed/projweap.cpp +++ b/game/reconstructed/projweap.cpp @@ -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