Fix svdemo_play from in-game and devmap from demo playback

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>
This commit is contained in:
serge_shubin 2026-03-24 06:32:08 +08:00
parent 74e2fc39c8
commit f871cc004f
5 changed files with 67 additions and 23 deletions

View file

@ -1013,7 +1013,6 @@ void ClientBegin( int clientNum ) {
client->pers.teamState.state = TEAM_BEGIN;
// demo playback: force all clients to spectator
// demo playback: force to spectator and update configstring
if ( g_svDemoPlaying.integer ) {
client->sess.sessionTeam = TEAM_SPECTATOR;
ClientUserinfoChanged( ent->client - level.clients );
@ -1031,6 +1030,13 @@ void ClientBegin( int clientNum ) {
// locate ent at a spawn point
ClientSpawn( ent );
// demo playback: ClientThink_real (which normally sets pm_type
// for spectators) is disabled. Set it explicitly after spawn.
if ( g_svDemoPlaying.integer && client->sess.sessionTeam == TEAM_SPECTATOR ) {
client->ps.pm_type = PM_SPECTATOR;
client->ps.speed = 480;
}
if ( client->sess.sessionTeam != TEAM_SPECTATOR ) {
// send event
tent = G_TempEntity( ent->client->ps.origin, EV_PLAYER_TELEPORT_IN );

View file

@ -183,6 +183,12 @@ G_WriteSessionData
void G_WriteSessionData( void ) {
int i;
// don't persist demo spectator sessions — the forced TEAM_SPECTATOR
// would carry over to the next normal game
if ( g_svDemoPlaying.integer ) {
return;
}
trap_Cvar_Set( "session", va("%i", g_gametype.integer) );
for ( i = 0 ; i < level.maxclients ; i++ ) {

View file

@ -352,12 +352,14 @@ void SVD_AutoRecord( void );
void SVD_CaptureServerCommand( const char *cmd );
void SVD_Play_f( void );
void SVD_StopPlay_f( void );
void SVD_CleanupPlayback( void );
void SVD_Stop_f( void );
void SVD_Pause_f( void );
qboolean SVD_PlaybackFrame( void );
qboolean SVD_IsRecording( void );
qboolean SVD_IsPlaying( void );
qboolean SVD_IsPaused( void );
qboolean SVD_IsStarting( void );
qboolean SVD_ShouldPause( void );
//============================================================

View file

@ -351,11 +351,17 @@ void SV_SpawnServer( char *server, qboolean killBots ) {
char systemInfo[16384];
const char *p;
// stop any active demo recording (one demo = one map)
// stop any active demo recording/playback (one demo = one map).
// SVD_IsStarting() returns true when SVD_Play_f is calling devmap
// internally — don't stop our own playback.
if ( SVD_IsRecording() ) {
Com_Printf( "Map change — stopping demo recording.\n" );
SVD_StopRecord_f();
}
if ( SVD_IsPlaying() && !SVD_IsStarting() ) {
Com_Printf( "Map change — stopping demo playback.\n" );
SVD_CleanupPlayback();
}
// shut down the existing game if it is running
SV_ShutdownGameProgs();
@ -680,12 +686,13 @@ void SV_Shutdown( char *finalmsg ) {
Com_Printf( "----- Server Shutdown -----\n" );
// clean up any active demo recording/playback
// clean up any active demo recording/playback.
// skip if SVD_Play_f is calling SV_Shutdown internally.
if ( SVD_IsRecording() ) {
SVD_StopRecord_f();
}
if ( SVD_IsPlaying() ) {
SVD_StopPlay_f();
if ( SVD_IsPlaying() && !SVD_IsStarting() ) {
SVD_CleanupPlayback();
}
if ( svs.clients && !com_errorEntered ) {

View file

@ -11,6 +11,9 @@ snapshot pipeline delivers them to a spectator client.
#include "server.h"
// Cvar_Set2 not in public header but needed to force-set CVAR_ROM cvars
extern cvar_t *Cvar_Set2( const char *var_name, const char *value, qboolean force );
// ---------------------------------------------------------------
// File format
// ---------------------------------------------------------------
@ -97,12 +100,14 @@ typedef struct {
char playMapName[SVDEMO_MAX_MAPNAME];
qboolean endOfDemo;
qboolean needConfigstrings; // apply saved configstrings on first frame
qboolean starting; // SVD_Play_f is running devmap internally
qboolean paused;
svdEntityState_t playPrevEntities[MAX_GENTITIES]; // previous frame for delta read
svdPlayerState_t playPrevPlayers[MAX_CLIENTS]; // previous frame player states
} svDemo_t;
static svDemo_t demo;
void SVD_CleanupPlayback( void );
// ---------------------------------------------------------------
// Recording helpers
@ -770,8 +775,8 @@ void SVD_Play_f( void ) {
char *s;
int len;
if ( demo.playing ) {
Com_Printf( "Already playing a server demo.\n" );
if ( demo.recording ) {
Com_Printf( "Stop recording first (svdemo_stop).\n" );
return;
}
@ -781,6 +786,11 @@ void SVD_Play_f( void ) {
return;
}
// stop current playback if switching demos
if ( demo.playing ) {
SVD_CleanupPlayback();
}
Com_sprintf( name, sizeof(name), "svdemos/%s.svdm", s );
memset( &demo, 0, sizeof(demo) );
@ -799,42 +809,50 @@ void SVD_Play_f( void ) {
demo.playing = qtrue;
demo.endOfDemo = qfalse;
demo.needConfigstrings = qtrue;
Com_Printf( "Playing server demo: map=%s maxclients=%d fps=%d\n",
demo.playMapName, demo.playMaxClients, demo.playFps );
// Signal demo mode BEFORE map load so the game module knows
// during ClientConnect/ClientBegin to force spectator team.
Cvar_Set( "sv_demoplaying", "1" );
// Shut down current server first so no clients carry over to
// reserved slots. SV_Shutdown triggers our cleanup hook, but
// demo.starting prevents it from clearing our state.
demo.starting = qtrue;
if ( com_sv_running->integer ) {
SV_Shutdown( "Demo playback\n" );
}
// Set demo cvar BEFORE devmap so G_InitGame can read it.
// The previous server is gone so no old game module to conflict with.
Cvar_Set2( "sv_demoplaying", "1", qtrue );
// Load the map with maxclients = MAX_CLIENTS to avoid entity slot collisions.
Cbuf_ExecuteText( EXEC_NOW, va("set sv_maxclients %d\n", MAX_CLIENTS) );
Cbuf_ExecuteText( EXEC_NOW, va("set sv_fps %d\n", demo.playFps) );
Cbuf_ExecuteText( EXEC_NOW, va("devmap %s\n", demo.playMapName) );
demo.starting = qfalse;
// Reserve recorded player slots so the connecting spectator
// doesn't land in slot 0 (which collides with recorded player 0).
// Mark as CS_ZOMBIE (non-free, won't be reused by SV_DirectConnect).
// Set rate and nextSnapshotTime so SV_SendClientMessages doesn't
// crash on division by zero or try to send them snapshots.
// Also set configstring directly for cgame (must be after devmap
// which creates the server and allocates configstrings)
if ( sv.state == SS_GAME ) {
SV_SetConfigstring( CS_SVDEMO, "1" );
}
// Reserve recorded player slots. Server is fresh (SV_Shutdown cleared
// old clients), local client hasn't connected yet.
{
int i;
for ( i = 0; i < demo.playMaxClients; i++ ) {
if ( svs.clients[i].state == CS_FREE ) {
svs.clients[i].state = CS_ZOMBIE;
svs.clients[i].rate = 10000;
svs.clients[i].nextSnapshotTime = 0x7FFFFFFF; // never send
svs.clients[i].nextSnapshotTime = 0x7FFFFFFF;
svs.clients[i].lastPacketTime = svs.time;
}
}
}
// Configstrings will be applied on the first playback frame,
// after the map has fully loaded.
demo.needConfigstrings = qtrue;
}
static void SVD_CleanupPlayback( void ) {
void SVD_CleanupPlayback( void ) {
int i;
if ( !demo.playing ) {
@ -858,7 +876,7 @@ static void SVD_CleanupPlayback( void ) {
}
memset( &demo, 0, sizeof(demo) );
Cvar_Set( "sv_demoplaying", "0" );
Cvar_Set2( "sv_demoplaying", "0", qtrue );
}
void SVD_StopPlay_f( void ) {
@ -957,6 +975,11 @@ qboolean SVD_IsPaused( void ) {
return demo.playing && demo.paused;
}
qboolean SVD_IsStarting( void ) {
return demo.starting;
}
/*
Returns qtrue if demo playback should pause (no active spectators).
Controlled by svdemo_pauseEmpty cvar.