Death and respawn: server-owned life state plus reliable lifecycle events
Status: Accepted (2026-06-25)
Context
A player's life has two sides. One is state: is the player alive, and which life (generation) is it on. A late joiner or a recipient that starts streaming a player needs this state at any moment. The other side is edges: the moment of a death, with the pose the victim died in, and the moment of a respawn. Edges are events; a recipient that misses one cannot reconstruct it from later state.
The continuous player frame (PlayerSync, RemotePlayerSync) is unreliable and loss-tolerant: a later frame replaces an earlier one. Carrying death and respawn as bits latched in that frame makes the edges implicit and leaves no room for event data. The death pose is such data: if a receiver only sees health drop to zero, the game on that receiver picks its own death animation (for example a fall), and the corpse lies differently on every screen. With the reliable event lane (ADR 0007) the edges can be events.
Who decides is the other question. A client that could declare itself alive again through its own frame could skip any respawn rule. A death, on the other hand, is first seen by the victim's own game.
Decision
Life state is the server's
- The server keeps each player's life state:
alive, the life generation, and whether a granted respawn is still unconfirmed. It stampsaliveand the generation (death_epoch) into every relayed frame; the values the client put there are not relayed. A client cannot fake a revival throughPlayerSync. - While the server holds a player alive, the relayed health is never lethal: a frame with
hp <= 0is relayed withhp = 1, so a receiver never sees a living player at zero health without a death event. - The frame keeps
aliveas state (for a late show and to stop replaying input on a corpse) anddeath_epochas the generation; neither triggers a death or a respawn on the receiver.
Death is reported by the victim and committed once
- The dying client sends DeathReport on its reliable stream with the means of death and the death animation. The server replaces a pose the game cannot produce with the default one.
- The server commits the death once per life, only for a spawned, living player that is not respawning. A death the server has already committed from its own health ledger (ADR 0045, Health accounting and death) is not committed again by a late report. A frame with
alive = 0never commits a death. - On a fresh commit the server sends PlayerWasted with the pose to the players the victim is shown to (not to the victim, which already played its death) and raises the Lua event
player_death(ADR 0029).
Respawn is requested by the client and granted by the server
- A dead client repeats RespawnRequest on a timer. A timer, not a key: the request is idempotent, repeating it is safe, and the server decides the moment anyway.
- The server grants it when its policy allows: the gamemode's per-death decision or the core defaults (ADR 0020, ADR 0012). The grant starts a new generation, resets combat correlation so a shot or credit of the previous life cannot carry over, and sends RespawnPlayer with the point to the respawning player. Until its first living frame confirms the new life, a repeated request gets the same grant again (a lost
RespawnPlayerheals itself) without a new generation. - On the same grant the server sends PlayerSpawned to the players the subject is shown to. Receivers rebuild the subject's copy at the point instead of reviving the corpse in place: the result is always a clean copy, at the cost of a short blink.
- The Lua event
player_spawnis raised on the first living frame of the new life, as after the connect spawn.
The edges are never lost and never misapplied
PlayerWastedandPlayerSpawnedare critical events: they are never dropped when a recipient's reliable queue is full. A recipient that overflows the hard cap is disconnected instead (Disconnect).- Each edge carries the subject's generation (
player_token); a receiver applies it only to that generation, so an edge of a departed player never hits a new player with the reused id. - The reliable lane is ordered, so a death always arrives before the following respawn; no extra counter is needed to order them.
PlayerSpawnedcarriessync_seq, the barrier that drops late frames of the previous life. - A recipient that does not see the subject (streaming, ADR 0035) gets no edges; its next show builds the copy from the current state.
Consequences
- Continuous state and lifecycle edges are separate: the frame carries what is true now, the reliable lane carries what happened, and the server owns both.
- Lifecycle edges and chat share one ordered reliable stream per connection; there is still a single stream, not one per event type.
- The death commit and the respawn grant are the natural points for gamemode events;
player_deathandplayer_spawnare raised there. death_epochin the frame may become redundant now that the edges are events; removing it waits for a per-packet life check of combat traffic.
Alternatives considered
- Keep death and respawn as latched bits in the frame and add the pose to the frame. Atomic with the edge and free of races, but it keeps lifecycle as a client-latched bit and mixes event data into continuous state. Rejected once a reliable lane existed.
- Death as a reliable event, respawn still on the frame's generation edge. Rejected: an asymmetry that needs a cross-channel binding by generation. Both edges on the lane remove it.
- Commit the death from the frame as well as from the report. Rejected: two commit paths race; if the frame came first, the pose broadcast would be suppressed as a duplicate.
- Revive the remote copy in place. Rejected: it would have to reproduce the game's own recovery on every receiver; rebuilding the copy reuses the normal create and remove path and is deterministic.
- Let the client decide its respawn and only validate the generation. Rejected: a client could raise its own generation and skip every respawn rule.
