Whirlwind respawn: authentic death->drop-zone->recreate cycle (task #52)
Death transition (mech4) now dispatches VehicleDead(-1) to the owning player; BTPlayer::VehicleDeadMessageHandler restructured to the authentic @004c05c4 three-way branch: -1 = death bookkeeping + sever playerVehicle (wreck stays) + 5s re-post; >=0 = engine drop-zone hunt -> DropZoneReply -> CreatePlayerVehicle (new mech); -2 = the acquire probe. Guarded on a DropZones group so a zone-less mission stays dead instead of aborting. Three latent bugs the respawn path exposed, all fixed: - IsMechDestroyed latched on 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/score, abort). Now latches on movementMode 2||9. - Score handlers dereferenced the severed playerVehicle during the dead window; guarded. - The console score flush routed a NetworkClient::Message through the player's Entity::Dispatch, which stamps entityID past the smaller struct -> /RTC1 stack overflow. Sent via application->SendMessage instead. - Renderer LoadMission re-entry (per viewpoint-make) re-read the env INI; its light block Fail'd on stale sceneLightCount. Reset it in DPLReadEnvironment so the respawn's second read is clean. Verified 2-node self-drive: B killed repeatedly by A respawns each time (wreck stays), ticks + reloads + takes fresh damage on the new mech, no abort/hang across multiple death->respawn cycles. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
e430e474cb
commit
83b3f31957
@@ -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.
|
||||
|
||||
+36
-9
@@ -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 —
|
||||
|
||||
@@ -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 `<Name>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]
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user