Skip to content

fix(fake-death): fix client death-restart sync (stuck Game Over, stale corpse, blocked doors)#44

Closed
lisniuse wants to merge 1 commit into
vaiserYT:mainfrom
lisniuse:fix/death-restart-sync-pr
Closed

fix(fake-death): fix client death-restart sync (stuck Game Over, stale corpse, blocked doors)#44
lisniuse wants to merge 1 commit into
vaiserYT:mainfrom
lisniuse:fix/death-restart-sync-pr

Conversation

@lisniuse
Copy link
Copy Markdown

Problem

In co-op, when both players go down and the host restarts the run (all-players-downed), the client could end up:

  • Stuck on the Game Over screen at 1 HP — never respawning or healing, sometimes crashing with Null access .curCine.
  • After the host's restart, the host kept seeing the client as a corpse pinned at the old death position, and the client could not interact with doors (exit doors gated as if still downed).

Reproduction

  1. Two players in co-op.
  2. Client goes down first, then the host goes down (all players downed → host restarts the run).
  3. Observe the client.

Root cause

1. In-place level reload pre-empts the full restart (stuck Game Over / crash).
On the all-downed restart the host broadcasts a new run seed + the new level graph. On the client:

  • the seed triggers QueueClientRestartFromHostSeed → a queued full launchGame restart;
  • the level graph then triggers TryTriggerLevelGraphReload → an in-place reloadAfterBossRuneModif, which runs first and reloads the old run in place, swallowing the full restart. The client is left in the old run at 1 HP with Game Over; a bad curCine state can crash with Null access .curCine.

2. Revived state never broadcast (stale corpse + blocked doors).
ResetDownedPlayersForRestart called ResetFakeDeathState(sendNetworkUpState: false), so the restarting client never told the peer it was no longer downed. The host's _remoteDowned[client] stayed set, so ReceiveGhostCoords pinned the client ghost at the corpse position and LevelExitSync gated its interactions.

Fix

  • LevelSync: guard TryTriggerLevelGraphReload / TryTriggerBossRuneReload with ShouldSuppressClientLevelReload() — skip the in-place reload while the local player is downed or a full restart is pending, so the queued launchGame restart wins. Map consistency is unaffected (still synced via generateGraph + TryApplyRemoteLevelGraph).
  • GameMenu: add a client-restart-pending flag (set when QueueClientRestartFromHostSeed queues, cleared on MarkInRun) to cover the window where _localFakeDead is already cleared but the new run hasn't started yet.
  • FakeDeath: ResetDownedPlayersForRestart now broadcasts the not-downed state (sendNetworkUpState: true) so the peer clears stale remote-downed tracking.

Notes

  • Trade-off: while a full restart is pending, the client skips the in-place hot-reload of the current level; the host changing the boss rune mid-level is reflected on next level entry instead of instantly. Normal forward progression is unaffected.
  • Verified in a live 2-player session (client-first death → host death → restart): client now respawns at the start level at full HP, the host sees the client move normally (no corpse), and door interactions work.

🤖 Generated with Claude Code

…e corpse, doors)

When all players are downed and the host restarts the run, the client could get stuck on Game Over (no respawn/heal, occasional Null access .curCine crash); after restart the host kept seeing the client as a corpse with interactions (exit doors) blocked.

- LevelSync: guard TryTriggerLevelGraphReload / TryTriggerBossRuneReload with ShouldSuppressClientLevelReload() so the host's restart-level graph cannot fire an in-place reloadAfterBossRuneModif that pre-empts the queued full launchGame restart while the client is downed / restart pending.
- GameMenu: add a client-restart-pending flag (set when QueueClientRestartFromHostSeed queues, cleared on MarkInRun) covering the window where _localFakeDead is already cleared but the new run has not started.
- FakeDeath: ResetDownedPlayersForRestart now broadcasts the not-downed state (sendNetworkUpState: true) so the peer clears stale remote-downed tracking (corpse pinning + interaction gating).

Source-level equivalent of the deployed binary patches (see patches/PATCHES.md).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@lisniuse lisniuse closed this Jun 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant