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:
co-authored by
Claude Opus 4.8
parent
efa7aeb683
commit
247e51e1e1
@@ -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();
|
||||
}
|
||||
|
||||
@@ -161,38 +161,10 @@ void Mech::RaiseStatusAlarm(int /*alarm_id*/)
|
||||
{
|
||||
}
|
||||
|
||||
// Mech::Reset (@0049fb74) -- place the mech at a (re)spawn origin.
|
||||
// The shipped Reset additionally zeroes ~50 heat/damage/motion state fields and
|
||||
// resets every subsystem in the roster (slot vtable+0x28 with the reset mode);
|
||||
// that full sweep is deferred (TODO(bring-up)). For first spawn a freshly
|
||||
// constructed mech is already clean, so positioning it + refreshing the world
|
||||
// transform + forcing an update is enough to get it placed and rendering.
|
||||
void Mech::Reset(const Origin &origin, int /*mode*/)
|
||||
{
|
||||
Check(this);
|
||||
localOrigin = origin; // this+0x100 (FUN_0040a938 copy)
|
||||
localToWorld = localOrigin; // this+0xd0 (FUN_0040ab44 rebuild)
|
||||
ForceUpdate(); // FUN_004a4c54 family -- push to renderer/replicants
|
||||
|
||||
// BRING-UP (Tier-2 locomotion): get the player mech onto the per-frame
|
||||
// simulation path so it can walk. Two engine gates block a freshly spawned
|
||||
// master entity from being executed:
|
||||
// (1) Entity::Execute only calls PerformAndWatch when the app is in
|
||||
// RunningMission/EndingMission OR the entity IsPreRunnable()
|
||||
// (ENTITY.cpp:~549) -> set the pre-run flag.
|
||||
// (2) UpdateManager::Execute skips a master unless IsInteresting()
|
||||
// (interestCount!=0) && IsNonReplicantExecutable() (UPDATE.cpp:148)
|
||||
// -> force it interesting so the update manager ticks it.
|
||||
// Without these the mech renders but is never simulated (static
|
||||
// localToWorld). See Mech::PerformAndWatch (mech4.cpp).
|
||||
SetPreRunFlag();
|
||||
if (interestCount == 0) interestCount = 1; // Entity::interestCount (public)
|
||||
DEBUG_STREAM << "[drive] Mech::Reset spawn at ("
|
||||
<< localOrigin.linearPosition.x << ", "
|
||||
<< localOrigin.linearPosition.y << ", "
|
||||
<< localOrigin.linearPosition.z << ") -- prerun+interest forced\n" << std::flush;
|
||||
Check_Fpu();
|
||||
}
|
||||
// Mech::Reset (@0049fb74) is now the real faithful reconstruction in mech4.cpp
|
||||
// (reposition + kill dead-reckon + clear death/damage + ResetToInitialState every
|
||||
// subsystem + heal every damage zone + ForceUpdate) -- no longer the reposition-
|
||||
// only stub that lived here.
|
||||
|
||||
// (Mech__DamageZone::LoadCriticalSubsystems was a no-op stub here; it is now the
|
||||
// real type-0x1e damage-state descriptor-table loader in mechdmg.cpp.)
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -921,6 +921,18 @@ void
|
||||
SetGraphicState(gstate); // engine DamageZone::SetGraphicState -> destroyed skin
|
||||
}
|
||||
|
||||
void
|
||||
Mech__DamageZone::Heal()
|
||||
{
|
||||
// Respawn heal: restore this zone to the undamaged/intact state the loader
|
||||
// builds -- full structure, intact skin, no burning/destroyed flag. The
|
||||
// authentic Mech::Reset (@0049fb74) heals via each subsystem's
|
||||
// ResetToInitialState; the zone's structural state is restored here.
|
||||
damageLevel = StructureMin; // 0.0 -- full structure (this+0x158)
|
||||
ApplyDamageGraphicState(ExistsGraphicState); // 0 -- intact skin (undo Destroyed/Gone)
|
||||
SetDamageZoneState(0); // clear burning / vital-destroyed state
|
||||
}
|
||||
|
||||
//#############################################################################
|
||||
// MechDeathHandler -- @0042a984 / @0042aa2c / @0042a9f4
|
||||
//#############################################################################
|
||||
|
||||
@@ -277,6 +277,9 @@
|
||||
// destroyed-skin swap the renderer reads (public wrapper so MechDeathHandler
|
||||
// can drive it; the base SetGraphicState is otherwise unreachable from it).
|
||||
void ApplyDamageGraphicState(int gstate);
|
||||
// Restore this zone to full structure + intact skin -- the respawn heal
|
||||
// (Mech::Reset, @0049fb74 resets every subsystem's structure).
|
||||
void Heal();
|
||||
protected:
|
||||
std::vector<DamageDescriptor>
|
||||
damageDescriptors; // binary this+0xd4 collection
|
||||
|
||||
Reference in New Issue
Block a user