Server-side demo recording and playback (netdemo)
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>
This commit is contained in:
parent
ba1c1b5a60
commit
a8044aad8b
8 changed files with 638 additions and 0 deletions
|
|
@ -1012,6 +1012,13 @@ void ClientBegin( int clientNum ) {
|
||||||
client->pers.enterTime = level.time;
|
client->pers.enterTime = level.time;
|
||||||
client->pers.teamState.state = TEAM_BEGIN;
|
client->pers.teamState.state = TEAM_BEGIN;
|
||||||
|
|
||||||
|
// demo playback: force all clients to spectator
|
||||||
|
if ( g_svDemoPlaying.integer ) {
|
||||||
|
client->sess.sessionTeam = TEAM_SPECTATOR;
|
||||||
|
client->ps.pm_type = PM_SPECTATOR;
|
||||||
|
client->ps.persistant[PERS_TEAM] = TEAM_SPECTATOR;
|
||||||
|
}
|
||||||
|
|
||||||
// save eflags around this, because changing teams will
|
// save eflags around this, because changing teams will
|
||||||
// cause this to happen with a valid entity, and we
|
// cause this to happen with a valid entity, and we
|
||||||
// want to make sure the teleport bit is set right
|
// want to make sure the teleport bit is set right
|
||||||
|
|
|
||||||
|
|
@ -641,6 +641,7 @@ void ClientCommand( int clientNum );
|
||||||
// g_active.c
|
// g_active.c
|
||||||
//
|
//
|
||||||
void ClientThink( int clientNum );
|
void ClientThink( int clientNum );
|
||||||
|
void ClientThink_real( gentity_t *ent );
|
||||||
void ClientEndFrame( gentity_t *ent );
|
void ClientEndFrame( gentity_t *ent );
|
||||||
void G_RunClient( gentity_t *ent );
|
void G_RunClient( gentity_t *ent );
|
||||||
|
|
||||||
|
|
@ -715,6 +716,7 @@ extern gentity_t g_entities[MAX_GENTITIES];
|
||||||
#define FOFS(x) ((int)&(((gentity_t *)0)->x))
|
#define FOFS(x) ((int)&(((gentity_t *)0)->x))
|
||||||
|
|
||||||
extern vmCvar_t g_gametype;
|
extern vmCvar_t g_gametype;
|
||||||
|
extern vmCvar_t g_svDemoPlaying;
|
||||||
extern vmCvar_t g_dedicated;
|
extern vmCvar_t g_dedicated;
|
||||||
extern vmCvar_t g_cheats;
|
extern vmCvar_t g_cheats;
|
||||||
extern vmCvar_t g_maxclients; // allow this many total, including spectators
|
extern vmCvar_t g_maxclients; // allow this many total, including spectators
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ gentity_t g_entities[MAX_GENTITIES];
|
||||||
gclient_t g_clients[MAX_CLIENTS];
|
gclient_t g_clients[MAX_CLIENTS];
|
||||||
|
|
||||||
vmCvar_t g_gametype;
|
vmCvar_t g_gametype;
|
||||||
|
vmCvar_t g_svDemoPlaying;
|
||||||
vmCvar_t g_dmflags;
|
vmCvar_t g_dmflags;
|
||||||
vmCvar_t g_fraglimit;
|
vmCvar_t g_fraglimit;
|
||||||
vmCvar_t g_timelimit;
|
vmCvar_t g_timelimit;
|
||||||
|
|
@ -108,6 +109,7 @@ static cvarTable_t gameCvarTable[] = {
|
||||||
|
|
||||||
// latched vars
|
// latched vars
|
||||||
{ &g_gametype, "g_gametype", "0", CVAR_SERVERINFO | CVAR_USERINFO | CVAR_LATCH, 0, qfalse },
|
{ &g_gametype, "g_gametype", "0", CVAR_SERVERINFO | CVAR_USERINFO | CVAR_LATCH, 0, qfalse },
|
||||||
|
{ &g_svDemoPlaying, "sv_demoplaying", "0", CVAR_ROM, 0, qfalse },
|
||||||
|
|
||||||
{ &g_maxclients, "sv_maxclients", "8", CVAR_SERVERINFO | CVAR_LATCH | CVAR_ARCHIVE, 0, qfalse },
|
{ &g_maxclients, "sv_maxclients", "8", CVAR_SERVERINFO | CVAR_LATCH | CVAR_ARCHIVE, 0, qfalse },
|
||||||
{ &g_maxGameClients, "g_maxGameClients", "0", CVAR_SERVERINFO | CVAR_LATCH | CVAR_ARCHIVE, 0, qfalse },
|
{ &g_maxGameClients, "g_maxGameClients", "0", CVAR_SERVERINFO | CVAR_LATCH | CVAR_ARCHIVE, 0, qfalse },
|
||||||
|
|
@ -1729,6 +1731,19 @@ int start, end;
|
||||||
// get any cvar changes
|
// get any cvar changes
|
||||||
G_UpdateCvars();
|
G_UpdateCvars();
|
||||||
|
|
||||||
|
// demo playback: only process spectator client, skip all entity logic
|
||||||
|
if ( g_svDemoPlaying.integer ) {
|
||||||
|
for ( i = 0; i < level.maxclients; i++ ) {
|
||||||
|
ent = &g_entities[i];
|
||||||
|
if ( ent->inuse && ent->client &&
|
||||||
|
ent->client->sess.sessionTeam == TEAM_SPECTATOR &&
|
||||||
|
ent->client->pers.connected == CON_CONNECTED ) {
|
||||||
|
ClientThink_real( ent );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// go through all allocated objects
|
// go through all allocated objects
|
||||||
//
|
//
|
||||||
|
|
|
||||||
|
|
@ -995,6 +995,8 @@
|
||||||
<Optimization Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">MaxSpeed</Optimization>
|
<Optimization Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">MaxSpeed</Optimization>
|
||||||
<Optimization Condition="'$(Configuration)|$(Platform)'=='vector|Win32'">MaxSpeed</Optimization>
|
<Optimization Condition="'$(Configuration)|$(Platform)'=='vector|Win32'">MaxSpeed</Optimization>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
|
<ClCompile Include="server\sv_netdemo.c">
|
||||||
|
</ClCompile>
|
||||||
<ClCompile Include="server\sv_world.c">
|
<ClCompile Include="server\sv_world.c">
|
||||||
<Optimization Condition="'$(Configuration)|$(Platform)'=='Debug TA DEMO|Win32'">Disabled</Optimization>
|
<Optimization Condition="'$(Configuration)|$(Platform)'=='Debug TA DEMO|Win32'">Disabled</Optimization>
|
||||||
<BrowseInformation Condition="'$(Configuration)|$(Platform)'=='Debug TA DEMO|Win32'">true</BrowseInformation>
|
<BrowseInformation Condition="'$(Configuration)|$(Platform)'=='Debug TA DEMO|Win32'">true</BrowseInformation>
|
||||||
|
|
|
||||||
|
|
@ -341,6 +341,19 @@ int SV_BotGetConsoleMessage( int client, char *buf, int size );
|
||||||
int BotImport_DebugPolygonCreate(int color, int numPoints, vec3_t *points);
|
int BotImport_DebugPolygonCreate(int color, int numPoints, vec3_t *points);
|
||||||
void BotImport_DebugPolygonDelete(int id);
|
void BotImport_DebugPolygonDelete(int id);
|
||||||
|
|
||||||
|
//
|
||||||
|
// sv_netdemo.c
|
||||||
|
//
|
||||||
|
void SVD_Record_f( void );
|
||||||
|
void SVD_StopRecord_f( void );
|
||||||
|
void SVD_RecordFrame( void );
|
||||||
|
void SVD_Play_f( void );
|
||||||
|
void SVD_StopPlay_f( void );
|
||||||
|
qboolean SVD_PlaybackFrame( void );
|
||||||
|
qboolean SVD_IsRecording( void );
|
||||||
|
qboolean SVD_IsPlaying( void );
|
||||||
|
int SVD_SpectatorClientNum( void );
|
||||||
|
|
||||||
//============================================================
|
//============================================================
|
||||||
//
|
//
|
||||||
// high level object sorting to reduce interaction tests
|
// high level object sorting to reduce interaction tests
|
||||||
|
|
|
||||||
|
|
@ -736,6 +736,11 @@ void SV_AddOperatorCommands( void ) {
|
||||||
if( com_dedicated->integer ) {
|
if( com_dedicated->integer ) {
|
||||||
Cmd_AddCommand ("say", SV_ConSay_f);
|
Cmd_AddCommand ("say", SV_ConSay_f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// server-side demo recording/playback
|
||||||
|
Cmd_AddCommand ("svdemo_record", SVD_Record_f);
|
||||||
|
Cmd_AddCommand ("svdemo_stop", SVD_StopRecord_f);
|
||||||
|
Cmd_AddCommand ("svdemo_play", SVD_Play_f);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
||||||
|
|
@ -833,8 +833,17 @@ void SV_Frame( int msec ) {
|
||||||
sv.timeResidual -= frameMsec;
|
sv.timeResidual -= frameMsec;
|
||||||
svs.time += frameMsec;
|
svs.time += frameMsec;
|
||||||
|
|
||||||
|
if ( SVD_IsPlaying() ) {
|
||||||
|
// demo playback: read recorded entities instead of running game logic
|
||||||
|
SVD_PlaybackFrame();
|
||||||
|
// still call the game frame for spectator movement
|
||||||
|
}
|
||||||
|
|
||||||
// let everything in the world think and move
|
// let everything in the world think and move
|
||||||
VM_Call( gvm, GAME_RUN_FRAME, svs.time );
|
VM_Call( gvm, GAME_RUN_FRAME, svs.time );
|
||||||
|
|
||||||
|
// capture frame for demo recording
|
||||||
|
SVD_RecordFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( com_speeds->integer ) {
|
if ( com_speeds->integer ) {
|
||||||
|
|
|
||||||
585
code/server/sv_netdemo.c
Normal file
585
code/server/sv_netdemo.c
Normal file
|
|
@ -0,0 +1,585 @@
|
||||||
|
/*
|
||||||
|
===========================================================================
|
||||||
|
Server-side demo recording and playback (netdemo).
|
||||||
|
|
||||||
|
Records the full entity state array each server frame so that
|
||||||
|
demos can be played back from any viewpoint. During playback the
|
||||||
|
recorded entities are injected into sv.gentities and the normal
|
||||||
|
snapshot pipeline delivers them to a spectator client.
|
||||||
|
===========================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "server.h"
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// File format
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// Header:
|
||||||
|
// 4 bytes magic "SVDM"
|
||||||
|
// 4 bytes version (1)
|
||||||
|
// 4 bytes original sv_maxclients
|
||||||
|
// 4 bytes original sv_fps
|
||||||
|
// 64 bytes map name (null-padded)
|
||||||
|
// Then: configstrings block
|
||||||
|
// for each non-empty configstring:
|
||||||
|
// 2 bytes index
|
||||||
|
// 2 bytes string length (incl NUL)
|
||||||
|
// N bytes string data
|
||||||
|
// 2 bytes index = 0xFFFF (terminator)
|
||||||
|
//
|
||||||
|
// Per frame:
|
||||||
|
// 4 bytes serverTime
|
||||||
|
// 2 bytes numEntities (how many entity records follow)
|
||||||
|
// for each entity:
|
||||||
|
// 2 bytes entity number
|
||||||
|
// entityState_t (fixed size, raw)
|
||||||
|
// 4 bytes svFlags
|
||||||
|
// 4 bytes linked
|
||||||
|
// 12 bytes currentOrigin[3]
|
||||||
|
// 12 bytes absmin[3]
|
||||||
|
// 12 bytes absmax[3]
|
||||||
|
// 2 bytes numConfigChanges
|
||||||
|
// for each change:
|
||||||
|
// 2 bytes index
|
||||||
|
// 2 bytes string length (incl NUL)
|
||||||
|
// N bytes string data
|
||||||
|
//
|
||||||
|
// Footer:
|
||||||
|
// 4 bytes serverTime = -1 (end marker)
|
||||||
|
//
|
||||||
|
|
||||||
|
#define SVDEMO_MAGIC (('S') | ('V' << 8) | ('D' << 16) | ('M' << 24))
|
||||||
|
#define SVDEMO_VERSION 1
|
||||||
|
#define SVDEMO_MAX_MAPNAME 64
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// State
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
// recording
|
||||||
|
fileHandle_t recordFile;
|
||||||
|
qboolean recording;
|
||||||
|
char *lastConfigstrings[MAX_CONFIGSTRINGS]; // for delta detection
|
||||||
|
|
||||||
|
// playback
|
||||||
|
fileHandle_t playFile;
|
||||||
|
qboolean playing;
|
||||||
|
int playMaxClients; // original maxclients from the recording
|
||||||
|
int playFps; // original sv_fps
|
||||||
|
char *savedConfigstrings[MAX_CONFIGSTRINGS]; // from demo header, re-applied after map load
|
||||||
|
char playMapName[SVDEMO_MAX_MAPNAME];
|
||||||
|
int spectatorClientNum; // which client slot is the demo spectator
|
||||||
|
int nextFrameTime; // serverTime of next frame to read
|
||||||
|
qboolean endOfDemo;
|
||||||
|
qboolean needConfigstrings; // apply saved configstrings on first frame
|
||||||
|
} svDemo_t;
|
||||||
|
|
||||||
|
static svDemo_t demo;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Recording helpers
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
static void SVD_WriteInt( fileHandle_t f, int v ) {
|
||||||
|
FS_Write( &v, 4, f );
|
||||||
|
}
|
||||||
|
|
||||||
|
static void SVD_WriteShort( fileHandle_t f, short v ) {
|
||||||
|
FS_Write( &v, 2, f );
|
||||||
|
}
|
||||||
|
|
||||||
|
static int SVD_ReadInt( fileHandle_t f ) {
|
||||||
|
int v = 0;
|
||||||
|
FS_Read( &v, 4, f );
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
static short SVD_ReadShort( fileHandle_t f ) {
|
||||||
|
short v = 0;
|
||||||
|
FS_Read( &v, 2, f );
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Write header
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
static void SVD_WriteHeader( fileHandle_t f ) {
|
||||||
|
int i;
|
||||||
|
char mapBuf[SVDEMO_MAX_MAPNAME];
|
||||||
|
|
||||||
|
SVD_WriteInt( f, SVDEMO_MAGIC );
|
||||||
|
SVD_WriteInt( f, SVDEMO_VERSION );
|
||||||
|
SVD_WriteInt( f, sv_maxclients->integer );
|
||||||
|
SVD_WriteInt( f, sv_fps->integer );
|
||||||
|
|
||||||
|
// map name
|
||||||
|
memset( mapBuf, 0, sizeof(mapBuf) );
|
||||||
|
Q_strncpyz( mapBuf, sv.configstrings[CS_SERVERINFO], sizeof(mapBuf) );
|
||||||
|
// actually store the mapname from CS_SERVERINFO... or just the map name
|
||||||
|
{
|
||||||
|
const char *mapname = Cvar_VariableString("mapname");
|
||||||
|
memset( mapBuf, 0, sizeof(mapBuf) );
|
||||||
|
Q_strncpyz( mapBuf, mapname, sizeof(mapBuf) );
|
||||||
|
}
|
||||||
|
FS_Write( mapBuf, SVDEMO_MAX_MAPNAME, f );
|
||||||
|
|
||||||
|
// configstrings
|
||||||
|
for ( i = 0; i < MAX_CONFIGSTRINGS; i++ ) {
|
||||||
|
if ( sv.configstrings[i] && sv.configstrings[i][0] ) {
|
||||||
|
int len = strlen( sv.configstrings[i] ) + 1;
|
||||||
|
SVD_WriteShort( f, (short)i );
|
||||||
|
SVD_WriteShort( f, (short)len );
|
||||||
|
FS_Write( sv.configstrings[i], len, f );
|
||||||
|
|
||||||
|
// store initial copy for delta detection
|
||||||
|
if ( demo.lastConfigstrings[i] ) {
|
||||||
|
Z_Free( demo.lastConfigstrings[i] );
|
||||||
|
}
|
||||||
|
demo.lastConfigstrings[i] = CopyString( sv.configstrings[i] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// terminator
|
||||||
|
SVD_WriteShort( f, (short)0xFFFF );
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Write one frame
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
static void SVD_WriteFrame( fileHandle_t f ) {
|
||||||
|
int i, count;
|
||||||
|
sharedEntity_t *ent;
|
||||||
|
short numChanges;
|
||||||
|
|
||||||
|
SVD_WriteInt( f, svs.time );
|
||||||
|
|
||||||
|
// count active entities
|
||||||
|
count = 0;
|
||||||
|
for ( i = 0; i < sv.num_entities; i++ ) {
|
||||||
|
ent = SV_GentityNum( i );
|
||||||
|
if ( ent->r.linked || ent->s.eType != 0 ) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SVD_WriteShort( f, (short)count );
|
||||||
|
|
||||||
|
// write each active entity
|
||||||
|
for ( i = 0; i < sv.num_entities; i++ ) {
|
||||||
|
ent = SV_GentityNum( i );
|
||||||
|
if ( !ent->r.linked && ent->s.eType == 0 ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
SVD_WriteShort( f, (short)i );
|
||||||
|
FS_Write( &ent->s, sizeof(entityState_t), f );
|
||||||
|
SVD_WriteInt( f, ent->r.svFlags );
|
||||||
|
SVD_WriteInt( f, ent->r.linked );
|
||||||
|
FS_Write( ent->r.currentOrigin, 12, f );
|
||||||
|
FS_Write( ent->r.absmin, 12, f );
|
||||||
|
FS_Write( ent->r.absmax, 12, f );
|
||||||
|
}
|
||||||
|
|
||||||
|
// configstring changes
|
||||||
|
numChanges = 0;
|
||||||
|
for ( i = 0; i < MAX_CONFIGSTRINGS; i++ ) {
|
||||||
|
const char *cur = sv.configstrings[i] ? sv.configstrings[i] : "";
|
||||||
|
const char *old = demo.lastConfigstrings[i] ? demo.lastConfigstrings[i] : "";
|
||||||
|
if ( strcmp( cur, old ) != 0 ) {
|
||||||
|
numChanges++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SVD_WriteShort( f, numChanges );
|
||||||
|
|
||||||
|
if ( numChanges > 0 ) {
|
||||||
|
for ( i = 0; i < MAX_CONFIGSTRINGS; i++ ) {
|
||||||
|
const char *cur = sv.configstrings[i] ? sv.configstrings[i] : "";
|
||||||
|
const char *old = demo.lastConfigstrings[i] ? demo.lastConfigstrings[i] : "";
|
||||||
|
if ( strcmp( cur, old ) != 0 ) {
|
||||||
|
int len = strlen( cur ) + 1;
|
||||||
|
SVD_WriteShort( f, (short)i );
|
||||||
|
SVD_WriteShort( f, (short)len );
|
||||||
|
FS_Write( cur, len, f );
|
||||||
|
|
||||||
|
if ( demo.lastConfigstrings[i] ) {
|
||||||
|
Z_Free( demo.lastConfigstrings[i] );
|
||||||
|
}
|
||||||
|
demo.lastConfigstrings[i] = CopyString( cur );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Recording commands
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
void SVD_Record_f( void ) {
|
||||||
|
char name[MAX_OSPATH];
|
||||||
|
char *s;
|
||||||
|
|
||||||
|
if ( demo.recording ) {
|
||||||
|
Com_Printf( "Already recording a server demo.\n" );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( sv.state != SS_GAME ) {
|
||||||
|
Com_Printf( "Not running a server.\n" );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
s = Cmd_Argv(1);
|
||||||
|
if ( !s[0] ) {
|
||||||
|
Com_Printf( "Usage: svdemo_record <demoname>\n" );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Com_sprintf( name, sizeof(name), "svdemos/%s.svdm", s );
|
||||||
|
|
||||||
|
demo.recordFile = FS_FOpenFileWrite( name );
|
||||||
|
if ( !demo.recordFile ) {
|
||||||
|
Com_Printf( "ERROR: couldn't open %s for writing.\n", name );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Com_Printf( "Recording server demo to %s\n", name );
|
||||||
|
demo.recording = qtrue;
|
||||||
|
|
||||||
|
SVD_WriteHeader( demo.recordFile );
|
||||||
|
}
|
||||||
|
|
||||||
|
void SVD_StopRecord_f( void ) {
|
||||||
|
int i;
|
||||||
|
|
||||||
|
if ( !demo.recording ) {
|
||||||
|
Com_Printf( "Not recording a server demo.\n" );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// write end marker
|
||||||
|
SVD_WriteInt( demo.recordFile, -1 );
|
||||||
|
|
||||||
|
FS_FCloseFile( demo.recordFile );
|
||||||
|
demo.recordFile = 0;
|
||||||
|
demo.recording = qfalse;
|
||||||
|
|
||||||
|
// free configstring tracking
|
||||||
|
for ( i = 0; i < MAX_CONFIGSTRINGS; i++ ) {
|
||||||
|
if ( demo.lastConfigstrings[i] ) {
|
||||||
|
Z_Free( demo.lastConfigstrings[i] );
|
||||||
|
demo.lastConfigstrings[i] = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Com_Printf( "Server demo recording stopped.\n" );
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Called from SV_Frame() after the game has run its frame.
|
||||||
|
*/
|
||||||
|
void SVD_RecordFrame( void ) {
|
||||||
|
if ( !demo.recording ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SVD_WriteFrame( demo.recordFile );
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Playback: read header
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
static qboolean SVD_ReadHeader( fileHandle_t f ) {
|
||||||
|
int magic, version;
|
||||||
|
|
||||||
|
magic = SVD_ReadInt( f );
|
||||||
|
if ( magic != SVDEMO_MAGIC ) {
|
||||||
|
Com_Printf( "Not a valid server demo file.\n" );
|
||||||
|
return qfalse;
|
||||||
|
}
|
||||||
|
|
||||||
|
version = SVD_ReadInt( f );
|
||||||
|
if ( version != SVDEMO_VERSION ) {
|
||||||
|
Com_Printf( "Unsupported server demo version %d.\n", version );
|
||||||
|
return qfalse;
|
||||||
|
}
|
||||||
|
|
||||||
|
demo.playMaxClients = SVD_ReadInt( f );
|
||||||
|
demo.playFps = SVD_ReadInt( f );
|
||||||
|
FS_Read( demo.playMapName, SVDEMO_MAX_MAPNAME, f );
|
||||||
|
demo.playMapName[SVDEMO_MAX_MAPNAME - 1] = '\0';
|
||||||
|
|
||||||
|
// read configstrings — store for re-application after map load
|
||||||
|
{
|
||||||
|
short idx;
|
||||||
|
while (1) {
|
||||||
|
idx = SVD_ReadShort( f );
|
||||||
|
if ( idx == (short)0xFFFF ) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
short len = SVD_ReadShort( f );
|
||||||
|
char buf[BIG_INFO_STRING];
|
||||||
|
if ( len > 0 && len < (short)sizeof(buf) ) {
|
||||||
|
FS_Read( buf, len, f );
|
||||||
|
buf[len - 1] = '\0';
|
||||||
|
if ( demo.savedConfigstrings[idx] ) {
|
||||||
|
Z_Free( demo.savedConfigstrings[idx] );
|
||||||
|
}
|
||||||
|
demo.savedConfigstrings[idx] = CopyString( buf );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return qtrue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Re-apply recorded configstrings after the map has loaded.
|
||||||
|
Called after devmap finishes in SVD_Play_f.
|
||||||
|
*/
|
||||||
|
static void SVD_ApplyConfigstrings( void ) {
|
||||||
|
int i;
|
||||||
|
for ( i = 0; i < MAX_CONFIGSTRINGS; i++ ) {
|
||||||
|
// skip CS_SERVERINFO and CS_SYSTEMINFO — they contain latched cvars
|
||||||
|
// (sv_maxclients, sv_pure, etc.) that would trigger a map restart
|
||||||
|
if ( i == CS_SERVERINFO || i == CS_SYSTEMINFO ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ( demo.savedConfigstrings[i] && demo.savedConfigstrings[i][0] ) {
|
||||||
|
SV_SetConfigstring( i, demo.savedConfigstrings[i] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Playback: read one frame, populate sv.gentities
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
static qboolean SVD_ReadFrame( fileHandle_t f ) {
|
||||||
|
int serverTime;
|
||||||
|
short numEnts, numChanges;
|
||||||
|
int i;
|
||||||
|
sharedEntity_t *ent;
|
||||||
|
|
||||||
|
serverTime = SVD_ReadInt( f );
|
||||||
|
if ( serverTime == -1 ) {
|
||||||
|
return qfalse; // end of demo
|
||||||
|
}
|
||||||
|
|
||||||
|
svs.time = serverTime;
|
||||||
|
|
||||||
|
// clear all entities except the spectator slot
|
||||||
|
for ( i = 0; i < sv.num_entities; i++ ) {
|
||||||
|
if ( i == demo.spectatorClientNum ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ent = SV_GentityNum( i );
|
||||||
|
ent->r.linked = qfalse;
|
||||||
|
ent->s.eType = 0;
|
||||||
|
ent->s.number = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
numEnts = SVD_ReadShort( f );
|
||||||
|
|
||||||
|
for ( i = 0; i < numEnts; i++ ) {
|
||||||
|
short entNum;
|
||||||
|
entityState_t es;
|
||||||
|
int svFlags, linked;
|
||||||
|
vec3_t currentOrigin, absmin, absmax;
|
||||||
|
|
||||||
|
entNum = SVD_ReadShort( f );
|
||||||
|
FS_Read( &es, sizeof(entityState_t), f );
|
||||||
|
svFlags = SVD_ReadInt( f );
|
||||||
|
linked = SVD_ReadInt( f );
|
||||||
|
FS_Read( currentOrigin, 12, f );
|
||||||
|
FS_Read( absmin, 12, f );
|
||||||
|
FS_Read( absmax, 12, f );
|
||||||
|
|
||||||
|
// skip the spectator's slot
|
||||||
|
if ( entNum == demo.spectatorClientNum ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ent = SV_GentityNum( entNum );
|
||||||
|
ent->s = es;
|
||||||
|
ent->s.number = entNum;
|
||||||
|
ent->r.svFlags = svFlags;
|
||||||
|
ent->r.linked = linked;
|
||||||
|
VectorCopy( currentOrigin, ent->r.currentOrigin );
|
||||||
|
VectorCopy( absmin, ent->r.absmin );
|
||||||
|
VectorCopy( absmax, ent->r.absmax );
|
||||||
|
|
||||||
|
// link into the world so PVS can find this entity
|
||||||
|
if ( linked ) {
|
||||||
|
SV_LinkEntity( ent );
|
||||||
|
}
|
||||||
|
|
||||||
|
// update num_entities high water mark
|
||||||
|
if ( entNum + 1 > sv.num_entities ) {
|
||||||
|
sv.num_entities = entNum + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure spectator slot is counted
|
||||||
|
if ( demo.spectatorClientNum + 1 > sv.num_entities ) {
|
||||||
|
sv.num_entities = demo.spectatorClientNum + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// read configstring changes
|
||||||
|
numChanges = SVD_ReadShort( f );
|
||||||
|
for ( i = 0; i < numChanges; i++ ) {
|
||||||
|
short idx = SVD_ReadShort( f );
|
||||||
|
short len = SVD_ReadShort( f );
|
||||||
|
char buf[BIG_INFO_STRING];
|
||||||
|
if ( len > 0 && len < (short)sizeof(buf) ) {
|
||||||
|
FS_Read( buf, len, f );
|
||||||
|
buf[len - 1] = '\0';
|
||||||
|
SV_SetConfigstring( idx, buf );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return qtrue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Playback commands
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
void SVD_Play_f( void ) {
|
||||||
|
char name[MAX_OSPATH];
|
||||||
|
char *s;
|
||||||
|
int len;
|
||||||
|
|
||||||
|
if ( demo.playing ) {
|
||||||
|
Com_Printf( "Already playing a server demo.\n" );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
s = Cmd_Argv(1);
|
||||||
|
if ( !s[0] ) {
|
||||||
|
Com_Printf( "Usage: svdemo_play <demoname>\n" );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Com_sprintf( name, sizeof(name), "svdemos/%s.svdm", s );
|
||||||
|
|
||||||
|
len = FS_FOpenFileRead( name, &demo.playFile, qtrue );
|
||||||
|
if ( !demo.playFile || len <= 0 ) {
|
||||||
|
Com_Printf( "ERROR: couldn't open %s.\n", name );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// read the header (populates demo.playMapName etc.)
|
||||||
|
memset( &demo, 0, sizeof(demo) );
|
||||||
|
demo.playFile = 0; // re-open after memset
|
||||||
|
len = FS_FOpenFileRead( name, &demo.playFile, qtrue );
|
||||||
|
|
||||||
|
if ( !SVD_ReadHeader( demo.playFile ) ) {
|
||||||
|
FS_FCloseFile( demo.playFile );
|
||||||
|
demo.playFile = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
demo.playing = qtrue;
|
||||||
|
demo.endOfDemo = qfalse;
|
||||||
|
|
||||||
|
// The spectator gets the highest slot: MAX_CLIENTS - 1
|
||||||
|
demo.spectatorClientNum = MAX_CLIENTS - 1;
|
||||||
|
|
||||||
|
Com_Printf( "Playing server demo: map=%s maxclients=%d fps=%d\n",
|
||||||
|
demo.playMapName, demo.playMaxClients, demo.playFps );
|
||||||
|
|
||||||
|
// 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) );
|
||||||
|
|
||||||
|
// Signal demo mode to the game module AFTER map load
|
||||||
|
Cvar_Set( "sv_demoplaying", "1" );
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
{
|
||||||
|
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].lastPacketTime = svs.time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configstrings will be applied on the first playback frame,
|
||||||
|
// after the map has fully loaded.
|
||||||
|
demo.needConfigstrings = qtrue;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SVD_StopPlay_f( void ) {
|
||||||
|
int i;
|
||||||
|
|
||||||
|
if ( !demo.playing ) {
|
||||||
|
Com_Printf( "Not playing a server demo.\n" );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FS_FCloseFile( demo.playFile );
|
||||||
|
|
||||||
|
// free saved configstrings
|
||||||
|
for ( i = 0; i < MAX_CONFIGSTRINGS; i++ ) {
|
||||||
|
if ( demo.savedConfigstrings[i] ) {
|
||||||
|
Z_Free( demo.savedConfigstrings[i] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
memset( &demo, 0, sizeof(demo) );
|
||||||
|
|
||||||
|
Cvar_Set( "sv_demoplaying", "0" );
|
||||||
|
Com_Printf( "Server demo playback stopped.\n" );
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Called from SV_Frame() to advance playback by one frame.
|
||||||
|
Returns qtrue if a frame was read, qfalse if demo ended.
|
||||||
|
*/
|
||||||
|
qboolean SVD_PlaybackFrame( void ) {
|
||||||
|
if ( !demo.playing || demo.endOfDemo ) {
|
||||||
|
return qfalse;
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply recorded configstrings once after map load
|
||||||
|
if ( demo.needConfigstrings ) {
|
||||||
|
SVD_ApplyConfigstrings();
|
||||||
|
demo.needConfigstrings = qfalse;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !SVD_ReadFrame( demo.playFile ) ) {
|
||||||
|
demo.endOfDemo = qtrue;
|
||||||
|
Com_Printf( "Server demo playback finished.\n" );
|
||||||
|
return qfalse;
|
||||||
|
}
|
||||||
|
|
||||||
|
return qtrue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Queries
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
qboolean SVD_IsRecording( void ) {
|
||||||
|
return demo.recording;
|
||||||
|
}
|
||||||
|
|
||||||
|
qboolean SVD_IsPlaying( void ) {
|
||||||
|
return demo.playing;
|
||||||
|
}
|
||||||
|
|
||||||
|
int SVD_SpectatorClientNum( void ) {
|
||||||
|
return demo.spectatorClientNum;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue