Issue #31: wire the EJECT button (EjectAmmo msg 0xb @004bb9b8) -- the in-mission unjam

Disasm-faithful transcription of the export-gap handler onto
ProjectileWeapon (MESSAGE_ENTRY id 0xb, the #19 pattern).  PRESS arms
the ~3s UpdateEject countdown (bin alarm -> Ejecting(3); gate 2 parks
the weapon offline mid-eject -- authentic).  HOLD to completion =
DumpAmmo jettisons the bay -> NoAmmo.  TAP (release early) cycles ONE
round out via FeedAmmo and returns the weapon to Loading -- clears a
jam at the cost of a round (and recovers a FailureHeat 7 while rounds
remain).  Binary structure kept exactly incl. the press-no-bin
fall-through into the release block.

AmmoBin: Ejecting(3)/Ejected(4) alarm levels decoded + named
SetAmmoState accessor (databinding rule).  Always-on forensics:
'[weap] EJECT tap' + '[ammo] bay DUMPED overboard (N rounds)'.

Rig-verified live (BT_EJECTTEST=1 / =hold on LRM15): taps eject one
round each and return the weapon to service (15->8 across cycles);
hold dumps all 16 -> NoAmmo.  Awaiting a human press of the actual
ENG-page button on a jammed weapon.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-23 13:49:11 -05:00
co-authored by Claude Opus 4.8
parent 225fe43f0c
commit 2edd1b5363
7 changed files with 134 additions and 8 deletions
+5 -1
View File
@@ -273,7 +273,11 @@ From the weapon `.SUB` records + the charge-curve `.data` constants (PE-parsed a
novice + @4ad7d4 heat-model + press-only, then idx@0x320=(idx+1)%(max@0x32C+1), NO
ResetFiringState); Emitter's faithful body existed as the orphaned "AdvanceSeekVoltage" (zero
callers) -- both registered via MESSAGE_ENTRY (ids 9/0xb). Verify: BT_SEEKTEST=1 +
BT_SEEK_LOG=1 → `[seek] <name> -> gear N` cycling. EjectAmmo remains unwired (#31).
BT_SEEK_LOG=1 → `[seek] <name> -> gear N` cycling. **EjectAmmo WIRED 2026-07-23 (issue
#31)**: MESSAGE_ENTRY id 0xb on `ProjectileWeapon::GetMessageHandlers` (projweap.cpp) +
`AmmoBin::SetAmmoState` accessor; rig-verified both paths (`BT_EJECTTEST=1` tap = one round
out/return to Loading, `=hold` = bay DUMPED; `[weap] EJECT tap` / `[ammo] bay DUMPED` logs).
Awaiting live cockpit-button verification.
**EjectAmmo @004bb9b8 DECODED 2026-07-23 (raw disasm; the fn sits in a Ghidra export gap)
[T1]** — old-timer testimony ("tap ejects a round, hold ejects the bay, eject clears a jam")
matched the bytes exactly: novice guard @4ac9c8**PRESS** (`msg+0xc > 0`): bin alarm
+3
View File
@@ -244,6 +244,9 @@ void AmmoBin::DumpAmmo()
{
if (ammoAlarm.Level() != Empty) // this+0x1A8 != 2
{
// ALWAYS log the bay dump (rare + forensic -- eject-hold / strip)
DEBUG_STREAM << "[ammo] " << GetName() << " bay DUMPED overboard ("
<< ammoCount << " rounds jettisoned)\n" << std::flush;
ammoCount = 0; // this+0x180
ammoAlarm.SetLevel(Dumped); // 5 (transient pulse)
ammoAlarm.SetLevel(Empty); // 2
+11 -1
View File
@@ -94,13 +94,17 @@
// Ammo feed state machine (this->ammoAlarm @0x194, level @0x1A8 = word 0x6A)
//
// The 6-level AlarmIndicator carries the feed cycle. Levels confirmed from
// usage; 3 and 4 are unobserved.
// usage; 3/4 decoded 2026-07-23 from the EjectAmmo handler @004bb9b8 (press
// raises 3 while the eject countdown runs; a tap-release blips 4 then 1
// before the FeedAmmo pull cycles the round out).
//
public:
enum AmmoState {
Feeding = 0, // a round is in transit (feedTimer counting down)
Loaded = 1, // a round is chambered and ready to fire
Empty = 2, // no rounds remain
Ejecting = 3, // EJECT held -- countdown live (@004bb9b8 press)
Ejected = 4, // tap-release blip before the round cycles out
Dumped = 5 // transient pulse raised by DumpAmmo() before Empty
};
@@ -169,6 +173,12 @@
int
GetAmmoState() const { return ammoAlarm.GetLevel(); }
// The EjectAmmo handler @004bb9b8 drives the feed alarm from the linked
// weapon (press -> Ejecting(3); tap-release -> Ejected(4) then Loaded(1)
// ahead of the FeedAmmo pull). Named setter per the databinding rule.
void
SetAmmoState(int level) { ammoAlarm.SetLevel(level); }
// Rounds remaining (this+0x180) -- read-only, for the AMMO digits gauge and
// the [ammo] fire trace (named accessor per the databinding rule).
int
+30
View File
@@ -5753,6 +5753,36 @@ void
}
}
// issue #31 verify (BT_EJECTTEST): pulse the EJECT button on the first
// MissileLauncher (0xBD0) -- press, then release ~2s later (inside the
// 3s countdown => a TAP: one round out, weapon back to Loading).
// BT_EJECTTEST=hold releases at ~5s instead (past the countdown => the
// HOLD: UpdateEject completes, DumpAmmo jettisons the bay -> NoAmmo).
// The [weap] EJECT tap log + [ammo] counts verify the @004bb9b8 wiring.
const char *ejectTest = getenv("BT_EJECTTEST");
if (ejectTest && (Entity *)this == application->GetViewpointEntity())
{
static int s_ejectTest = 0;
int releaseAt = (*ejectTest == 'h') ? 300 : 120; // 5s (hold) vs 2s (tap)
int phase = ++s_ejectTest % 480; // 8s cycle @60Hz
if (phase == 0 || phase == releaseAt) // press at 0, release later
{
for (int s = 1; s < GetSubsystemCount(); ++s)
{
Subsystem *sub = GetSubsystem(s);
if (sub != 0 && (int)sub->GetClassID() == 0xBD0)
{
ReceiverDataMessageOf<ControlsButton> msg(
0x0b /*ProjectileWeapon::EjectAmmoMessageID*/,
sizeof(ReceiverDataMessageOf<ControlsButton>),
(ControlsButton)(phase == 0 ? 1 : 0));
sub->Dispatch(&msg);
break;
}
}
}
}
// issue #20 verify (BT_BALTEST): alternate a MoveValve (id 4) press to
// the first condenser with a Mech BalanceCoolant (id 0x16) press every
// ~2s -- BT_VALVE_LOG shows the valve going 1->5 then BALANCE pulling
+68 -1
View File
@@ -169,11 +169,21 @@ Derivation
);
// task #6: inherit MechWeapon's handler set (see emitter.cpp note).
// issue #31 (2026-07-23): register the ammo-weapon handler table @0x512210
// {0xb, "EjectAmmo" -> @004bb9b8} on top of MechWeapon's ids 9/10 -- the pod's
// EJECT button (tap = eject one round / unjam, hold = jettison the bay) was
// silently swallowed. (The energy-weapon branch binds 0xb to ToggleSeekVoltage
// in ITS table instead -- see emitter.cpp.)
Receiver::MessageHandlerSet&
ProjectileWeapon::GetMessageHandlers()
{
static const Receiver::HandlerEntry entries[]=
{
MESSAGE_ENTRY(ProjectileWeapon, EjectAmmo) // id 0xb @004bb9b8
};
static Receiver::MessageHandlerSet messageHandlers(
MechWeapon::GetMessageHandlers()); // copy-inherit
ELEMENTS(entries), entries,
MechWeapon::GetMessageHandlers()); // copy-inherit (ids 9/10)
return messageHandlers;
}
Simulation::AttributeIndexSet ProjectileWeapon::AttributeIndex;
@@ -495,6 +505,63 @@ void
}
}
//
// @004bb9b8 -- the EJECT button (ammo-weapon handler table @0x512210, id 0xb).
// Disasm-transcribed 2026-07-23 (the fn sits in a Ghidra export gap; issue
// #31). PRESS (data > 0, bin resolved): bin alarm -> Ejecting(3), reset the
// countdown, ejectState = 0 (UpdateEject runs it; hold ~3s to completion =
// DumpAmmo whole bay -> NoAmmo). RELEASE with the countdown still live (a
// TAP): bin alarm Ejected(4) -> Loaded(1), FeedAmmo cycles ONE round out;
// bay dry -> weapon NoAmmo(7), else weapon -> Loading(3) + full recoil -- the
// weapon RETURNS TO SERVICE (this is the in-mission UNJAM; it also recovers a
// FailureHeat 7 while rounds remain). Binary structure kept exactly: a press
// with NO bin falls THROUGH to the release block (jle/je both target 4bba29).
//
void
ProjectileWeapon::EjectAmmoMessageHandler(ReceiverDataMessageOf<int> *message)
{
if (MechSubsystem::IsDamaged()) // @4ac9c8 -- NOVICE lockout bridge
return;
AmmoBin *bin = (AmmoBin*)ammoBinLink.Resolve(); // FUN_00417ab4(this+0x43c)
if (message->dataContents > 0 && bin != 0) // PRESS @4bb9d0/4bb9e9
{
bin->SetAmmoState(AmmoBin::Ejecting); // SetLevel(bin+0x194, 3)
timeToEject = totalTimeToEject; // this+0x3f4 = this+0x3f0
percentOfEject = 0.0f; // this+0x3f8
ejectState = 0; // this+0x400 -- countdown live
return; // jmp 4bbad9
}
// RELEASE (or press-no-bin fall-through) @4bba29
if (ejectState == 0) // countdown still live -> a TAP
{
if (bin != 0)
{
bin->SetAmmoState(AmmoBin::Ejected); // SetLevel(bin+0x194, 4)
bin->SetAmmoState(AmmoBin::Loaded); // SetLevel(bin+0x194, 1)
bin->FeedAmmo(); // @4bd4f4 -- cycle one round out
if (bin->GetAmmoState() == AmmoBin::Empty) // [bin+0x1a8] == 2
weaponAlarm.SetLevel(NoAmmoState); // bay dry -> 7
else
{
weaponAlarm.SetLevel(3); // -> Loading: BACK IN SERVICE
recoil = rechargeRate; // this+0x3e8 = this+0x3dc (dial restarts)
}
DEBUG_STREAM << "[weap] " << GetName() << " EJECT tap: round out, "
<< bin->GetAmmoCount() << " left -> "
<< (bin->GetAmmoState() == AmmoBin::Empty ? "NoAmmo" : "reloading")
<< "\n" << std::flush;
timeToEject = totalTimeToEject;
percentOfEject = 0.0f;
}
else
weaponAlarm.SetLevel(NoAmmoState); // @4bbabe -> 7
}
ejectState = -1; // @4bbacf -- countdown cancelled
}
//
// @004bbc78 -- bleed the firing charge (MechWeapon recoil @0x3E8) at a rate set
// by the available bus voltage and the AmmoBin's remaining supply ratio.
+13 -1
View File
@@ -102,7 +102,19 @@ class NotationFile;
public:
static Derivation ClassDerivations; // parent = MechWeapon::ClassDerivations,
// name = "ProjectileWeapon" (DAT_00512280)
static Receiver::MessageHandlerSet& GetMessageHandlers(); // inherits MechWeapon's (task #6)
static Receiver::MessageHandlerSet& GetMessageHandlers(); // MechWeapon's + id 0xb EjectAmmo (issue #31)
enum { EjectAmmoMessageID = 0x0b }; // ammo-weapon table @0x512210
// @004bb9b8 -- the ammo-weapon handler table @0x512210 {0xb, "EjectAmmo"}.
// The pod's EJECT button: PRESS arms the ~3s UpdateEject countdown
// (hold-to-completion = DumpAmmo whole bay -> NoAmmo); TAP (release
// early) cycles ONE round out via FeedAmmo and, with ammo remaining,
// returns the weapon to Loading -- the in-mission UNJAM. Wired
// 2026-07-23 (issue #31); disasm-faithful incl. the press-no-bin
// fall-through into the release block.
void
EjectAmmoMessageHandler(ReceiverDataMessageOf<int> *message);
static AttributeIndexSet AttributeIndex;
static SharedData DefaultData;
+4 -4
View File
@@ -45,10 +45,10 @@ CONTROLS (keyboard):
A weapon gone quiet with ammo left has probably JAMMED (heat makes
ballistic weapons jam -- authentic!). Check its panel's ENG DATA page
for the jam lamp (a cocked round with warning bolts). The pod's fix
is the EJECT button: TAP to eject the jammed round (weapon reloads and
returns to service), HOLD ~3s to jettison the whole bay before it
cooks off. (Wiring in progress -- until it lands, a respawn clears
jams.) Watch the TEMP strip and pace salvos.
is the EJECT button on that weapon's ENG DATA page: TAP to eject the
jammed round (weapon reloads and returns to service), HOLD ~3s to
jettison the whole bay before it cooks off. Watch the TEMP strip and
pace salvos.
G (hold) + fire key .. regroup a weapon onto that trigger -- or hold a
weapon panel's red PROGRAM button with the mouse and tap a fire key;
the joystick diagram on the panel shows the binding live.