Reset-based respawn: reuse+heal the mech, not create-a-new-one (task #52)

The respawn glitches (2 mechs, on-fire respawn, camera-inside, can't-control,
wreck-never-disappears) all traced to one architectural divergence: our respawn
SEVERED playerVehicle on death and CREATED a new mech, leaving the old as a
permanent wreck and building a duplicate viewpoint whose old render tree was
never torn down.

The authentic engine (FUN_0049fb74 + RPPlayer) REUSES the same mech entity: on
respawn Mech::Reset heals it and moves it in place. Implemented faithfully,
adapted to our layout (the 1995 raw offsets map to different 2007 engine fields,
so reset the equivalent named members, not the offsets):
- Mech::Reset (real, was a reposition-only stub): reposition + kill dead-reckon
  (projectedOrigin/projectedVelocity/updateVelocity + our relocated gait
  accumulators) so the replicant stops lerping to the death spot; clear the
  death latch (movementMode=1, graphicAlarm=0); Heal every damage zone
  (new Mech__DamageZone::Heal: full structure, intact skin); DeathReset
  (vtable+0x28) every subsystem; ForceUpdate to broadcast.
- btplayer.cpp: VehicleDead no longer severs playerVehicle; the respawn re-post
  gates on the mech still being dead; DropZoneReply resets the EXISTING mech in
  place (heal+move) instead of creating a new one, then fires the warp. Warp
  moved to the shared placement (initial drop-in + respawn).

Verified 2-node: mech entity ID stays 3:22 across 3 deaths (reused, not a new
3:32); each Reset logs alive=1, 20 zones healed, 33 subsystems reset; A sees ONE
mech (no wreck+new pair). Warp fires each respawn.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-09 23:13:44 -05:00
co-authored by Claude Opus 4.8
parent efa7aeb683
commit 247e51e1e1
5 changed files with 196 additions and 69 deletions
+83
View File
@@ -996,6 +996,89 @@ Logical
return graphicAlarm.GetLevel() >= 9 ? True : False;
}
//###########################################################################
// Mech::Reset (@0049fb74) -- the respawn RESET. The authentic respawn REUSES
// the same mech entity: it moves the mech to the new drop-zone origin AND heals
// it back to a clean, alive state. It does NOT create a new mech (our old
// sever-and-create respawn left the wreck behind as a second entity and built a
// duplicate viewpoint -- the source of the "2 mechs / camera-inside / on-fire
// respawn" glitches).
//
// Faithful reproduction of FUN_0049fb74, adapted to OUR layout: the 1995 raw
// offsets the decomp writes (+0x260/+0x298/+0x12c/...) land on DIFFERENT engine
// fields in the 2007 base (projectedOrigin/projectedVelocity/updateOrigin) and
// on our RELOCATED gait accumulators, so we reset the equivalent NAMED members
// rather than the raw offsets (per the databinding rule). Steps mirror the
// decomp: reposition + kill dead-reckon; clear the death latch; heal every
// damage zone; DeathReset (vtable+0x28) every subsystem; ForceUpdate to
// broadcast the reset state to replicants.
//###########################################################################
void
Mech::Reset(const Origin &origin, int mode)
{
Check(this);
// --- reposition (localOrigin @0x100, localToWorld @0xd0 rebuilt) ---
localOrigin = origin;
localToWorld = origin;
// --- kill dead-reckon / replication motion so the reset mech SNAPS to the
// drop zone and its replicant stops lerping back toward the death spot
// (the 2007 Mover-base fields at the decomp's +0x260/+0x298/+0x12c) ---
projectedOrigin = origin;
previousOrigin = origin;
projectedVelocity = Motion::Identity;
updateVelocity = Motion::Identity;
updateAcceleration = Motion::Identity;
// --- our RELOCATED gait/motion accumulators -> identity (the 1995 offsets
// the decomp zeroes map to these named members in our layout) ---
ReconQuatIdentity(&motionDelta, &kIdentityQuat);
ReconQuatIdentity(&worldPose, &kIdentityQuat);
ReconQuatIdentity(&worldPoseBase, &kIdentityQuat);
ReconQuatIdentity(&angularAccum, &kIdentityQuat);
ReconQuatIdentity(&aimRate, &kIdentityQuat);
// --- CLEAR THE DEATH LATCH: alive gait mode + no going-down alarm, so
// IsMechDestroyed() is false and the drive/render treat it as alive ---
movementMode = 1; // ground/alive (death settled it to 9)
graphicAlarm.SetLevel(0); // clear >=9 (the vital-kill trigger)
// --- HEAL every damage zone: full structure, intact skin, no burning ---
for (int z = 0; z < damageZoneCount; ++z)
{
Mech__DamageZone *zone = Zone(z);
if (zone != 0)
zone->Heal();
}
// --- RESET every subsystem (the decomp's loop-2 vtable+0x28 = DeathReset,
// the base virtual symmetric with the DeathShutdown we run on death):
// restores each subsystem's heat/power/ammo/charge to the initial state ---
for (int i = 0; i < GetSubsystemCount(); ++i)
{
Subsystem *s = GetSubsystem(i);
if (s != 0)
s->DeathReset(mode);
}
// --- locomotion pre-run + interest gates (a reset master must tick) ---
SetPreRunFlag();
if (interestCount == 0) interestCount = 1;
// --- broadcast the reset state to replicants (FUN_004a4c54(this, 0x1f)) ---
ForceUpdate();
if (getenv("BT_DEATH_LOG"))
DEBUG_STREAM << "[respawn] Mech::Reset " << GetEntityID()
<< " healed+moved to (" << localOrigin.linearPosition.x << ","
<< localOrigin.linearPosition.y << "," << localOrigin.linearPosition.z
<< ") alive=" << (int)(!IsMechDestroyed())
<< " zones=" << damageZoneCount
<< " subsys=" << GetSubsystemCount() << "\n" << std::flush;
Check_Fpu();
}
void
Mech::UpdateDeathState(Scalar dt)
{