From 2edd1b53635cada2bf8efc2fa5f067edaf153492 Mon Sep 17 00:00:00 2001 From: arcattack Date: Thu, 23 Jul 2026 13:49:11 -0500 Subject: [PATCH] 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) --- context/decomp-reference.md | 6 ++- game/reconstructed/ammobin.cpp | 3 ++ game/reconstructed/ammobin.hpp | 12 +++++- game/reconstructed/mech4.cpp | 30 ++++++++++++++ game/reconstructed/projweap.cpp | 69 ++++++++++++++++++++++++++++++++- game/reconstructed/projweap.hpp | 14 ++++++- players/README.txt | 8 ++-- 7 files changed, 134 insertions(+), 8 deletions(-) diff --git a/context/decomp-reference.md b/context/decomp-reference.md index a73fd0d..86dcef9 100644 --- a/context/decomp-reference.md +++ b/context/decomp-reference.md @@ -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] -> gear N` cycling. EjectAmmo remains unwired (#31). + BT_SEEK_LOG=1 → `[seek] -> 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 diff --git a/game/reconstructed/ammobin.cpp b/game/reconstructed/ammobin.cpp index ca902f0..4097e58 100644 --- a/game/reconstructed/ammobin.cpp +++ b/game/reconstructed/ammobin.cpp @@ -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 diff --git a/game/reconstructed/ammobin.hpp b/game/reconstructed/ammobin.hpp index fa67d56..5d05996 100644 --- a/game/reconstructed/ammobin.hpp +++ b/game/reconstructed/ammobin.hpp @@ -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 diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 23af7ae..1565cab 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -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 msg( + 0x0b /*ProjectileWeapon::EjectAmmoMessageID*/, + sizeof(ReceiverDataMessageOf), + (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 diff --git a/game/reconstructed/projweap.cpp b/game/reconstructed/projweap.cpp index 5fe3d11..e82ad5c 100644 --- a/game/reconstructed/projweap.cpp +++ b/game/reconstructed/projweap.cpp @@ -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 *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. diff --git a/game/reconstructed/projweap.hpp b/game/reconstructed/projweap.hpp index 407bc21..83086f7 100644 --- a/game/reconstructed/projweap.hpp +++ b/game/reconstructed/projweap.hpp @@ -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 *message); static AttributeIndexSet AttributeIndex; static SharedData DefaultData; diff --git a/players/README.txt b/players/README.txt index 79dcc90..c2813c8 100644 --- a/players/README.txt +++ b/players/README.txt @@ -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.