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:
parent
74e2fc39c8
commit
f871cc004f
5 changed files with 67 additions and 23 deletions
|
|
@ -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 );
|
||||
|
|
|
|||
|
|
@ -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++ ) {
|
||||
|
|
|
|||
|
|
@ -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 );
|
||||
|
||||
//============================================================
|
||||
|
|
|
|||
|
|
@ -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 ) {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue