diff --git a/game/reconstructed/btplayer.cpp b/game/reconstructed/btplayer.cpp index ab0909a..788762c 100644 --- a/game/reconstructed/btplayer.cpp +++ b/game/reconstructed/btplayer.cpp @@ -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(); } diff --git a/game/reconstructed/btstubs.cpp b/game/reconstructed/btstubs.cpp index c19458a..23f79f1 100644 --- a/game/reconstructed/btstubs.cpp +++ b/game/reconstructed/btstubs.cpp @@ -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.) diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index b673673..b8bbb47 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.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) { diff --git a/game/reconstructed/mechdmg.cpp b/game/reconstructed/mechdmg.cpp index 80468c4..bc27ecb 100644 --- a/game/reconstructed/mechdmg.cpp +++ b/game/reconstructed/mechdmg.cpp @@ -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 //############################################################################# diff --git a/game/reconstructed/mechdmg.hpp b/game/reconstructed/mechdmg.hpp index a129f8c..9544c1f 100644 --- a/game/reconstructed/mechdmg.hpp +++ b/game/reconstructed/mechdmg.hpp @@ -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 damageDescriptors; // binary this+0xd4 collection