#55 steps 0/1/2/4: the 1995 DEATH pass restored, the arg gate made real, and 5 subsystems the respawn sweep never reached
THE HEADLINE: the port's death sweep was a total no-op, and fixing it required
fixing the arg gate in the same commit -- otherwise corpses refill their ammo.
STEP 2 -- the authentic dispatch shape:
* engine/MUNGA/SUBSYSTM.h: Subsystem::DeathShutdown gains the binary's
universal base body { DeathReset(c) } (@004ad10e). It was empty, and NO
port class overrode it, so everything the 1995 game does at death was
skipped entirely.
* mech4.cpp death sweep now passes 0, not 1 (the binary's @0049fe0c arg) --
the wreck shape: alarms/state settle, nothing refills.
* ARG PROPAGATION (mandatory once the sweep runs): AmmoBin::DeathReset is now
arg-gated exactly as @004bd26c -- refill only when arg != 0, ammoAlarm
unconditional. Ignoring the arg was harmless only while the sweep was
dead; with it restored, every corpse would have re-armed. Also forwarded
in PoweredSubsystem / HeatSink / Condenser / HeatableSubsystem / Generator
(each binary body forwards it -- verified addresses in the plan).
STEP 4 -- coverage for 5 classes that had an authentic slot-10 body but no
DeathReset, so the respawn sweep fell through to the empty base:
Torso (this is Gitea #70 -- "loosing torso twist function after a death"),
Gyroscope, Myomers, HUD, Seeker.
STEPS 0/1 -- observability + the David chain:
* Three unconditional matchlog rows: DEAD_NOTIFY (mech + resolved link),
PLAYER_LINK (player <-> vehicle), RESPAWN (mode/alive/zones/subsys/pos).
A respawn was previously INVISIBLE in the matchlog -- night 3's analysis
had to infer them from ammo arithmetic.
* Mech::PlayerLinkMessageHandler + the death dispatch site: when the engine's
one-shot registry lookup misses (no null check, no retry -> the whole
death/respawn cycle silently swallowed), recover the SAME object via the
reverse link the binary's own respawn branch walks (player+0x1FC ==
playerVehicle). Complete-type TU, no raw offsets.
* deathPending cleared in the first-spawn branch (a latch carried in would
permanently kill every later respawn -- the #57 class).
VERIFIED on the 2-pod rig (madcat vs thor, forced kills, 5 death/respawn cycles
per pod): build clean, ZERO new /FORCE unresolved externs from the 5 new
overrides; refill lines appear ONLY immediately before a Mech::Reset and NEVER
between a death and the next respawn (the corpse-refill regression this commit
had to pre-empt); all three probe rows present in both pods' matchlogs; every
DEAD_NOTIFY carried a non-null link.
DELIBERATELY DEFERRED (documented, not forgotten): the mechsub.cpp rename pair
(ResetToInitialState -> GenerateFault, ClearStatus -> the root reset @004ac22c),
deleting HeatableSubsystem::ResetToInitialState, and deleting RespawnRepair.
Those need the HeatableSubsystem vtable-slot-10 pre-flight the plan calls for,
and we have no binary image here to dump the vtable from -- guessing at it
risks the vptr-alias trap. Next session with the decomp shards open.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
26e678e570
commit
ef8e449a17
@@ -163,8 +163,15 @@ virtual void
|
||||
{}
|
||||
|
||||
virtual void
|
||||
DeathShutdown(int /*shutdown_command*/)
|
||||
{}
|
||||
DeathShutdown(int shutdown_command)
|
||||
{
|
||||
// #55: the binary's universal base @004ad10e forwards slot 11 to
|
||||
// slot 10 -- { this->vtbl[0x28](this, arg); } -- so a death sweep
|
||||
// with arg 0 runs every subsystem's DeathReset in its wreck shape
|
||||
// (no refill). The empty body here made the whole 1995 death
|
||||
// pass a no-op in the port.
|
||||
DeathReset(shutdown_command);
|
||||
}
|
||||
};
|
||||
|
||||
//##########################################################################
|
||||
|
||||
@@ -455,15 +455,29 @@ void AmmoBin::ReadUpdateRecord(UpdateRecord *update)
|
||||
// respawn restores exactly those ctor values. (ResetToInitialState below
|
||||
// stays authentically EMPTY per AMMOBIN.TCP.)
|
||||
//
|
||||
void AmmoBin::DeathReset(int /*reset_command*/)
|
||||
void AmmoBin::DeathReset(int reset_command)
|
||||
{
|
||||
if (getenv("BT_DEATH_LOG"))
|
||||
DEBUG_STREAM << "[respawn] ammo bin " << subsystemID << " refilled "
|
||||
<< ammoCount << " -> " << initialAmmoCount << "\n" << std::flush;
|
||||
ammoCount = initialAmmoCount; // @0x180 <- @0x220 (ctor value)
|
||||
feedTimer = 0; // @0x184
|
||||
cookOffArmed = 0; // @0x18C
|
||||
ammoAlarm.SetLevel(Loaded); // @0x1A8 (ctor: 1)
|
||||
//
|
||||
// THE ARG GATE IS AUTHENTIC AND LOAD-BEARING (@004bd26c, #55 step 2):
|
||||
// if (arg) { feedTimer = 0; [0x228] = 0; ammoCount = initialAmmoCount;
|
||||
// cookOffArmed = 0; }
|
||||
// ammoAlarm.SetLevel(Loaded); // UNCONDITIONAL
|
||||
// The refill runs on a RESPAWN (arg 1) and must NOT run at DEATH (arg 0) --
|
||||
// a wreck keeps its counts. This body used to ignore the arg, which was
|
||||
// harmless only while the death sweep was a no-op; the moment that sweep
|
||||
// was restored (SUBSYSTM.h DeathShutdown -> DeathReset with arg 0) an
|
||||
// unguarded refill would have re-armed every corpse.
|
||||
//
|
||||
if (reset_command != 0)
|
||||
{
|
||||
if (getenv("BT_DEATH_LOG"))
|
||||
DEBUG_STREAM << "[respawn] ammo bin " << subsystemID << " refilled "
|
||||
<< ammoCount << " -> " << initialAmmoCount << "\n" << std::flush;
|
||||
ammoCount = initialAmmoCount; // @0x180 <- @0x220 (ctor value)
|
||||
feedTimer = 0; // @0x184
|
||||
cookOffArmed = 0; // @0x18C
|
||||
}
|
||||
ammoAlarm.SetLevel(Loaded); // @0x1A8 -- unconditional per the binary
|
||||
}
|
||||
|
||||
//#############################################################################
|
||||
|
||||
@@ -1292,6 +1292,12 @@ void
|
||||
DEBUG_STREAM << "[cam] InitializePlayerLink dispatched (player "
|
||||
<< GetEntityID() << " -> vehicle "
|
||||
<< playerVehicle->GetEntityID() << ")\n" << std::flush;
|
||||
// #55 step 0: prove the link in the matchlog -- a lost PlayerLink race
|
||||
// (the David chain) was previously invisible.
|
||||
BTMatchLog("PLAYER_LINK", "player=%d:%d vehicle=%d:%d",
|
||||
BTMatchHostOf(GetEntityID()), (int)GetEntityID(),
|
||||
BTMatchHostOf(playerVehicle->GetEntityID()),
|
||||
(int)playerVehicle->GetEntityID());
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
@@ -1367,6 +1373,9 @@ void
|
||||
|
||||
AlwaysExecute(); // param_1[10] &= ~2 (run every frame)
|
||||
deathCount = 0; // param_1[0x80]
|
||||
deathPending = 0; // #55 step 1: a latch carried into a
|
||||
// fresh spawn would permanently kill
|
||||
// every later respawn (the #57 class)
|
||||
// (warp fired in the shared placement below -- initial drop-in + respawn)
|
||||
}
|
||||
else if (deathCount == message->deathCount) // param_2[0xe] == param_1[0x80]
|
||||
|
||||
@@ -1083,3 +1083,12 @@ void GyroApplyDamageTorque(Subsystem *gyro, Scalar x, Scalar y, Scalar z, Scalar
|
||||
if (gyro != 0)
|
||||
static_cast<Gyroscope *>(gyro)->ApplyDamageTorque(x, y, z, magnitude);
|
||||
}
|
||||
|
||||
//
|
||||
// DeathReset (#55 step 4) -- the respawn sweep's entry for the Gyroscope.
|
||||
//
|
||||
void
|
||||
Gyroscope::DeathReset(int /*reset_command*/)
|
||||
{
|
||||
ResetToInitialState(); // @004b2678 (takes no arg)
|
||||
}
|
||||
|
||||
@@ -241,6 +241,12 @@ class Damage; // engine damage record (DAMAGE.h); ApplyDamageResponse input
|
||||
public:
|
||||
Logical HandleDeathMessage(Message &message); // slot 9, @004b2660 -> @004b179c
|
||||
void ResetToInitialState(); // slot 10, @004b2678
|
||||
// #55 step 4: the respawn sweep (Mech::Reset -> DeathReset) never
|
||||
// reached this class -- it fell through to the empty Subsystem base, so
|
||||
// this subsystem kept its previous life's state. Chains our own
|
||||
// authentic slot-10 body.
|
||||
void DeathReset(int reset_command);
|
||||
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Construction and Destruction
|
||||
|
||||
@@ -195,9 +195,9 @@ void
|
||||
// the heat family was reset on respawn at all.
|
||||
//
|
||||
void
|
||||
HeatableSubsystem::DeathReset(int /*reset_command*/)
|
||||
HeatableSubsystem::DeathReset(int reset_command)
|
||||
{
|
||||
ResetToInitialState(True);
|
||||
ResetToInitialState(reset_command != 0); // #55: arg forwarded
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
@@ -377,9 +377,9 @@ void
|
||||
// [T3 -- do NOT claim valves are fixed; tracked on the issue.]
|
||||
//
|
||||
void
|
||||
Condenser::DeathReset(int /*reset_command*/)
|
||||
Condenser::DeathReset(int reset_command)
|
||||
{
|
||||
ResetToInitialState(True);
|
||||
ResetToInitialState(reset_command != 0); // #55: arg forwarded
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
@@ -723,9 +723,9 @@ void
|
||||
// coolant").
|
||||
//
|
||||
void
|
||||
HeatSink::DeathReset(int /*reset_command*/)
|
||||
HeatSink::DeathReset(int reset_command)
|
||||
{
|
||||
ResetToInitialState(True); // @004ad760
|
||||
ResetToInitialState(reset_command != 0); // @004ad760 (#55: arg forwarded)
|
||||
// Gitea #55 regression guard: the respawn refill must actually land. Verified
|
||||
// live 2026-07-25 -- coolant == capacity on every heat sink across 4 respawn
|
||||
// cycles, and the temperature restored to startingTemperature (77) instead of
|
||||
|
||||
@@ -466,3 +466,12 @@ Subsystem *CreateHUDSubsystem(Mech *owner, int id, void *seg)
|
||||
return (Subsystem *) new (Memory::Allocate(0x2a4))
|
||||
HUD(owner, id, (HUD::SubsystemResource *)seg, HUD::DefaultData);
|
||||
}
|
||||
|
||||
//
|
||||
// DeathReset (#55 step 4) -- the respawn sweep's entry for the HUD.
|
||||
//
|
||||
void
|
||||
HUD::DeathReset(int reset_command)
|
||||
{
|
||||
ResetToInitialState(reset_command != 0); // @004b77bc
|
||||
}
|
||||
|
||||
@@ -225,6 +225,12 @@
|
||||
HandleMessage(int message); // slot 9, @004b7780 (body not captured)
|
||||
void
|
||||
ResetToInitialState(Logical powered = True); // slot 10, @004b77bc
|
||||
// #55 step 4: the respawn sweep (Mech::Reset -> DeathReset) never
|
||||
// reached this class -- it fell through to the empty Subsystem base, so
|
||||
// this subsystem kept its previous life's state. Chains our own
|
||||
// authentic slot-10 body.
|
||||
void DeathReset(int reset_command);
|
||||
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Test Class Support
|
||||
|
||||
@@ -724,6 +724,28 @@ void
|
||||
(GetInstance() == ReplicantInstance) ? 'R' : 'M',
|
||||
BTMatchHostOf(message->playerID), (int)message->playerID,
|
||||
(int)(owner != 0));
|
||||
//
|
||||
// #55 step 1 (the David chain): the engine base wrote playerLink =
|
||||
// host->GetEntityPointer(playerID) with NO null check and NO retry -- a
|
||||
// lost registry race left this mech ownerless forever, silently swallowing
|
||||
// its whole death/respawn cycle. Recover the SAME object via the reverse
|
||||
// link the binary's own respawn branch walks (player+0x1FC ==
|
||||
// playerVehicle): the local mission player whose vehicle is this mech.
|
||||
// Complete-type TU (Mech + Player both visible) -- no raw offsets.
|
||||
//
|
||||
if (owner == 0 && application != 0)
|
||||
{
|
||||
Player *mission_player = (Player *)application->GetMissionPlayer();
|
||||
if (mission_player != 0
|
||||
&& mission_player->GetPlayerVehicle() == (Entity *)this)
|
||||
{
|
||||
playerLink = mission_player; // public engine member
|
||||
owner = mission_player;
|
||||
DEBUG_STREAM << "[respawn] PlayerLink registry miss RECOVERED "
|
||||
"via the reverse link (mech " << GetEntityID() << ")\n"
|
||||
<< std::flush;
|
||||
}
|
||||
}
|
||||
if (owner != NULL)
|
||||
{
|
||||
owner->SetPlayerVehicle(this); // player+0x1fc = this
|
||||
|
||||
@@ -1829,6 +1829,16 @@ void
|
||||
<< ") alive=" << (int)(!IsMechDestroyed())
|
||||
<< " zones=" << damageZoneCount
|
||||
<< " subsys=" << GetSubsystemCount() << "\n" << std::flush;
|
||||
// #55 step 0: a respawn used to be INVISIBLE in the matchlog (VEHICLE only
|
||||
// logs on the CreatePlayerVehicle path, which reset-based respawn never
|
||||
// takes) -- night 3's analysis had to infer every respawn from ammo
|
||||
// arithmetic. One unconditional row per reset.
|
||||
BTMatchLog("RESPAWN", "mech=%d:%d mode=%d alive=%d zones=%d subsys=%d "
|
||||
"pos=%.1f,%.1f,%.1f",
|
||||
BTMatchHostOf(GetEntityID()), (int)GetEntityID(), mode,
|
||||
(int)(!IsMechDestroyed()), damageZoneCount, GetSubsystemCount(),
|
||||
localOrigin.linearPosition.x, localOrigin.linearPosition.y,
|
||||
localOrigin.linearPosition.z);
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
@@ -1940,7 +1950,9 @@ void
|
||||
{
|
||||
Subsystem *s = GetSubsystem(i);
|
||||
if (s != 0)
|
||||
s->DeathShutdown(1);
|
||||
s->DeathShutdown(0); // #55: the binary sweep @0049fe0c passes 0
|
||||
// (wreck shape: alarms/state settle, ammo
|
||||
// bins do NOT refill the corpse)
|
||||
}
|
||||
// Request the DEATH record BEFORE entering the disabled state (the
|
||||
// ForceUpdate filter masks types 2..8 once IsDisabled) -- the binary
|
||||
@@ -2052,6 +2064,27 @@ void
|
||||
//
|
||||
{
|
||||
Player *owner = GetPlayerLink();
|
||||
// #55 step 1 (the David fix): the death notification used to hinge on
|
||||
// this single never-retried pointer -- playerLink is written once by
|
||||
// Entity::PlayerLinkMessageHandler with no null check, so a lost race
|
||||
// silently swallowed the whole respawn cycle. Resolve the SAME object
|
||||
// by the reverse link the binary's own respawn branch uses
|
||||
// (player+0x1FC == playerVehicle): the mission player whose vehicle is
|
||||
// this mech. Not a stand-in -- the identical entity, second index.
|
||||
if (owner == 0 && application != 0)
|
||||
{
|
||||
Player *mission_player = (Player *)application->GetMissionPlayer();
|
||||
if (mission_player != 0
|
||||
&& mission_player->GetPlayerVehicle() == (Entity *)this)
|
||||
{
|
||||
owner = mission_player;
|
||||
}
|
||||
}
|
||||
// #55 step 0: a NULL link was a SILENT swallow -- record the
|
||||
// resolution unconditionally so the matchlog can prove it either way.
|
||||
BTMatchLog("DEAD_NOTIFY", "mech=%d:%d link=%p",
|
||||
BTMatchHostOf(GetEntityID()), (int)GetEntityID(),
|
||||
(void *)owner);
|
||||
if (owner != 0)
|
||||
{
|
||||
Player::VehicleDeadMessage
|
||||
|
||||
@@ -634,3 +634,11 @@ Subsystem *CreateMyomersSubsystem(Mech *owner, int id, void *seg)
|
||||
return (Subsystem *) new (Memory::Allocate(0x358))
|
||||
Myomers(owner, id, (Myomers::SubsystemResource *)seg);
|
||||
}
|
||||
|
||||
//
|
||||
// DeathReset (#55 step 4) -- the respawn sweep's entry for the Myomers.
|
||||
//
|
||||
void Myomers::DeathReset(int reset_command)
|
||||
{
|
||||
ResetToInitialState(reset_command != 0); // @004b8aa4
|
||||
}
|
||||
|
||||
@@ -245,6 +245,12 @@ class Mech;
|
||||
HandleMessage(int message); // slot 9, @004b8a8c (-> PoweredSubsystem @004b0efc)
|
||||
void
|
||||
ResetToInitialState(Logical powered = True); // slot 10, @004b8aa4 (-> PoweredSubsystem @004b0e6c)
|
||||
// #55 step 4: the respawn sweep (Mech::Reset -> DeathReset) never
|
||||
// reached this class -- it fell through to the empty Subsystem base, so
|
||||
// this subsystem kept its previous life's state. Chains our own
|
||||
// authentic slot-10 body.
|
||||
void DeathReset(int reset_command);
|
||||
|
||||
// slot 15 (PoweredSubsystem base @004b1780) -- the SeekVoltageGraph's
|
||||
// voltage-response sampler (issue #11: the shared subsystem
|
||||
// vtbl+0x3C slot the graph's Execute @004c6934 calls; Emitter's
|
||||
|
||||
@@ -427,9 +427,12 @@ Scalar
|
||||
// timer reset, but only while the source is actually Ready (state 2).
|
||||
//
|
||||
void
|
||||
PoweredSubsystem::DeathReset(int /*reset_command*/)
|
||||
PoweredSubsystem::DeathReset(int reset_command)
|
||||
{
|
||||
ResetToInitialState(True); // @004b0e6c
|
||||
// #55 step 2: forward the arg (every binary slot-11 body does). At DEATH
|
||||
// (0) the !powered branch idles the electrical alarm -- the authentic wreck
|
||||
// shape; on RESPAWN (1) it returns to Ready with the start timer seeded.
|
||||
ResetToInitialState(reset_command != 0); // @004b0e6c
|
||||
}
|
||||
|
||||
void
|
||||
@@ -1228,9 +1231,12 @@ void
|
||||
// its previous life's generator state.
|
||||
//
|
||||
void
|
||||
Generator::DeathReset(int /*reset_command*/)
|
||||
Generator::DeathReset(int reset_command)
|
||||
{
|
||||
ResetToInitialState(True); // @004b215c
|
||||
// #55 step 2: forward the arg (@004b2164 does). The body brings the
|
||||
// generator ON LINE (outputVoltage = ratedVoltage) either way -- the
|
||||
// binary's shape, and what un-bricks a respawned pilot's energy weapons.
|
||||
ResetToInitialState(reset_command != 0); // @004b215c
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -235,3 +235,11 @@ Logical Seeker::TestInstance() const
|
||||
{
|
||||
return Subsystem::TestInstance();
|
||||
}
|
||||
|
||||
//
|
||||
// DeathReset (#55 step 4) -- the respawn sweep's entry for the Seeker.
|
||||
//
|
||||
void Seeker::DeathReset(int /*reset_command*/)
|
||||
{
|
||||
ResetToInitialState();
|
||||
}
|
||||
|
||||
@@ -154,6 +154,12 @@
|
||||
TestClass(Missile &);
|
||||
void
|
||||
ResetToInitialState();
|
||||
// #55 step 4: the respawn sweep (Mech::Reset -> DeathReset) never
|
||||
// reached this class -- it fell through to the empty Subsystem base, so
|
||||
// this subsystem kept its previous life's state. Chains our own
|
||||
// authentic slot-10 body.
|
||||
void DeathReset(int reset_command);
|
||||
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Construction and Destruction
|
||||
|
||||
@@ -1090,3 +1090,14 @@ Scalar *BTGetTorsoTwistAddr(Subsystem *torso)
|
||||
{
|
||||
return torso ? ((Torso *)torso)->CurrentTwistAddr() : 0;
|
||||
}
|
||||
|
||||
//
|
||||
// DeathReset (#55 step 4) -- the respawn sweep's entry for the Torso. Without
|
||||
// it a respawned mech kept its previous life's twist state and watcher latches
|
||||
// (playtest night 4: "loosing torso twist function after a death", Gitea #70).
|
||||
//
|
||||
void
|
||||
Torso::DeathReset(int /*reset_command*/)
|
||||
{
|
||||
ResetToInitialState(); // @004b5bf8 (takes no arg)
|
||||
}
|
||||
|
||||
@@ -285,6 +285,12 @@ class Joint; // engine skeleton node (JOINT.h); the twist target
|
||||
void WriteUpdateRecord(UpdateRecord *message, int update_model); // slot 7, @004b6a1c (serialize)
|
||||
Logical HandleDeathMessage(Message &message); // slot 9, @004b5be0 -> @004b179c
|
||||
void ResetToInitialState(); // slot 10, @004b5bf8
|
||||
// #55 step 4: the respawn sweep (Mech::Reset -> DeathReset) never
|
||||
// reached this class -- it fell through to the empty Subsystem base, so
|
||||
// this subsystem kept its previous life's state. Chains our own
|
||||
// authentic slot-10 body.
|
||||
void DeathReset(int reset_command);
|
||||
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Construction and Destruction
|
||||
|
||||
Reference in New Issue
Block a user