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.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
|
||||
// cause this to happen with a valid entity, and we
|
||||
// want to make sure the teleport bit is set right
|
||||
|
|
|
|||
|
|
@ -641,6 +641,7 @@ void ClientCommand( int clientNum );
|
|||
// g_active.c
|
||||
//
|
||||
void ClientThink( int clientNum );
|
||||
void ClientThink_real( gentity_t *ent );
|
||||
void ClientEndFrame( 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))
|
||||
|
||||
extern vmCvar_t g_gametype;
|
||||
extern vmCvar_t g_svDemoPlaying;
|
||||
extern vmCvar_t g_dedicated;
|
||||
extern vmCvar_t g_cheats;
|
||||
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];
|
||||
|
||||
vmCvar_t g_gametype;
|
||||
vmCvar_t g_svDemoPlaying;
|
||||
vmCvar_t g_dmflags;
|
||||
vmCvar_t g_fraglimit;
|
||||
vmCvar_t g_timelimit;
|
||||
|
|
@ -108,6 +109,7 @@ static cvarTable_t gameCvarTable[] = {
|
|||
|
||||
// latched vars
|
||||
{ &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_maxGameClients, "g_maxGameClients", "0", CVAR_SERVERINFO | CVAR_LATCH | CVAR_ARCHIVE, 0, qfalse },
|
||||
|
|
@ -1729,6 +1731,19 @@ int start, end;
|
|||
// get any cvar changes
|
||||
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
|
||||
//
|
||||
|
|
|
|||
|
|
@ -995,6 +995,8 @@
|
|||
<Optimization Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">MaxSpeed</Optimization>
|
||||
<Optimization Condition="'$(Configuration)|$(Platform)'=='vector|Win32'">MaxSpeed</Optimization>
|
||||
</ClCompile>
|
||||
<ClCompile Include="server\sv_netdemo.c">
|
||||
</ClCompile>
|
||||
<ClCompile Include="server\sv_world.c">
|
||||
<Optimization Condition="'$(Configuration)|$(Platform)'=='Debug TA DEMO|Win32'">Disabled</Optimization>
|
||||
<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);
|
||||
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
|
||||
|
|
|
|||
|
|
@ -736,6 +736,11 @@ void SV_AddOperatorCommands( void ) {
|
|||
if( com_dedicated->integer ) {
|
||||
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;
|
||||
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
|
||||
VM_Call( gvm, GAME_RUN_FRAME, svs.time );
|
||||
|
||||
// capture frame for demo recording
|
||||
SVD_RecordFrame();
|
||||
}
|
||||
|
||||
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