From f871cc004f433e057cb9892b411b278695d49621 Mon Sep 17 00:00:00 2001 From: serge_shubin Date: Tue, 24 Mar 2026 06:32:08 +0800 Subject: [PATCH] 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) --- code/game/g_client.c | 8 +++++- code/game/g_session.c | 6 ++++ code/server/server.h | 2 ++ code/server/sv_init.c | 15 +++++++--- code/server/sv_netdemo.c | 59 ++++++++++++++++++++++++++++------------ 5 files changed, 67 insertions(+), 23 deletions(-) diff --git a/code/game/g_client.c b/code/game/g_client.c index 719e382..9aed85c 100644 --- a/code/game/g_client.c +++ b/code/game/g_client.c @@ -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 ); diff --git a/code/game/g_session.c b/code/game/g_session.c index e65c3e1..a8586f8 100644 --- a/code/game/g_session.c +++ b/code/game/g_session.c @@ -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++ ) { diff --git a/code/server/server.h b/code/server/server.h index 8fc7b40..9a68b00 100644 --- a/code/server/server.h +++ b/code/server/server.h @@ -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 ); //============================================================ diff --git a/code/server/sv_init.c b/code/server/sv_init.c index 5aee942..701e8cd 100644 --- a/code/server/sv_init.c +++ b/code/server/sv_init.c @@ -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 ) { diff --git a/code/server/sv_netdemo.c b/code/server/sv_netdemo.c index da5e649..33f48c7 100644 --- a/code/server/sv_netdemo.c +++ b/code/server/sv_netdemo.c @@ -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.