Demo pause: freeze svs.time, hold demo data consumption

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>
This commit is contained in:
serge_shubin 2026-03-24 03:28:32 +08:00
parent 9799952b34
commit 7141d941a3
4 changed files with 1087 additions and 1056 deletions

View file

@ -353,9 +353,11 @@ void SVD_CaptureServerCommand( const char *cmd );
void SVD_Play_f( void ); void SVD_Play_f( void );
void SVD_StopPlay_f( void ); void SVD_StopPlay_f( void );
void SVD_Stop_f( void ); void SVD_Stop_f( void );
void SVD_Pause_f( void );
qboolean SVD_PlaybackFrame( void ); qboolean SVD_PlaybackFrame( void );
qboolean SVD_IsRecording( void ); qboolean SVD_IsRecording( void );
qboolean SVD_IsPlaying( void ); qboolean SVD_IsPlaying( void );
qboolean SVD_IsPaused( void );
qboolean SVD_ShouldPause( void ); qboolean SVD_ShouldPause( void );
//============================================================ //============================================================

View file

@ -744,6 +744,7 @@ void SV_AddOperatorCommands( void ) {
Cmd_AddCommand ("svdemo_record", SVD_Record_f); Cmd_AddCommand ("svdemo_record", SVD_Record_f);
Cmd_AddCommand ("svdemo_stop", SVD_Stop_f); Cmd_AddCommand ("svdemo_stop", SVD_Stop_f);
Cmd_AddCommand ("svdemo_play", SVD_Play_f); Cmd_AddCommand ("svdemo_play", SVD_Play_f);
Cmd_AddCommand ("svdemo_pause", SVD_Pause_f);
} }
/* /*

View file

@ -831,6 +831,15 @@ void SV_Frame( int msec ) {
// run the game simulation in chunks // run the game simulation in chunks
while ( sv.timeResidual >= frameMsec ) { while ( sv.timeResidual >= frameMsec ) {
sv.timeResidual -= frameMsec; sv.timeResidual -= frameMsec;
if ( SVD_IsPaused() ) {
// demo paused: freeze svs.time so trajectories freeze
// and client doesn't see time jumps on unpause.
// still run game frame for spectator movement (at frozen time).
VM_Call( gvm, GAME_RUN_FRAME, svs.time );
continue;
}
svs.time += frameMsec; svs.time += frameMsec;
if ( SVD_IsPlaying() ) { if ( SVD_IsPlaying() ) {

View file

@ -104,6 +104,7 @@ typedef struct {
char playMapName[SVDEMO_MAX_MAPNAME]; char playMapName[SVDEMO_MAX_MAPNAME];
qboolean endOfDemo; qboolean endOfDemo;
qboolean needConfigstrings; // apply saved configstrings on first frame qboolean needConfigstrings; // apply saved configstrings on first frame
qboolean paused;
svdEntityState_t playPrevEntities[MAX_GENTITIES]; // previous frame for delta read svdEntityState_t playPrevEntities[MAX_GENTITIES]; // previous frame for delta read
svdPlayerState_t playPrevPlayers[MAX_CLIENTS]; // previous frame player states svdPlayerState_t playPrevPlayers[MAX_CLIENTS]; // previous frame player states
} svDemo_t; } svDemo_t;
@ -987,6 +988,15 @@ void SVD_Stop_f( void ) {
} }
} }
void SVD_Pause_f( void ) {
if ( !demo.playing ) {
Com_Printf( "Not playing a server demo.\n" );
return;
}
demo.paused = !demo.paused;
Com_Printf( "Demo playback %s.\n", demo.paused ? "paused" : "resumed" );
}
/* /*
Called from SV_Frame() to advance playback by one frame. Called from SV_Frame() to advance playback by one frame.
Returns qtrue if a frame was read, qfalse if demo ended. Returns qtrue if a frame was read, qfalse if demo ended.
@ -996,6 +1006,11 @@ qboolean SVD_PlaybackFrame( void ) {
return qfalse; return qfalse;
} }
// manual pause — don't consume demo data
if ( demo.paused ) {
return qfalse;
}
// wait for a spectator to be fully in-game before starting playback. // wait for a spectator to be fully in-game before starting playback.
// the server keeps running frames (so the connection handshake completes) // the server keeps running frames (so the connection handshake completes)
// but no demo data is consumed until someone is CS_ACTIVE. // but no demo data is consumed until someone is CS_ACTIVE.
@ -1034,6 +1049,10 @@ qboolean SVD_IsPlaying( void ) {
return demo.playing; return demo.playing;
} }
qboolean SVD_IsPaused( void ) {
return demo.playing && demo.paused;
}
/* /*
Returns qtrue if demo playback should pause (no active spectators). Returns qtrue if demo playback should pause (no active spectators).
Controlled by svdemo_pauseEmpty cvar. Controlled by svdemo_pauseEmpty cvar.