diff --git a/engine/MUNGA/SUBSYSTM.h b/engine/MUNGA/SUBSYSTM.h index 1a29bc8..665e9ff 100644 --- a/engine/MUNGA/SUBSYSTM.h +++ b/engine/MUNGA/SUBSYSTM.h @@ -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); + } }; //########################################################################## diff --git a/game/reconstructed/ammobin.cpp b/game/reconstructed/ammobin.cpp index 2a4dc23..8182996 100644 --- a/game/reconstructed/ammobin.cpp +++ b/game/reconstructed/ammobin.cpp @@ -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 } //############################################################################# diff --git a/game/reconstructed/btplayer.cpp b/game/reconstructed/btplayer.cpp index 8f65cd7..b331801 100644 --- a/game/reconstructed/btplayer.cpp +++ b/game/reconstructed/btplayer.cpp @@ -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] diff --git a/game/reconstructed/gyro.cpp b/game/reconstructed/gyro.cpp index df238fd..6aaab3b 100644 --- a/game/reconstructed/gyro.cpp +++ b/game/reconstructed/gyro.cpp @@ -1083,3 +1083,12 @@ void GyroApplyDamageTorque(Subsystem *gyro, Scalar x, Scalar y, Scalar z, Scalar if (gyro != 0) static_cast(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) +} diff --git a/game/reconstructed/gyro.hpp b/game/reconstructed/gyro.hpp index 4f90f72..6544afb 100644 --- a/game/reconstructed/gyro.hpp +++ b/game/reconstructed/gyro.hpp @@ -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 diff --git a/game/reconstructed/heat.cpp b/game/reconstructed/heat.cpp index 2e09b51..6b1b8e1 100644 --- a/game/reconstructed/heat.cpp +++ b/game/reconstructed/heat.cpp @@ -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 diff --git a/game/reconstructed/hud.cpp b/game/reconstructed/hud.cpp index 9fe3e3f..8d1299a 100644 --- a/game/reconstructed/hud.cpp +++ b/game/reconstructed/hud.cpp @@ -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 +} diff --git a/game/reconstructed/hud.hpp b/game/reconstructed/hud.hpp index 20d9ec7..e251e0e 100644 --- a/game/reconstructed/hud.hpp +++ b/game/reconstructed/hud.hpp @@ -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 diff --git a/game/reconstructed/mech.cpp b/game/reconstructed/mech.cpp index a2878f5..d8c398a 100644 --- a/game/reconstructed/mech.cpp +++ b/game/reconstructed/mech.cpp @@ -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 diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 0600ee4..97b3560 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -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 diff --git a/game/reconstructed/myomers.cpp b/game/reconstructed/myomers.cpp index d3bb3f5..ab0acdf 100644 --- a/game/reconstructed/myomers.cpp +++ b/game/reconstructed/myomers.cpp @@ -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 +} diff --git a/game/reconstructed/myomers.hpp b/game/reconstructed/myomers.hpp index 13e1c51..3b68ced 100644 --- a/game/reconstructed/myomers.hpp +++ b/game/reconstructed/myomers.hpp @@ -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 diff --git a/game/reconstructed/powersub.cpp b/game/reconstructed/powersub.cpp index c70ac73..f261fa6 100644 --- a/game/reconstructed/powersub.cpp +++ b/game/reconstructed/powersub.cpp @@ -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 } // diff --git a/game/reconstructed/seeker.cpp b/game/reconstructed/seeker.cpp index 1d658bf..7b65922 100644 --- a/game/reconstructed/seeker.cpp +++ b/game/reconstructed/seeker.cpp @@ -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(); +} diff --git a/game/reconstructed/seeker.hpp b/game/reconstructed/seeker.hpp index 224cab0..bff7545 100644 --- a/game/reconstructed/seeker.hpp +++ b/game/reconstructed/seeker.hpp @@ -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 diff --git a/game/reconstructed/torso.cpp b/game/reconstructed/torso.cpp index b23a87c..a33c3a4 100644 --- a/game/reconstructed/torso.cpp +++ b/game/reconstructed/torso.cpp @@ -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) +} diff --git a/game/reconstructed/torso.hpp b/game/reconstructed/torso.hpp index 2445e2d..2e85cfc 100644 --- a/game/reconstructed/torso.hpp +++ b/game/reconstructed/torso.hpp @@ -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