diff --git a/context/combat-damage.md b/context/combat-damage.md index 8546056..9815e24 100644 --- a/context/combat-damage.md +++ b/context/combat-damage.md @@ -316,6 +316,19 @@ to reach its latch; and spawned NON-player mechs don't advance their body anim a (the exported per-subsystem destroyed-skin + explosion engine, `FUN_0042a984`/`FUN_0042aa2c`) is still the no-op stub → no death explosions/skin-swaps yet. (c) the whole-mech **DeathSplash** radius damage (un-exported). +### ⚠ Death-latch correction (2026-07-09, task #52) [T1→T2] +`IsMechDestroyed()` originally tested **`graphicAlarm >= 9` alone** — a bring-up shortcut that made a +wreck **resurrectable**: `graphicAlarm` is a STATUS indicator, and a later leg hit on the wreck +legitimately rewrites it to 4/3 (`SetDamageState`, mechdmg.cpp:417/419) → `IsMechDestroyed()` flipped +false → the drive's guarded `movementMode = 1` write revived the gait → the NEXT vital hit re-ran the +ENTIRE death transition (double kill-score, double `VehicleDead(-1)`; during the respawn window the +re-entered `BTPostKillScore` → `Player::ScoreMessageHandler`'s `Check(playerVehicle)` **abort()** — engine +MUNGA TUs compile with `Check` ACTIVE; caught live under cdb, 2-node force-damage). The AUTHENTIC latch +is the death `movementMode`: `IsDestroyed == (movementMode 2 || 9)` (`FUN_0049fb54`/`FUN_004ab1c8`) — +the death modes never revert. Fix: `IsMechDestroyed()` = `movementMode 2||9` (latch) **OR** +`graphicAlarm >= 9` (the vital-kill TRIGGER that first enters the transition). Lesson: **a gauge/status +alarm is never a state latch** — later writers rewrite it; latch on the state machine's own mode. + ## Key Relationships - Weapons/roster: [[subsystems]]. Aim source: [[locomotion]] (drive/facing). Effects: [[rendering]]. - P5 forensics: `docs/HARD_PROBLEMS.md`. Data: [[decomp-reference]] §4-5. diff --git a/context/multiplayer.md b/context/multiplayer.md index dca9ab3..388a76a 100644 --- a/context/multiplayer.md +++ b/context/multiplayer.md @@ -182,15 +182,42 @@ transition, HUD all landed since P6): console egg → mesh → RunningMission on wreck SWAP does appear on the killer's screen, but the death SINK/burial does not (the collapse+ burial runs only in the owning pod's death transition). Fix path: transcribe the recovered Mech-level WriteUpdateRecord death/knockdown record types (5/6 — disasm preserved, task #51). -3b. **Respawn cycle** (task #52): a dead player currently stays dead (and pre-gate could ghost-drive - the wreck — fixed: the drive input zeroes on IsMechDestroyed, input-only so the collapse still - plays). The authentic respawn (user recollection: a BLUE WHIRLWIND recovers the dead mech, then a - re-drop) — scouting: no whirlwind strings in the exe; BTL4.RES type-12 `*int.scp` = per-mech INTRO - (arrival/drop) scripts but they are BINARY ref-records {ver, off, size, refCount, refIds} pointing - at other resources (not text like the type-13 explosion scripts); `adrop` (id 1994) sits in the - MAP type (14). The machinery: death detection (BTPlayer) → recovery/drop effect → AssignDropZone → - Mech::Reset (@0049fb74, the respawn placement — its full subsystem-reset sweep is still a - bring-up TODO) → roster ResetToInitialState. +3b. **Respawn cycle** (task #52) — the AUTHENTIC cycle is now reconstructed [T1 @004c05c4 + + engine PLAYER.cpp]: mech death transition (mech4.cpp) dispatches `VehicleDead` (ctor default + `deathCount = -1` = the death notification) to `GetPlayerLink()` → `BTPlayer:: + VehicleDeadMessageHandler` (@004c05c4 structure): MissionEnding(state 4) skips all; -1 → + dedup on deathPending, ++deathCount, debit `scenarioRole` life, sever `playerVehicle` + (the WRECK entity stays), re-post the same message at **now + 5.0 s** (5.0f @004c0830) + stamped with deathCount; out of lives → the binary posts msgID 0x18 at +10 s (mission + review; deferred). deathCount ≥ 0 / -2 with a vehicle in hand = acquired (clears + deathPending); without one → engine base @0042db80 (PLAYER.cpp:214: closest non-"win" + DropZone → AssignDropZone → 2s re-post probe) → DropZoneReply → CreatePlayerVehicle (a NEW + mech; `deathCount = 0`) + viewpoint relink (Verify(viewpointEntity==NULL) is compiled out at + DEBUG_LEVEL 0 — release overwrites, same as the 1995 build). @0049fe0c = roster-wide + slot-11 DeathShutdown broadcast (the death transition already runs that pass). + CORRECTIONS to the earlier scouting: BTL4.RES **type 12 = InternalAudioStream** (the engine's + own resource-type table) — `*int.scp` are internal-AUDIO scripts, NOT per-mech intro scripts; + and `adrop` (id 1994) is a **type-14 MakeMessageStream** = the map's four DropZone make + records (0xE0 each: classID 3, origin, facing, name "two"/"three"/…), i.e. the respawn + infrastructure, not a whirlwind. The whirlwind/recovery VISUAL remains unfound: no + whirlwind/tornado/vortex-named asset in BTL4.RES (ModelList/VideoModel/GameModel/Animation + sweeps) and no vortex-like .PFX; still open. Mech::Reset (@0049fb74) full subsystem-reset + sweep is still a bring-up TODO. + **Three respawn bugs found + fixed on the way (all [T2], 2-node force-damage verified):** + (i) **death-latch un-latch** — `IsMechDestroyed` tested `graphicAlarm>=9` alone; a later leg hit + on the wreck rewrites the alarm to 4/3, un-latching → the death transition RE-RAN (double kill, + double `VehicleDead(-1)`). Fixed to latch on `movementMode 2||9` (see combat-damage.md + "Death-latch correction"). (ii) **score into severed vehicle** — `BTPlayer::ScoreMessageHandler` + / `ScoreInflictedMessageHandler` deref `playerVehicle` unguarded; during the 5s dead window it is + 0 → guard added (apply the award, skip the vehicle response). (iii) **renderer LoadMission + re-entry** — the KILLER bug: `MakeAndLinkViewpointEntity` (APP.cpp:1265) calls + `videoRenderer->LoadMission` on EVERY viewpoint-make, so the respawn re-runs + `DPLReadEnvironment`→`DPLReadINIPage`; its light block `Fail`s ("All lights must be defined on a + single INI page") because `sceneLightCount` was never reset between reads (ctor-only init). Fixed + in L4VIDEO.cpp `DPLReadEnvironment`: tear down the prior mission's `sceneLight[]` + + `sceneLightCount=0` before re-reading (the per-page invariant is intra-read only). This is a + PORT-layer re-entrancy gap in the 2007 renderer, not BT logic. Caught via cdb `bp ucrtbased!abort` + — a `Fail`/`Check` is an abort() MessageBox, not an AV, so `sxe av` misses it. 4. ✅ **Cross-pod beam visuals — DONE (task #51, 2026-07-09).** The keepalive theory needed one correction: `FUN_0041c350` = (a) queue the LOCAL deferred beam-effect callback (@0x4bac0c) on the app+0x34 manager (our per-weapon render walk plays that role) AND (b) set the subsystem DIRTY bit — diff --git a/context/reconstruction-gotchas.md b/context/reconstruction-gotchas.md index b744d92..056539e 100644 --- a/context/reconstruction-gotchas.md +++ b/context/reconstruction-gotchas.md @@ -145,6 +145,17 @@ real PoweredSubsystem). Keep the alloc SIZE + special-cache when swapping a fact Hit by: the spawned dummy, replicants, AND — task #47 — a peer's own **MASTER** mech: cross-pod TakeDamage reached B, resolved to B's real mech, then `Entity::Receive` saw `valid=0` and deferred it forever → 0 damage. Fix = `Mech::Make` sets `ValidFlag` for the master too (mech.cpp), not just replicants. [T2] +- **Never send a NON-Entity message through `Entity::Dispatch`.** `Entity::Dispatch` (ENTITY.cpp:236) + unconditionally stamps `message->entityID`/`interestZoneID` at the **Entity::Message** field offsets + (after Receiver::Message's 12-byte header). A `NetworkClient::Message` (the console + `ConsolePlayer*Message` family) has no such fields and is SMALLER — those stamps write PAST the + object. On a stack-allocated console message that is an `/RTC1` stack-guard overflow → `_RTC_StackFailure` + → abort (caught on the respawned player's first score flush, task #52). Console/network messages go + over the stream: `application->SendMessage(host->GetHostID(), NetworkClient::ConsoleClientID, &msg)` + (which forwards to `networkManager->Send` with no entity stamping) — mirror the working VTV-damaged + push in `ScoreMessageHandler`, don't call the player's `Dispatch`. (Entity::Dispatch's `messageID < + Receiver::NextMessageID` early branch does NOT save you — it lacks a `return`, and the console IDs + aren't in that range anyway.) [T2] ## 10. Container-Execute must override (gauges) @@ -180,6 +191,15 @@ AVs. Fill gaps with a shared read-only pad member. Same for a class's `Att be initialized in EVERY ctor init-list (debug heap fills 0xCDCDCDCD → an uninit flag reads TRUE); and any device state a special draw path sets must be save/restored exactly. Deleting stale `.obj`s fixes layout-mismatch corruption when a base class grows. [T2] +- **Status alarm is not a latch:** gauge/status alarms (`graphicAlarm` etc.) are INDICATORS whose + level later events legitimately REWRITE (a leg hit on a wreck rewrites 9→4/3). A predicate like + `IsMechDestroyed = alarm>=9` un-latches → the wreck "resurrects" and the death transition re-runs + (double score, abort in the respawn window). Latch on the state machine's own mode + (`movementMode 2||9`); use the alarm only as the entry TRIGGER. (Task #52.) [T2] +- **Engine `Check`/`Verify` are ACTIVE in MUNGA TUs:** a NULL hitting an engine `Check(ptr)` is an + ucrtbased **abort() dialog** ("Debug Error!"), not an AV — `sxe av` won't break there; the box + blocks the event loop (a headless node just "stops logging"). cdb: run with a config that does + `g` then `kb 40` — the int3 lands ON the aborting thread. [T2] --- diff --git a/engine/MUNGA_L4/L4VIDEO.cpp b/engine/MUNGA_L4/L4VIDEO.cpp index 5a915d3..f485fe7 100644 --- a/engine/MUNGA_L4/L4VIDEO.cpp +++ b/engine/MUNGA_L4/L4VIDEO.cpp @@ -4706,6 +4706,24 @@ void mObjectPaths.clear(); mMaterialPaths.clear(); mTexmapPaths.clear(); + // BT (task #52): LoadMission runs ONCE PER VIEWPOINT-MAKE, not once per + // mission -- MakeAndLinkViewpointEntity (APP.cpp:1265) re-links the renderer + // and re-reads the environment every time a new viewpoint mech is built, + // which the RESPAWN path does. The light block in DPLReadINIPage Fails ("All + // lights must be defined on a single INI file page") when sceneLightCount is + // already non-zero -- true on the second read, since neither the ctor-only + // init nor DPLReadINIPage ever clears it. That invariant is meant to catch + // lights SPLIT ACROSS pages WITHIN one read; across separate reads the state + // must reset. Tear the prior mission's lights down so the re-read is clean. + if (sceneLight != NULL) + { + if (mDevice != NULL) + for (int li = 0; li < sceneLightCount; ++li) + mDevice->LightEnable(li, FALSE); + delete [] sceneLight; + sceneLight = NULL; + } + sceneLightCount = 0; if (renderer_environment->PageExists("main")) { if (!(renderer_environment->GetLogicalEntry("main", "debug", &debug_printing))) diff --git a/game/reconstructed/btplayer.cpp b/game/reconstructed/btplayer.cpp index 697fcf9..f18ddc1 100644 --- a/game/reconstructed/btplayer.cpp +++ b/game/reconstructed/btplayer.cpp @@ -203,7 +203,7 @@ static_assert(sizeof(BTPlayer::MakeMessage) == 0xD0, "BTPlayer::MakeMessage wire size must be 0xD0"); static const char *SelfDestructName = "self destruct"; // &DAT_00524b38 -static const Scalar ResetDelay = 1.0f; // re-post delay +static const Scalar RespawnDelay = 5.0f; // death -> drop-zone hunt (@004c0830) static const Scalar TicksPerSecond = 1.0f; // (see note in PlayerSimulation) // @@ -281,12 +281,27 @@ BTPlayer::SharedData //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // VehicleDeadMessageHandler // -// @004c012c (file=? in the index, but operates on the BTPlayer object and -// is reached through the "VehicleDead" handler slot). Records the death, -// stamps the outgoing message with our death count, debits one "return from -// death" against the role, raises the console alarm, remembers the killer's -// name and re-posts the message a short time later so we can hunt for a new -// drop zone. +// @004c05c4 The BT death/respawn cycle (task #52). Three message flavors +// arrive here, keyed on message->deathCount: +// +// -1 The immediate DEATH NOTIFICATION (the ctor default), dispatched +// by the vehicle's death transition (mech4.cpp). Bookkeeping, +// sever playerVehicle (the wreck entity STAYS in the world) and +// re-post this message to ourselves at +5 seconds (5.0f +// @004c0830) stamped with the new death count. +// >= 0 Our own 5-second re-post: hand it to the engine base handler +// (@0042db80, PLAYER.cpp:214) which hunts the closest drop zone, +// dispatches AssignDropZone and posts the 2s -2 probe. The +// DropZoneReply then CREATEs the replacement mech. +// -2 The engine's 2s "did the reply arrive?" probe. If the player +// has a vehicle again the drop zone WAS acquired -> moot. +// +// The binary also runs killer/victim score bumps (+0x280 via @0042e580) here +// -- our kill scoring already flows through BTPostKillScore at the death +// transition, so it is not duplicated -- and, when the role is out of lives, +// posts the mission-review message (id 0x18) at +10 s instead of respawning +// (deferred with the mission-flow work). @004c012c is a sibling retry +// helper reached from DropZoneReply. // void BTPlayer::VehicleDeadMessageHandler(VehicleDeadMessage *message) @@ -295,36 +310,87 @@ void Check(message); // - // scoring wave (DEATHS fix): @004c012c is the drop-zone RESPAWN-RETRY helper -- - // the engine's RequestDropZone (PLAYER.cpp:400) posts a 2s-delayed VehicleDead - // (deathCount=-2) as a "did the drop zone reply?" timer. If the player already - // has a vehicle, the drop zone WAS acquired (the DropZoneReply created it) -> the - // retry is moot: don't count a death and don't re-post. The prior unconditional - // ++deathCount + re-post was an INFINITE loop in solo (the drop-zone handshake - // never "completes" in the bring-up), climbing deathCount ~1/s. (A real MP death - // -- vehicle destroyed -> playerVehicle cleared/the @004c05c4 path -- is deferred.) + // Mission teardown swallows the whole cycle. ([this+0x40] == 4 skip.) // - if (playerVehicle != 0) + if (GetSimulationState() == MissionEndingState) { - deathPending = 0; // this+0x290 - suppressConsole = 0; // this+0x258 Check_Fpu(); return; } - if (getenv("BT_SCORE_LOG")) - DEBUG_STREAM << "[score] VehicleDeadMessageHandler: deathCount " << deathCount - << " -> " << (deathCount + 1) << "\n" << std::flush; + 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.) + // + if (playerVehicle != 0) + { + deathPending = 0; // this+0x290 + suppressConsole = 0; // this+0x258 + Check_Fpu(); + return; + } + + if (getenv("BT_SCORE_LOG")) + DEBUG_STREAM << "[score] VehicleDead: drop-zone hunt (msgDeath=" + << message->deathCount << " deathCount=" << deathCount + << ")\n" << std::flush; + + // + // Respawning needs a drop zone. A dev mission without a "DropZones" + // group would hit the engine base's ACTIVE Check(dropzones) -> an + // abort() dialog; no zones -> no respawn (stay dead), honestly logged. + // + { + EntityManager *entity_manager = application->GetEntityManager(); + if (entity_manager == 0 + || entity_manager->FindGroup("DropZones") == 0) + { + if (getenv("BT_SCORE_LOG")) + DEBUG_STREAM << "[score] VehicleDead: no DropZones group -- " + "respawn unavailable in this mission\n" << std::flush; + Check_Fpu(); + return; + } + } + + // + // No vehicle: find/ask a drop zone for a spot. The base gates on + // message->deathCount == deathCount, so stale re-posts from an + // earlier death fall through harmlessly. + // + Player::VehicleDeadMessageHandler(message); // engine base @0042db80 + return; + } // - // One more death; tell the message (so replicants stay in sync) and - // debit a life against the scoring role. + // ======== deathCount == -1: the immediate death notification ========== + // Dedup on deathPending -- one death, one cycle (cleared when a vehicle + // is back in hand, above). + // + if (deathPending != 0) + { + Check_Fpu(); + return; + } + deathPending = 1; // this+0x290 + + if (getenv("BT_SCORE_LOG")) + DEBUG_STREAM << "[score] VehicleDead(-1): death #" << (deathCount + 1) + << ", respawn hunt in 5s\n" << std::flush; + + // + // One more death; stamp the message (the base handler's identity gate) + // and debit a life against the scoring role. // ++deathCount; // this+0x200 message->deathCount = deathCount; // message+0x38 // TODO(bring-up): scenarioRole is the scoring role looked up from the mission's // role registry (btplayer.cpp:815, currently deferred), so it is NULL for the - // minimal TEST.EGG dev mission. Only debit a life when a role is present. + // minimal dev missions. Only debit a life when a role is present. if (scenarioRole != 0) // this[0x208] { scenarioRole->SetReturnFromDeath( // --*(this[0x208]+0x28) @@ -335,30 +401,36 @@ void Set_Alarm_Level((char *)this + 0x2c, 1); // FUN_0041bbd8(this+0x2c, 1) // - // Remember who killed us (for the pilot HUD). If the kill came from a - // real attacker the message carries its name (+0x1c); otherwise fall - // back to the canned "self destruct" string (@00524b38). + // Remember who killed us (for the pilot HUD). The 1995 message is an + // extended VehicleDeadMessage carrying the killer's name (the binary + // reads message+0x1c); our compiled Entity::Message header is a + // different size, so that raw offset lands inside the header -- a + // garbage pointer. Until the pilot-roster wave defines the extended + // message, every death reads as the canned string (@00524b38). // - if (showDamageInflicted == 0) // this+0x264 - { - // The BT VehicleDead message carried the killer's name at +0x1c; the - // engine base Player__VehicleDeadMessage has no such field, so it is - // read through the documented byte offset. BEST-EFFORT (message gap). - killerName = *(const char *const *)((const char *)message + 0x1c); - } - else - { - killerName = SelfDestructName; // &DAT_00524b38 - } + killerName = SelfDestructName; // this+0x1d0 // - // Re-post the dead message to ourselves after a short delay. + // 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. // - Time when = Now(); // FUN_00414b60 + FUN_004dcd94 - when += ResetDelay; - application->Post(HighEventPriority, this, message, when); // app+0x60 + playerVehicle = 0; // this+0x1fc + + // + // If lives remain (solo/dev: no role -> always), re-post to ourselves at + // now + 5 seconds: the pilot watches the wreck, then the recovery drop. + // + if (scenarioRole == 0 || scenarioRole->GetReturnFromDeath() > 0) + { + Time when = Now(); // FUN_00414b60 + FUN_004dcd94 + when += RespawnDelay; // 5.0f @004c0830 + application->Post(HighEventPriority, this, message, when); // app+0x60 + } + // else: out of lives -> the +10s mission-review post (id 0x18). Deferred. - deathPending = 0; // this+0x290 suppressConsole = 0; // this+0x258 } @@ -397,6 +469,17 @@ void (Mech *)application->GetHostManager()->GetEntityPointer(message->senderMechID); // app+0x2c, msg+0x34 Mech *our_mech = (Mech *)playerVehicle; // this+0x1fc + // + // Respawn-cycle guard (task #52): during the 5-second dead window + // playerVehicle is severed; the tonnage ratio below is undefined without + // our own mech, so a straggler inflicted-score is dropped. + // + if (our_mech == 0) + { + Check_Fpu(); + return; + } + Scalar award = CalcInflictedScore(message->damageAmount, target_mech, 0.0f); if (target_mech == our_mech) { @@ -594,8 +677,17 @@ void // // Hand the (now-scored) message to the base class to apply the award. + // Respawn-cycle guard (task #52): during the 5-second dead window + // playerVehicle is severed, and the engine base does + // playerVehicle->RespondToScoreMessage unguarded (Check aborts) -- so a + // straggler score lands the award without the vehicle response. // message->scoreAward = award; // msg+0x1c + if (playerVehicle == 0) + { + currentScore += message->scoreAward; // the base's award apply + return; + } Player::ScoreMessageHandler(message); // FUN_0042da20 } @@ -690,7 +782,20 @@ void application->GetHostManager()->GetConsoleHost(); // FUN_00429078 if (console_host) { - Dispatch(&score_message); // (**(*this+0xc))(this, &msg) + // A console message is a NetworkClient::Message, NOT an + // Entity::Message -- it must reach the console over the + // NETWORK stream (as the VTV-damaged push above does), + // not through the player's Entity::Dispatch. Entity::Dispatch + // stamps entityID/interestZoneID at the (larger) Entity::Message + // offsets, writing PAST this small stack message -> an RTC stack + // overflow (caught live on the respawned player's first score + // flush, task #52). Route it through SendMessage like the + // damaged-message push. + application->SendMessage( // app+0x20, slot+0x18 + console_host->GetHostID(), // console_host+0xc + NetworkClient::ConsoleClientID, // 5 + &score_message + ); currentScore = 0; // this[0x9e] = 0 } } diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 67d85cd..b673673 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -141,6 +141,9 @@ #include // BoxedSolid / BoxedSolidCollision / BoxedSolidCollisionList #include // CulturalIcon::IsStoppingCollisionVolume / GetClassDerivations #include // HostManager::GetEntityPointer (band-effect attacker resolve) +#if !defined(PLAYER_HPP) +# include // Player::VehicleDeadMessage -- the death->respawn notification (task #52) +#endif #if !defined(EMITTER_HPP) # include // Emitter/PPC beam state (the per-weapon beam render walk) #endif @@ -976,7 +979,20 @@ void Logical Mech::IsMechDestroyed() { - // graphicAlarm level 9 = a vital zone / leg gone -> going down (mechdmg.cpp). + // AUTHENTIC LATCH first: IsDestroyed == (movementMode 2 || 9) -- the death + // modes (FUN_004ab1c8 zeroes locomotion for exactly these) [T1]. The death + // modes never revert, so a wreck STAYS destroyed. + // + // graphicAlarm >= 9 is only the vital-kill TRIGGER that enters the death + // transition (mechdmg.cpp:407/426). It is a STATUS indicator, not a latch: + // a later leg hit on the wreck legitimately rewrites it to 4/3 + // (mechdmg.cpp:417/419), which -- when this predicate was the alarm alone -- + // "resurrected" the wreck (movementMode reverted to 1) and let the next + // vital hit run the WHOLE death transition again: double kill score, and a + // Score dispatched into the respawn window's severed playerVehicle -> the + // engine's Check(playerVehicle) abort (task #52 cdb catch). + if (movementMode == 2 || movementMode == 9) + return True; return graphicAlarm.GetLevel() >= 9 ? True : False; } @@ -1132,6 +1148,36 @@ void << " DESTROYED (death effects dispatched from the death transition) ***\n" << std::flush; } + + // + // --- RESPAWN CYCLE (task #52): notify the owning player of the death. --- + // The BTPlayer VehicleDead handler @004c05c4 receives this with + // deathCount == -1 (the ctor default = "immediate death notification"), + // does the death bookkeeping, severs playerVehicle (this wreck entity + // STAYS in the world) and re-posts the message to itself at +5 seconds + // (5.0f @004c0830) -> the engine drop-zone hunt -> DropZoneReply -> + // CreatePlayerVehicle (a NEW mech). Only the owner pod's master has a + // live playerLink, which is exactly where the cycle must run; replicant + // wrecks just mirror the master's death. [T1 the handler @004c05c4; + // T3 this dispatch site -- the binary's exact sender is undecoded, but + // the once-per-death transition is the only death edge and the message + // ctor's deathCount=-1 default exists for precisely this notification.] + // + { + Player *owner = GetPlayerLink(); + if (owner != 0) + { + Player::VehicleDeadMessage + vehicle_dead( + Player::VehicleDeadMessageID, + sizeof(Player::VehicleDeadMessage) + ); + owner->Dispatch(&vehicle_dead); + if (getenv("BT_DEATH_LOG")) + DEBUG_STREAM << "[death] VehicleDead(-1) dispatched to the owning player\n" + << std::flush; + } + } } void