Read keyframe directly in SVD_Seek_f instead of deferring to
SVD_PlaybackFrame (which is blocked when paused). Both svdemo_seek
and svdemo_seekexact now work regardless of pause state.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
svdemo_seek snaps to the nearest keyframe (fast, ±interval accuracy).
svdemo_seekexact reads forward from the keyframe to the exact target
time, giving frame-accurate positioning at the cost of a brief
processing delay.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Server:
- Reset nextSnapshotTime for active clients after seek so
SV_SendClientMessages doesn't skip sending (was comparing
future nextSnapshotTime against past svs.time)
- First frame always a keyframe (beginning seekable)
- Keyframes during normal playback only reset delta state,
no SNAPFLAG_RESET_ENTITIES (no visual glitch every 5 sec)
Engine client:
- Reset cl.oldFrameServerTime on SERVERCOUNT toggle to prevent
"time went backwards" error in CL_SetCGameTime
cgame:
- Handle backwards time in CG_ProcessSnapshots for demo seek:
accept the time jump, reset cg.time, clear all entity state
(currentValid, interpolate, time-dependent fields), clear
local entities (particles, gibs, smoke), wait for next snapshot
- Prevents CG_InterpolateEntityPosition NULL nextSnap error
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Recording:
- svdemo_keyframeInterval cvar (default 5 seconds, 0 = disabled)
- Periodic delta state reset produces full-state frames
- Frame flag bit 1 marks keyframes for reader to reset delta state
- Keyframe index (time + file offset) written at end of file
with numKf_copy trailer for efficient playback loading
- Dynamic allocation for unlimited keyframe count
Playback:
- Keyframe index loaded from file footer on playback start
- svdemo_seek <seconds>: relative seek from current time
Finds nearest keyframe at or before target, seeks file position,
resets delta state + svs.time, sets SNAPFLAG_RESET_ENTITIES
via demo.seeked flag (one-shot on next frame read)
- Normal keyframe frames only reset delta state (no visual glitch)
- Map restart frames also set SNAPFLAG_RESET_ENTITIES
Demo format: keyframe flag (bit 1) added to frame flags byte.
Backward compatible — old demos have no keyframes (numKf=0).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The flag was cleared at the start of SVD_PlaybackFrame before the
snapshot was built — it never reached the client. Only SERVERCOUNT
toggle is needed for the time delta reset.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entities must be linked immediately when injected, not deferred to
G_RunFrame. When multiple server frames execute per SV_Frame cycle,
the clearing loop in the next iteration unlinks entities before the
snapshot is built — one-frame entities (explosions, temp effects)
are lost. G_RunFrame still runs BG_EvaluateTrajectory + trap_LinkEntity
to refine origins for moving entities (rockets with TR_LINEAR).
Also restore SNAPFLAG_RESET_ENTITIES on unpause so fast-moving
entities snap instead of interpolating through the pause gap.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Forward declaration of SVD_CleanupPlayback no longer needed (non-static,
declared in server.h). Server-side SV_SetConfigstring(CS_SVDEMO) is
redundant — G_InitGame sets it from the cvar which is now reliably
set before devmap.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Server doesn't run PmoveSingle for demo spectator. cgame uses its
own local camera with PM_SPECTATOR. Game module checks
sess.sessionTeam, not pm_type. The set was dead code.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Multiple issues fixed for seamless transitions:
Server lifecycle:
- SV_Shutdown before devmap in SVD_Play_f: drops old clients so
spectator doesn't land in slot 0 (recorded player collision)
- SVD_IsStarting flag prevents cleanup hooks from destroying demo
state during our own SV_Shutdown/SV_SpawnServer calls
- SV_SpawnServer stops demo playback on non-demo map changes
via SVD_CleanupPlayback (no disconnect, just state cleanup)
- SVD_CleanupPlayback made non-static for use from sv_init.c
Cvar handling:
- Use Cvar_Set2 with force=qtrue for CVAR_ROM sv_demoplaying
- Set cvar AFTER SV_Shutdown (old game module gone) but BEFORE
devmap (so new G_InitGame reads correct value)
- Set CS_SVDEMO configstring after devmap as backup for cgame
Game module:
- ClientBegin: set pm_type=PM_SPECTATOR after ClientSpawn (which
memsets ps). ClientThink_real normally sets this but is disabled
in demo mode.
- G_WriteSessionData: skip during demo playback so forced
TEAM_SPECTATOR doesn't persist to next normal game
- ClientThink: return early in demo mode (no server-side movement)
Removed debug prints and unused SVD_GetPlayMaxClients.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Delta-compressed entity/playerState bitstreams have minimal
redundancy for LZ4 to exploit. The per-block 8-byte header
overhead was comparable to the compression savings.
Removed: lz4.h include, SVD_WriteBlock/ReadBlock functions,
SVDEMO_FLAG_COMPRESSED, demo.compressed field, svdemo_compress
cvar, lz4.c from build. Direct size+data writes replace block I/O.
Demo format v3 (flags field reserved, always 0).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Stop recording currentOrigin, absmin, absmax, linked per entity
(44 bytes per moving entity per frame). During playback, G_RunFrame
computes currentOrigin via BG_EvaluateTrajectory and calls
trap_LinkEntity to register in BSP for PVS.
svFlags still recorded (1-bit change flag + 4 bytes when changed).
Entity linking moved from SVD_ReadFrame (server, no trajectory eval)
to G_RunFrame demo mode (game module, has BG_EvaluateTrajectory).
Scan all MAX_GENTITIES during playback since recorded entities may
have indices above level.num_entities (game-module-spawned count).
Demo format bumped to v3. Significant file size reduction.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
sv_demoplaying is set before devmap, synced during G_RegisterCvars,
and updated every frame by G_UpdateCvars. No need for defensive
trap_Cvar_VariableIntegerValue calls which add unnecessary syscall
overhead on every invocation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three issues fixed:
1. ClientThink called ClientThink_real during demo playback, which
ran server-side PmoveSingle and overwrote the client-owned
ps.origin. Now ClientThink returns early in demo mode after
updating pers.cmd (for button access).
2. Buttons never reached the server while paused: SV_UserMove
discards usercmds with duplicate serverTime (line 1421). During
pause, all usercmds have frozen serverTime. Fix: always process
the last usercmd in the packet during demo playback.
3. Spectator button handling in G_RunFrame demo mode processes
MOUSE1 (attack) to cycle follow targets via Cmd_FollowCycle_f.
Removed debug print.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entity positions change by one server frame (50ms) on unpause —
normal interpolation handles this fine. Only SNAPFLAG_SERVERCOUNT
is needed to hard-reset the client's time delta.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Suppress CG_DrawDisconnect during server demo playback — connection
can't be interrupted on a local demo. Detect pause from snapshot
serverTime (frozen time = paused) rather than configstrings, since
configstrings are OOB and may not be in sync with snapshots.
Show "Playback Paused" centered on screen when detected.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
On unpause, toggle SNAPFLAG_SERVERCOUNT and set SNAPFLAG_RESET_ENTITIES.
In CL_ParseSnapshot, detect SERVERCOUNT toggle and hard-reset
cl.serverTimeDelta instead of letting CL_AdjustTimeDelta slowly drift.
During pause, the delta drifted because snapshots had frozen serverTime
while cls.realtime advanced. Without the hard reset, it took 1-2 seconds
of choppy interpolation to re-sync.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
cgame runs its own PmoveSingle for the free camera using real time,
independent of server time. Solves pause freezing — camera moves
smoothly while svs.time is frozen.
Changes:
- usercmd_t: add optional origin[3] with 1-bit flag (zero cost for
normal gameplay, 13 bytes during demo viewing)
- msg.c: serialize optional origin in usercmd delta encoding
- CG_SETCLIENTORIGIN trap: cgame sends camera origin to engine
- cl_input.c: CL_FinishMove writes cgame origin into usercmd
- cg_predict.c: CG_PredictPlayerState runs local PmoveSingle for
free camera in svDemo mode, sends origin via trap for PVS
- cg_snapshot.c: detect follow→free camera transition, init camera
from last known position
- cg_main.c: detect svDemo mode from CS_SVDEMO configstring,
lazy-init camera from first snapshot's spectator spawn
- g_main.c: G_InitGame sets CS_SVDEMO configstring, G_RunFrame
copies cmd.origin to ps.origin for PVS instead of ClientThink_real
- bg_public.h: CS_SVDEMO configstring index (26)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
svdemo_pause toggles playback pause. When paused:
- svs.time frozen so entity trajectories freeze correctly
- No demo frames consumed (SVD_PlaybackFrame returns early)
- Game frame still runs at frozen time for spectator movement
- No time jump on unpause — svs.time resumes from where it was
Spectator movement degrades during pause (200ms PmoveSingle cap)
— will be resolved by client-owned camera in a future change.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously all team changes were blocked during demo playback.
Now 'team spectator' (or 'team s') is allowed — it calls
StopFollowing to return to free camera. All other team changes
remain blocked.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the gamestate resend approach for map_restart with a custom
snapshot flag (SNAPFLAG_RESET_ENTITIES, bit 4 in snapFlags byte).
Server side (sv_netdemo.c):
- On restart frame, OR the flag into svs.snapFlagServerBit (one-shot)
- Cleared at the start of the next playback frame
Client side (cg_snapshot.c):
- CG_SetNextSnap: clear currentValid for all entities when flag is set,
making them all "new" — existing interpolation check at line 228
sets interpolate=qfalse, CG_TransitionEntity calls CG_ResetEntity
- Also set cg.nextFrameTeleport=qtrue to prevent playerstate
interpolation during follow mode
No loading screen, no lost frames, no gamestate resend. Entities and
playerstate both snap to correct positions on map_restart.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move pause check from SV_Frame (which blocked connection handshake)
into SVD_PlaybackFrame. The server runs frames normally so clients
can connect and load, but demo data isn't consumed until a client
reaches CS_ACTIVE. The spectator sees the demo from the very first
frame without missing any gameplay.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Set sv_demoplaying before devmap so game module knows during
ClientConnect/ClientBegin
- Call ClientUserinfoChanged after forcing spectator team so
CS_PLAYERS configstring has correct team value for cgame
- Record sanitized playerState for spectators (pm_type=PM_SPECTATOR,
PERS_TEAM=TEAM_SPECTATOR) so they show correctly on scoreboard
instead of appearing as regular players from follow-mode corruption
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CS_ACTIVE requires a fully connected client, but clients go through
CS_CONNECTED → CS_PRIMED → CS_ACTIVE. If the server is paused
waiting for CS_ACTIVE, the connection handshake never completes.
Check >= CS_CONNECTED to allow connecting clients to unpause.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Prevent spectators from joining a playing team during demo
playback, which would disrupt the playback state. Prints a
message to the client instead.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
svdemo_pauseEmpty (default: 1) pauses frame processing when no
client is CS_ACTIVE during demo playback. Prevents the demo from
advancing with nobody watching. Time residual is cleared so the
demo resumes from where it paused when a spectator connects.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
spectatorClientNum was hardcoded to MAX_CLIENTS-1 but the spectator
actually connected at a different slot. All skip checks using it
were no-ops protecting the wrong slot. Removed entirely:
- Zombie slots handle player slot reservation
- G_RunFrame recreates the spectator entity via ClientThink_real
- PlayerState injection writes all slots (spectator's ps gets
overwritten by ClientThink_real anyway)
- SVD_SpectatorClientNum accessor removed
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
A real map_restart sends SV_SendClientGameState to all clients,
which triggers CL_ParseGamestate → CL_ClearState on the client,
wiping all snapshot and entity history. Previously we only forced
non-delta snapshots which doesn't clear cgame's entity state.
Now we call SV_SendClientGameState for all active clients on the
restart frame, exactly matching real server behavior.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The recording resets its delta state on map_restart (writes from
zero baseline). The playback must do the same, otherwise the
delta decoder uses stale pre-restart state as baseline, producing
corrupt entity data and preventing EF_TELEPORT_BIT from being
decoded correctly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
snapFlagServerBit toggle alone doesn't clear client entity
interpolation state. Also reset deltaMessage=-1 for all active
clients, forcing the next snapshot to be full (non-delta). The
client receives deltaNum<=0, clears old entity state, and
renders all entities at their new positions without interpolation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Record a per-frame restart flag (1 byte) set by SVD_ResetDeltaState
when map_restart occurs. During playback, toggle svs.snapFlagServerBit
so the client treats the next snapshot as a fresh baseline — no
interpolation of entities from pre-restart positions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Per-client chat is sent to every connected client in a loop.
Capture chat/tchat commands regardless of target clientNum, and
deduplicate by checking if the exact string is already buffered
for the current frame.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Chat messages use per-client trap_SendServerCommand (not broadcast),
so they weren't captured. Move capture hook to SV_GameSendServerCommand
which is the game module boundary. Capture broadcasts (clientNum=-1)
and chat/tchat commands sent to client 0 (first in the per-client
loop, deduplicated by only capturing once).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
spectatorClientNum was hardcoded to MAX_CLIENTS-1 (63) but the
spectator actually connects at the first free slot after zombie
reservations. Broadcasting with SV_SendServerCommand(NULL, ...)
reaches the spectator regardless of their actual slot number.
Zombie clients (< CS_PRIMED) are skipped by the broadcast loop.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reverting the svs.time change from the bugfix commit — entity
trajectories (rockets, grenades, bobbing items) need svs.time to
match recorded time for client-side interpolation. The zombie
timeout issue is already handled by skipping SV_CheckTimeouts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Capture broadcast server commands (chat, print, cp, etc.) from
SV_SendServerCommand when cl==NULL. Buffer up to 64 commands per
frame. Written after configstrings in the demo file, replayed to
the spectator client during playback.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Per-frame entity and playerState blocks are compressed with LZ4
when svdemo_compress is set (default: 1). The block format writes
[original_size][compressed_size][data] — compressed_size=0 means
uncompressed. Playback auto-detects based on header flags.
Demo format bumped to version 2 with SVDEMO_FLAG_COMPRESSED flag.
Version 1 (uncompressed) demos are no longer compatible.
Uses the lz4.c/lz4.h library already in the server code directory.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Set svdemo_autorecord 1 to automatically record a demo on every
map load. Demo files are named <mapname>_YYYYMMDD_HHMMSS.svdm
in the svdemos/ directory.
Refactored SVD_Record_f to use SVD_StartRecording helper so both
manual and auto recording share the same code path. Also fixed
prevPlayers delta state not being cleared on recording start.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SV_SpawnServer reinitializes the game module, invalidating all
entity and configstring state. Recording across a map boundary
would produce corrupt deltas. Stop recording automatically at
the start of SV_SpawnServer (before game shutdown).
map_restart is unaffected — it has its own path (SV_MapRestart_f)
which doesn't call SV_SpawnServer.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The per-frame configstring path in SVD_ReadFrame was not filtering
dangerous configstrings like SVD_ApplyConfigstrings does. When a
recorded map_restart changed CS_SERVERINFO (containing maxclients),
applying it overwrote maxclients=64 with the recorded value,
triggering a latched restart loop.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. File handle leak: SVD_Play_f opened file twice, first handle leaked.
Fix: memset demo state before opening.
2. svdemo_stop now handles both recording and playback via SVD_Stop_f.
Playback stop disconnects client to return to menu.
3. Zombie client timeout: skip SV_CheckTimeouts during playback so
reserved player slots aren't freed.
4. Buffer overflow: increase entity buffer to MAX_GENTITIES*300 and
playerState buffer to MAX_CLIENTS*600 for worst-case first frame.
Made static to avoid stack overflow.
5. svs.time jump: don't overwrite svs.time with recorded time.
Server time advances normally, avoiding timeout/heartbeat issues.
6. map_restart: SVD_ResetDeltaState clears entity/player delta state
so next frame writes full states, preventing corrupt deltas.
7. Demo end and manual stop both disconnect the client.
8. SV_Shutdown cleans up active recording/playback.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Extract SVD_CleanupPlayback for shared cleanup logic
- Called on demo end (SVD_PlaybackFrame) and manual stop
- Frees zombie client slots, saved configstrings, file handle
- Clears sv_demoplaying cvar so game module exits demo mode
- Hook into SV_Shutdown to clean up on server shutdown/map change
- Allows playing another demo after one finishes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Call CalculateRanks and ClientEndFrame for the spectator in demo
mode G_RunFrame. SpectatorClientEndFrame copies the followed
player's recorded playerState into the spectator's ps, giving
first-person view with full HUD (health, armor, ammo, weapon).
Standard spectator controls work: mouse1 to cycle, mouse2 to
enter follow from free camera.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Clear the game module's playerState when a recorded player
disconnects, so G_RunFrame sees commandTime=0 and marks them
as CON_DISCONNECTED. Without this, disconnected players stayed
visible on the scoreboard indefinitely.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Record delta-compressed playerState_t for each active player per
frame using MSG_WriteDeltaPlayerstate. During playback, inject
into game module via SV_GameClientNum and mark players as
CON_CONNECTED with correct team. CalculateRanks and the
scoreboard now show recorded players with scores.
This also lays the groundwork for player-follow spectating.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use MSG_WriteDeltaEntity/MSG_ReadDeltaEntity for entity state
serialization. Only changed fields are written per frame. PVS
fields (svFlags, linked, origin, absmin, absmax) also use a
1-bit change flag to skip when unchanged.
Reduces a 10-second demo from ~1400KB to ~52KB (27x smaller).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Records full entity state array each server frame, enabling
free-camera demo playback from any viewpoint.
Recording:
- svdemo_record <name> / svdemo_stop
- Captures entityState_t + PVS fields (svFlags, linked,
currentOrigin, absmin, absmax) for all active entities
- Records configstring changes per frame
- File format: svdemos/<name>.svdm
Playback:
- svdemo_play <name>
- Loads map with maxclients=64, reserves recorded player slots
(CS_ZOMBIE with safe rate/timing) so spectator gets a high slot
- Injects recorded entities into sv.gentities each frame with
SV_LinkEntity for PVS visibility
- Re-applies demo configstrings (CS_PLAYERS etc.) after map load,
skipping CS_SERVERINFO/CS_SYSTEMINFO to avoid latch restarts
- Game module runs in demo mode (sv_demoplaying cvar): G_RunFrame
only processes spectator movement, skips all entity logic
- Spectator forced to TEAM_SPECTATOR on connect (ClientBegin)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>