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
+94 -37
View File
@@ -321,17 +321,25 @@ void
if (message->deathCount != -1)
{
//
// The 5s respawn re-post (>=0) or the engine's 2s probe (-2). A
// vehicle in hand means the drop zone was acquired -> nothing to do.
// (This branch also kept the old solo bring-up honest: the initial
// spawn's -2 probes land here and are eaten.)
// The 5s respawn re-post (>=0) or the engine's 2s probe (-2). RESET-BASED
// respawn: the mech STAYS as playerVehicle through death (we no longer
// sever it), so "has a vehicle" no longer means "acquired a drop zone".
// Gate on whether the mech is still DEAD -- if it's alive again (already
// reset by a prior reply), this re-post is moot; if still dead, fall
// through to the drop-zone hunt so it gets a spot to reset into.
//
if (playerVehicle != 0)
{
deathPending = 0; // this+0x290
suppressConsole = 0; // this+0x258
Check_Fpu();
return;
Mech *mech = (Mech *)playerVehicle;
Logical mech_dead = (mech != 0
&& mech->GetClassID() == Mech::MechClassID
&& mech->IsMechDestroyed());
if (!mech_dead)
{
deathPending = 0; // this+0x290
suppressConsole = 0; // this+0x258
Check_Fpu();
return;
}
}
if (getenv("BT_SCORE_LOG"))
@@ -411,13 +419,15 @@ void
killerName = SelfDestructName; // this+0x1d0
//
// Sever the vehicle link. @004c05c4 powers the roster down through
// @0049fe0c (the slot-11 DeathShutdown broadcast -- our mech's own death
// transition already runs that pass) and clears this+0x1fc. The wreck
// stays registered in the world; only the player's claim on it ends --
// which is what lets DropZoneReply create the replacement.
// RESET-BASED respawn: do NOT sever playerVehicle. The authentic respawn
// REUSES the same mech entity -- it stays our vehicle (a dead wreck) and is
// later healed + moved in place by Mech::Reset (DropZoneReply). Severing it
// (the old model) orphaned the wreck as a second entity and made the reply
// build a NEW viewpoint mech, which is what produced the "2 mechs / camera-
// inside / on-fire respawn" glitches. The ghost-drive is already gated by
// the input zero on IsMechDestroyed (mech4.cpp), so keeping the pointer is
// safe. (The dead mech keeps ticking as a frozen wreck until reset.)
//
playerVehicle = 0; // this+0x1fc
//
// If lives remain (solo/dev: no role -> always), re-post to ourselves at
@@ -828,6 +838,28 @@ void
HostManager *host_manager = application->GetHostManager();
Check(host_manager);
// TEST HOOK (BT_SPAWN_XZ="x,z"): force the spawn/respawn to a fixed ground
// position (X/Z; Y stays the drop-zone ground height, the ground model
// snaps it). The map's drop zones are picked ~randomly and land the two
// -net instances ~1500u apart -- hard to find each other. Set the same
// base on both pods with a small offset so they spawn side by side.
{
const char *xz = getenv("BT_SPAWN_XZ");
if (xz != 0)
{
float sx = 0.0f, sz = 0.0f;
if (sscanf(xz, "%f,%f", &sx, &sz) == 2)
{
mech_location.linearPosition.x = sx;
mech_location.linearPosition.z = sz;
if (getenv("BT_MP_LOG"))
DEBUG_STREAM << "[spawn] BT_SPAWN_XZ override -> ("
<< sx << ", " << mech_location.linearPosition.y
<< ", " << sz << ")\n" << std::flush;
}
}
}
//
// Make any MUNGA-level vehicle (camera ship for an observer player).
//
@@ -1020,6 +1052,25 @@ void
<< " deathCount=" << deathCount << " msgDeath=" << message->deathCount
<< " state=" << (int)GetSimulationState() << "\n" << std::flush;
// TEST HOOK (BT_SPAWN_XZ="x,z"): force the drop-zone X/Z so two -net instances
// land in the same area (map zones are ~random, ~1500u apart -- hard to find
// each other). Applied to the MESSAGE so BOTH the CreatePlayerVehicle make
// AND the Mech::Reset placement below use it (Reset re-places at
// message->dropZoneLocation, which would otherwise undo the override). Y
// stays the drop-zone ground height; the ground model snaps it.
{
const char *xz = getenv("BT_SPAWN_XZ");
if (xz != 0)
{
float sx = 0.0f, sz = 0.0f;
if (sscanf(xz, "%f,%f", &sx, &sz) == 2)
{
message->dropZoneLocation.linearPosition.x = sx;
message->dropZoneLocation.linearPosition.z = sz;
}
}
}
if (!playerVehicle) // param_1[0x7f] == 0
{
CreatePlayerVehicle(message->dropZoneLocation); // (**vtable+0x40)(origin)
@@ -1047,19 +1098,7 @@ void
AlwaysExecute(); // param_1[10] &= ~2 (run every frame)
deathCount = 0; // param_1[0x80]
//
// WARP (task #52): fire the translocation sphere at our reinsertion.
// Self-contained render one-shot at the drop-zone position -- it does NOT
// touch the player's SimulationState (that dial drives the camera/POV +
// targeting in our reconstruction; pulsing it for the sphere trigger
// regressed all of those). The effect plays its own collapse->expand.
//
{
const Point3D &p = message->dropZoneLocation.linearPosition;
extern void BTStartWarpEffect(float x, float y, float z);
BTStartWarpEffect((float)p.x, (float)p.y, (float)p.z);
}
// (warp fired in the shared placement below -- initial drop-in + respawn)
}
else if (deathCount == message->deathCount) // param_2[0xe] == param_1[0x80]
{
@@ -1068,15 +1107,23 @@ void
Check_Fpu();
return;
}
if (GetSimulationState() != DropZoneAcquiredState) // state != 1
//
// RESPAWN (reset-based): the mech STAYS ours through death; this reply
// (deathCount matches) resets the EXISTING dead mech in place -- fall
// through to the placement below (Mech::Reset heals + moves it). Guard
// against a duplicate reply / the 2s probe: if the mech is already alive
// again, there is nothing to reset. (The old path pulsed
// DropZoneAcquiredState and looped through the dead handler to build a
// NEW mech -- removed; that state drives our camera/targeting.)
//
Mech *mech = (Mech *)playerVehicle;
if (!(mech != 0 && mech->GetClassID() == Mech::MechClassID
&& mech->IsMechDestroyed()))
{
//
// Reset-after-death: BT routes this through the death handler
// (FUN_004bffd0 calls FUN_004c012c here).
//
VehicleDeadMessageHandler((VehicleDeadMessage *)message);
Check_Fpu();
return;
}
// else: still dead -> fall through to placement (reset + warp)
}
else // stale / old message
{
@@ -1084,14 +1131,24 @@ void
}
//
// Place (or re-place) the mech at the drop-zone origin and raise the
// "translocated" cockpit alarm.
// Place the mech at the drop-zone origin: heal + move the SAME entity
// (Mech::Reset), raise the "translocated" cockpit alarm, and fire the
// translocation-sphere warp. Runs for the initial drop-in (fresh mech) and
// for a respawn (dead mech healed) alike.
//
Set_Alarm_Level((char *)this + 0x2c, 2); // FUN_0041bbd8(this+0x2c, 2)
if (playerVehicle->GetClassID() == Mech::MechClassID) // 0xbb9
{
Mech *mech = (Mech *)playerVehicle;
mech->Reset(message->dropZoneLocation, 1 /* True */); // FUN_0049fb74
mech->Reset(message->dropZoneLocation, 1 /* True */); // FUN_0049fb74 (heal+move)
// WARP (task #52): the translocation sphere at our reinsertion. A
// self-contained render one-shot -- it does NOT touch SimulationState
// (that dial drives the camera/POV + targeting; pulsing it regressed all
// of those). The effect plays its own collapse->expand.
const Point3D &p = message->dropZoneLocation.linearPosition;
extern void BTStartWarpEffect(float x, float y, float z);
BTStartWarpEffect((float)p.x, (float)p.y, (float)p.z);
}
Check_Fpu();
}