Compare commits
No commits in common. "3d8291658f539285d511a5db45f64ff67312dc8f" and "6e13c747ba1a55477653d1f82fbcb834347cd806" have entirely different histories.
3d8291658f
...
6e13c747ba
12 changed files with 168 additions and 165 deletions
|
|
@ -1650,16 +1650,6 @@ static void CG_DrawDisconnect( void ) {
|
||||||
const char *s;
|
const char *s;
|
||||||
int w; // bk010215 - FIXME char message[1024];
|
int w; // bk010215 - FIXME char message[1024];
|
||||||
|
|
||||||
// server demo playback: detect pause from frozen snapshot time
|
|
||||||
if ( cg.svDemoPlayback ) {
|
|
||||||
if ( cg.nextSnap && cg.nextSnap->serverTime == cg.snap->serverTime ) {
|
|
||||||
s = "Playback Paused";
|
|
||||||
w = CG_DrawStrlen( s ) * BIGCHAR_WIDTH;
|
|
||||||
CG_DrawBigString( 320 - w/2, 100, s, 1.0F );
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// draw the phone jack if we are completely past our buffers
|
// draw the phone jack if we are completely past our buffers
|
||||||
cmdNum = trap_GetCurrentCmdNumber() - CMD_BACKUP + 1;
|
cmdNum = trap_GetCurrentCmdNumber() - CMD_BACKUP + 1;
|
||||||
trap_GetUserCmd( cmdNum, &cmd );
|
trap_GetUserCmd( cmdNum, &cmd );
|
||||||
|
|
|
||||||
|
|
@ -288,9 +288,6 @@ void CL_ParseSnapshot( msg_t *msg ) {
|
||||||
cl.snapshots[oldMessageNum & PACKET_MASK].valid = qfalse;
|
cl.snapshots[oldMessageNum & PACKET_MASK].valid = qfalse;
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
|
||||||
int oldSnapFlags = cl.snap.snapFlags;
|
|
||||||
|
|
||||||
// copy to the current good spot
|
// copy to the current good spot
|
||||||
cl.snap = newSnap;
|
cl.snap = newSnap;
|
||||||
cl.snap.ping = 999;
|
cl.snap.ping = 999;
|
||||||
|
|
@ -302,15 +299,6 @@ void CL_ParseSnapshot( msg_t *msg ) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// if server count changed (map_restart or demo unpause), force-reset
|
|
||||||
// the time delta so the client doesn't slowly drift back to sync
|
|
||||||
if ( ( oldSnapFlags ^ newSnap.snapFlags ) & SNAPFLAG_SERVERCOUNT ) {
|
|
||||||
cl.serverTimeDelta = cl.snap.serverTime - cls.realtime;
|
|
||||||
cl.oldServerTime = cl.snap.serverTime;
|
|
||||||
cl.serverTime = cl.snap.serverTime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// save the frame off in the backup array for later delta comparisons
|
// save the frame off in the backup array for later delta comparisons
|
||||||
cl.snapshots[cl.snap.messageNum & PACKET_MASK] = cl.snap;
|
cl.snapshots[cl.snap.messageNum & PACKET_MASK] = cl.snap;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1034,12 +1034,6 @@ void ClientThink( int clientNum ) {
|
||||||
// phone jack if they don't get any for a while
|
// phone jack if they don't get any for a while
|
||||||
ent->client->lastCmdTime = level.time;
|
ent->client->lastCmdTime = level.time;
|
||||||
|
|
||||||
// demo playback: don't run ClientThink_real — cgame owns
|
|
||||||
// the camera movement, G_RunFrame handles buttons and PVS origin
|
|
||||||
if ( g_svDemoPlaying.integer ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( !(ent->r.svFlags & SVF_BOT) && !g_synchronousClients.integer ) {
|
if ( !(ent->r.svFlags & SVF_BOT) && !g_synchronousClients.integer ) {
|
||||||
ClientThink_real( ent );
|
ClientThink_real( ent );
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1013,6 +1013,7 @@ void ClientBegin( int clientNum ) {
|
||||||
client->pers.teamState.state = TEAM_BEGIN;
|
client->pers.teamState.state = TEAM_BEGIN;
|
||||||
|
|
||||||
// demo playback: force all clients to spectator
|
// demo playback: force all clients to spectator
|
||||||
|
// demo playback: force to spectator and update configstring
|
||||||
if ( g_svDemoPlaying.integer ) {
|
if ( g_svDemoPlaying.integer ) {
|
||||||
client->sess.sessionTeam = TEAM_SPECTATOR;
|
client->sess.sessionTeam = TEAM_SPECTATOR;
|
||||||
ClientUserinfoChanged( ent->client - level.clients );
|
ClientUserinfoChanged( ent->client - level.clients );
|
||||||
|
|
|
||||||
|
|
@ -662,7 +662,7 @@ void Cmd_Team_f( gentity_t *ent ) {
|
||||||
char s[MAX_TOKEN_CHARS];
|
char s[MAX_TOKEN_CHARS];
|
||||||
|
|
||||||
// demo playback: only allow "team spectator" (to exit follow mode)
|
// demo playback: only allow "team spectator" (to exit follow mode)
|
||||||
if ( g_svDemoPlaying.integer ) {
|
if ( trap_Cvar_VariableIntegerValue( "sv_demoplaying" ) ) {
|
||||||
char s2[MAX_TOKEN_CHARS];
|
char s2[MAX_TOKEN_CHARS];
|
||||||
if ( trap_Argc() == 2 ) {
|
if ( trap_Argc() == 2 ) {
|
||||||
trap_Argv( 1, s2, sizeof(s2) );
|
trap_Argv( 1, s2, sizeof(s2) );
|
||||||
|
|
|
||||||
|
|
@ -419,7 +419,7 @@ void G_InitGame( int levelTime, int randomSeed, int restart ) {
|
||||||
G_RegisterCvars();
|
G_RegisterCvars();
|
||||||
|
|
||||||
// signal server-side demo mode to cgame via configstring
|
// signal server-side demo mode to cgame via configstring
|
||||||
if ( g_svDemoPlaying.integer ) {
|
if ( trap_Cvar_VariableIntegerValue( "sv_demoplaying" ) ) {
|
||||||
trap_SetConfigstring( CS_SVDEMO, "1" );
|
trap_SetConfigstring( CS_SVDEMO, "1" );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1761,17 +1761,6 @@ int start, end;
|
||||||
VectorCopy( cl->pers.cmd.origin, e->s.pos.trBase );
|
VectorCopy( cl->pers.cmd.origin, e->s.pos.trBase );
|
||||||
VectorCopy( cl->pers.cmd.origin, e->r.currentOrigin );
|
VectorCopy( cl->pers.cmd.origin, e->r.currentOrigin );
|
||||||
}
|
}
|
||||||
// process spectator buttons for follow mode switching
|
|
||||||
// (pers.cmd is updated by ClientThink which still runs)
|
|
||||||
{
|
|
||||||
int oldButtons = cl->buttons;
|
|
||||||
cl->oldbuttons = cl->buttons;
|
|
||||||
cl->buttons = cl->pers.cmd.buttons;
|
|
||||||
// attack cycles follow targets
|
|
||||||
if ( ( cl->buttons & BUTTON_ATTACK ) && !( oldButtons & BUTTON_ATTACK ) ) {
|
|
||||||
Cmd_FollowCycle_f( e, 1 );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
specEnt = e;
|
specEnt = e;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -1792,27 +1781,6 @@ int start, end;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// evaluate trajectories and link all recorded entities for PVS.
|
|
||||||
// SVD_ReadFrame injected entityState_t but didn't link because
|
|
||||||
// the server doesn't have BG_EvaluateTrajectory.
|
|
||||||
// scan up to MAX_GENTITIES because recorded entities may exceed
|
|
||||||
// level.num_entities (which only counts game-module-spawned entities).
|
|
||||||
for ( i = 0; i < MAX_GENTITIES; i++ ) {
|
|
||||||
ent = &g_entities[i];
|
|
||||||
if ( ent->s.eType == 0 && !ent->inuse ) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// skip spectator slot
|
|
||||||
if ( ent->client && ent->client->pers.connected == CON_CONNECTED
|
|
||||||
&& ent->client->sess.sessionTeam == TEAM_SPECTATOR ) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// compute currentOrigin from trajectory
|
|
||||||
BG_EvaluateTrajectory( &ent->s.pos, level.time, ent->r.currentOrigin );
|
|
||||||
ent->r.linked = qtrue;
|
|
||||||
trap_LinkEntity( ent );
|
|
||||||
}
|
|
||||||
|
|
||||||
// update rankings so follow mode can cycle through players
|
// update rankings so follow mode can cycle through players
|
||||||
CalculateRanks();
|
CalculateRanks();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -183,12 +183,6 @@ G_WriteSessionData
|
||||||
void G_WriteSessionData( void ) {
|
void G_WriteSessionData( void ) {
|
||||||
int i;
|
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) );
|
trap_Cvar_Set( "session", va("%i", g_gametype.integer) );
|
||||||
|
|
||||||
for ( i = 0 ; i < level.maxclients ; i++ ) {
|
for ( i = 0 ; i < level.maxclients ; i++ ) {
|
||||||
|
|
|
||||||
|
|
@ -997,6 +997,8 @@
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<ClCompile Include="server\sv_netdemo.c">
|
<ClCompile Include="server\sv_netdemo.c">
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
|
<ClCompile Include="server\lz4.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>
|
||||||
|
|
|
||||||
|
|
@ -352,14 +352,12 @@ void SVD_AutoRecord( void );
|
||||||
void SVD_CaptureServerCommand( const char *cmd );
|
void SVD_CaptureServerCommand( const char *cmd );
|
||||||
void SVD_Play_f( void );
|
void SVD_Play_f( void );
|
||||||
void SVD_StopPlay_f( void );
|
void SVD_StopPlay_f( void );
|
||||||
void SVD_CleanupPlayback( void );
|
|
||||||
void SVD_Stop_f( void );
|
void SVD_Stop_f( void );
|
||||||
void SVD_Pause_f( void );
|
void SVD_Pause_f( void );
|
||||||
qboolean SVD_PlaybackFrame( void );
|
qboolean SVD_PlaybackFrame( void );
|
||||||
qboolean SVD_IsRecording( void );
|
qboolean SVD_IsRecording( void );
|
||||||
qboolean SVD_IsPlaying( void );
|
qboolean SVD_IsPlaying( void );
|
||||||
qboolean SVD_IsPaused( void );
|
qboolean SVD_IsPaused( void );
|
||||||
qboolean SVD_IsStarting( void );
|
|
||||||
qboolean SVD_ShouldPause( void );
|
qboolean SVD_ShouldPause( void );
|
||||||
|
|
||||||
//============================================================
|
//============================================================
|
||||||
|
|
|
||||||
|
|
@ -1419,13 +1419,8 @@ static void SV_UserMove( client_t *cl, msg_t *msg, qboolean delta ) {
|
||||||
// don't execute if this is an old cmd which is already executed
|
// don't execute if this is an old cmd which is already executed
|
||||||
// these old cmds are included when cl_packetdup > 0
|
// these old cmds are included when cl_packetdup > 0
|
||||||
if ( cmds[i].serverTime <= cl->lastUsercmd.serverTime ) {
|
if ( cmds[i].serverTime <= cl->lastUsercmd.serverTime ) {
|
||||||
// demo playback: still process the LAST cmd even with duplicate
|
|
||||||
// serverTime (paused = frozen time, all cmds have same time).
|
|
||||||
// Need buttons and origin from fresh usercmds.
|
|
||||||
if ( !SVD_IsPlaying() || i != cmdCount - 1 ) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
SV_ClientThink (cl, &cmds[ i ]);
|
SV_ClientThink (cl, &cmds[ i ]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -351,17 +351,11 @@ void SV_SpawnServer( char *server, qboolean killBots ) {
|
||||||
char systemInfo[16384];
|
char systemInfo[16384];
|
||||||
const char *p;
|
const char *p;
|
||||||
|
|
||||||
// stop any active demo recording/playback (one demo = one map).
|
// stop any active demo recording (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() ) {
|
if ( SVD_IsRecording() ) {
|
||||||
Com_Printf( "Map change — stopping demo recording.\n" );
|
Com_Printf( "Map change — stopping demo recording.\n" );
|
||||||
SVD_StopRecord_f();
|
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
|
// shut down the existing game if it is running
|
||||||
SV_ShutdownGameProgs();
|
SV_ShutdownGameProgs();
|
||||||
|
|
@ -629,6 +623,7 @@ void SV_Init (void) {
|
||||||
|
|
||||||
// server-side demo settings
|
// server-side demo settings
|
||||||
Cvar_Get ("svdemo_autorecord", "0", CVAR_ARCHIVE);
|
Cvar_Get ("svdemo_autorecord", "0", CVAR_ARCHIVE);
|
||||||
|
Cvar_Get ("svdemo_compress", "1", CVAR_ARCHIVE);
|
||||||
Cvar_Get ("svdemo_pauseEmpty", "1", CVAR_ARCHIVE);
|
Cvar_Get ("svdemo_pauseEmpty", "1", CVAR_ARCHIVE);
|
||||||
|
|
||||||
// initialize bot cvars so they are listed and can be set before loading the botlib
|
// initialize bot cvars so they are listed and can be set before loading the botlib
|
||||||
|
|
@ -686,13 +681,12 @@ void SV_Shutdown( char *finalmsg ) {
|
||||||
|
|
||||||
Com_Printf( "----- Server Shutdown -----\n" );
|
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() ) {
|
if ( SVD_IsRecording() ) {
|
||||||
SVD_StopRecord_f();
|
SVD_StopRecord_f();
|
||||||
}
|
}
|
||||||
if ( SVD_IsPlaying() && !SVD_IsStarting() ) {
|
if ( SVD_IsPlaying() ) {
|
||||||
SVD_CleanupPlayback();
|
SVD_StopPlay_f();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( svs.clients && !com_errorEntered ) {
|
if ( svs.clients && !com_errorEntered ) {
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,7 @@ snapshot pipeline delivers them to a spectator client.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "server.h"
|
#include "server.h"
|
||||||
|
#include "lz4.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
|
// File format
|
||||||
|
|
@ -53,19 +51,24 @@ extern cvar_t *Cvar_Set2( const char *var_name, const char *value, qboolean forc
|
||||||
//
|
//
|
||||||
|
|
||||||
#define SVDEMO_MAGIC (('S') | ('V' << 8) | ('D' << 16) | ('M' << 24))
|
#define SVDEMO_MAGIC (('S') | ('V' << 8) | ('D' << 16) | ('M' << 24))
|
||||||
#define SVDEMO_VERSION 3 // v3: removed PVS data, svFlags only
|
#define SVDEMO_VERSION 2 // v2: optional LZ4 compression
|
||||||
#define SVDEMO_MAX_MAPNAME 64
|
#define SVDEMO_MAX_MAPNAME 64
|
||||||
|
|
||||||
// header flags
|
// header flags
|
||||||
|
#define SVDEMO_FLAG_COMPRESSED 1 // per-frame data is LZ4 compressed
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
// State
|
// State
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
// per-entity data stored for delta compression
|
// per-entity data stored for delta compression (entityState + PVS fields)
|
||||||
typedef struct {
|
typedef struct {
|
||||||
entityState_t es;
|
entityState_t es;
|
||||||
int svFlags;
|
int svFlags;
|
||||||
|
qboolean linked;
|
||||||
|
vec3_t currentOrigin;
|
||||||
|
vec3_t absmin;
|
||||||
|
vec3_t absmax;
|
||||||
qboolean active; // was this entity present last frame?
|
qboolean active; // was this entity present last frame?
|
||||||
} svdEntityState_t;
|
} svdEntityState_t;
|
||||||
|
|
||||||
|
|
@ -82,6 +85,7 @@ typedef struct {
|
||||||
// recording
|
// recording
|
||||||
fileHandle_t recordFile;
|
fileHandle_t recordFile;
|
||||||
qboolean recording;
|
qboolean recording;
|
||||||
|
qboolean compressed; // LZ4 compression enabled
|
||||||
char *lastConfigstrings[MAX_CONFIGSTRINGS]; // for delta detection
|
char *lastConfigstrings[MAX_CONFIGSTRINGS]; // for delta detection
|
||||||
svdEntityState_t prevEntities[MAX_GENTITIES]; // previous frame for delta
|
svdEntityState_t prevEntities[MAX_GENTITIES]; // previous frame for delta
|
||||||
svdPlayerState_t prevPlayers[MAX_CLIENTS]; // previous frame player states
|
svdPlayerState_t prevPlayers[MAX_CLIENTS]; // previous frame player states
|
||||||
|
|
@ -100,13 +104,13 @@ typedef struct {
|
||||||
char playMapName[SVDEMO_MAX_MAPNAME];
|
char playMapName[SVDEMO_MAX_MAPNAME];
|
||||||
qboolean endOfDemo;
|
qboolean endOfDemo;
|
||||||
qboolean needConfigstrings; // apply saved configstrings on first frame
|
qboolean needConfigstrings; // apply saved configstrings on first frame
|
||||||
qboolean starting; // SVD_Play_f is running devmap internally
|
|
||||||
qboolean paused;
|
qboolean paused;
|
||||||
svdEntityState_t playPrevEntities[MAX_GENTITIES]; // previous frame for delta read
|
svdEntityState_t playPrevEntities[MAX_GENTITIES]; // previous frame for delta read
|
||||||
svdPlayerState_t playPrevPlayers[MAX_CLIENTS]; // previous frame player states
|
svdPlayerState_t playPrevPlayers[MAX_CLIENTS]; // previous frame player states
|
||||||
} svDemo_t;
|
} svDemo_t;
|
||||||
|
|
||||||
static svDemo_t demo;
|
static svDemo_t demo;
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
// Recording helpers
|
// Recording helpers
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
|
@ -131,6 +135,58 @@ static short SVD_ReadShort( fileHandle_t f ) {
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// LZ4 block I/O: writes [uncompressed_size][compressed_size][data]
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
static void SVD_WriteBlock( fileHandle_t f, const byte *data, int len ) {
|
||||||
|
if ( demo.compressed && len > 0 ) {
|
||||||
|
int bound = LZ4_compressBound( len );
|
||||||
|
static byte compBuf[MAX_GENTITIES * 300];
|
||||||
|
int compLen;
|
||||||
|
|
||||||
|
compLen = LZ4_compress_default( (const char *)data, (char *)compBuf, len, bound );
|
||||||
|
if ( compLen > 0 ) {
|
||||||
|
SVD_WriteInt( f, len ); // original size
|
||||||
|
SVD_WriteInt( f, compLen ); // compressed size
|
||||||
|
FS_Write( compBuf, compLen, f );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// fall through to uncompressed on failure
|
||||||
|
}
|
||||||
|
SVD_WriteInt( f, len ); // original size
|
||||||
|
SVD_WriteInt( f, 0 ); // 0 = not compressed
|
||||||
|
FS_Write( data, len, f );
|
||||||
|
}
|
||||||
|
|
||||||
|
static int SVD_ReadBlock( fileHandle_t f, byte *buf, int bufSize ) {
|
||||||
|
int origLen, compLen;
|
||||||
|
|
||||||
|
origLen = SVD_ReadInt( f );
|
||||||
|
compLen = SVD_ReadInt( f );
|
||||||
|
|
||||||
|
if ( origLen <= 0 || origLen > bufSize ) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( compLen > 0 ) {
|
||||||
|
// compressed
|
||||||
|
static byte compBuf[MAX_GENTITIES * 300];
|
||||||
|
if ( compLen > (int)sizeof(compBuf) ) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
FS_Read( compBuf, compLen, f );
|
||||||
|
if ( LZ4_decompress_safe( (const char *)compBuf, (char *)buf, compLen, bufSize ) != origLen ) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// uncompressed
|
||||||
|
FS_Read( buf, origLen, f );
|
||||||
|
}
|
||||||
|
|
||||||
|
return origLen;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
// Write header
|
// Write header
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
|
@ -141,7 +197,7 @@ static void SVD_WriteHeader( fileHandle_t f ) {
|
||||||
|
|
||||||
SVD_WriteInt( f, SVDEMO_MAGIC );
|
SVD_WriteInt( f, SVDEMO_MAGIC );
|
||||||
SVD_WriteInt( f, SVDEMO_VERSION );
|
SVD_WriteInt( f, SVDEMO_VERSION );
|
||||||
SVD_WriteInt( f, 0 ); // flags (reserved)
|
SVD_WriteInt( f, demo.compressed ? SVDEMO_FLAG_COMPRESSED : 0 );
|
||||||
SVD_WriteInt( f, sv_maxclients->integer );
|
SVD_WriteInt( f, sv_maxclients->integer );
|
||||||
SVD_WriteInt( f, sv_fps->integer );
|
SVD_WriteInt( f, sv_fps->integer );
|
||||||
|
|
||||||
|
|
@ -219,17 +275,38 @@ static void SVD_WriteFrame( fileHandle_t f ) {
|
||||||
}
|
}
|
||||||
MSG_WriteDeltaEntity( &msg, from, &ent->s, qtrue );
|
MSG_WriteDeltaEntity( &msg, from, &ent->s, qtrue );
|
||||||
|
|
||||||
// write svFlags only if changed (rarely changes)
|
// write PVS fields only if changed from previous frame
|
||||||
if ( ent->r.svFlags != demo.prevEntities[i].svFlags ) {
|
{
|
||||||
MSG_WriteBits( &msg, 1, 1 );
|
qboolean pvsChanged = !demo.prevEntities[i].active
|
||||||
|
|| ent->r.svFlags != demo.prevEntities[i].svFlags
|
||||||
|
|| ent->r.linked != demo.prevEntities[i].linked
|
||||||
|
|| !VectorCompare( ent->r.currentOrigin, demo.prevEntities[i].currentOrigin )
|
||||||
|
|| !VectorCompare( ent->r.absmin, demo.prevEntities[i].absmin )
|
||||||
|
|| !VectorCompare( ent->r.absmax, demo.prevEntities[i].absmax );
|
||||||
|
|
||||||
|
MSG_WriteBits( &msg, pvsChanged, 1 );
|
||||||
|
if ( pvsChanged ) {
|
||||||
MSG_WriteLong( &msg, ent->r.svFlags );
|
MSG_WriteLong( &msg, ent->r.svFlags );
|
||||||
} else {
|
MSG_WriteLong( &msg, ent->r.linked );
|
||||||
MSG_WriteBits( &msg, 0, 1 );
|
MSG_WriteFloat( &msg, ent->r.currentOrigin[0] );
|
||||||
|
MSG_WriteFloat( &msg, ent->r.currentOrigin[1] );
|
||||||
|
MSG_WriteFloat( &msg, ent->r.currentOrigin[2] );
|
||||||
|
MSG_WriteFloat( &msg, ent->r.absmin[0] );
|
||||||
|
MSG_WriteFloat( &msg, ent->r.absmin[1] );
|
||||||
|
MSG_WriteFloat( &msg, ent->r.absmin[2] );
|
||||||
|
MSG_WriteFloat( &msg, ent->r.absmax[0] );
|
||||||
|
MSG_WriteFloat( &msg, ent->r.absmax[1] );
|
||||||
|
MSG_WriteFloat( &msg, ent->r.absmax[2] );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// update prev state
|
// update prev state
|
||||||
demo.prevEntities[i].es = ent->s;
|
demo.prevEntities[i].es = ent->s;
|
||||||
demo.prevEntities[i].svFlags = ent->r.svFlags;
|
demo.prevEntities[i].svFlags = ent->r.svFlags;
|
||||||
|
demo.prevEntities[i].linked = ent->r.linked;
|
||||||
|
VectorCopy( ent->r.currentOrigin, demo.prevEntities[i].currentOrigin );
|
||||||
|
VectorCopy( ent->r.absmin, demo.prevEntities[i].absmin );
|
||||||
|
VectorCopy( ent->r.absmax, demo.prevEntities[i].absmax );
|
||||||
demo.prevEntities[i].active = qtrue;
|
demo.prevEntities[i].active = qtrue;
|
||||||
} else if ( demo.prevEntities[i].active ) {
|
} else if ( demo.prevEntities[i].active ) {
|
||||||
// entity was removed — write a remove marker
|
// entity was removed — write a remove marker
|
||||||
|
|
@ -242,9 +319,8 @@ static void SVD_WriteFrame( fileHandle_t f ) {
|
||||||
// end of entities marker
|
// end of entities marker
|
||||||
MSG_WriteBits( &msg, (MAX_GENTITIES - 1), GENTITYNUM_BITS );
|
MSG_WriteBits( &msg, (MAX_GENTITIES - 1), GENTITYNUM_BITS );
|
||||||
|
|
||||||
// write entity message to file
|
// write entity message to file (optionally LZ4 compressed)
|
||||||
SVD_WriteInt( f, msg.cursize );
|
SVD_WriteBlock( f, msg.data, msg.cursize );
|
||||||
FS_Write( msg.data, msg.cursize, f );
|
|
||||||
|
|
||||||
// write player states (delta compressed)
|
// write player states (delta compressed)
|
||||||
{
|
{
|
||||||
|
|
@ -295,8 +371,7 @@ static void SVD_WriteFrame( fileHandle_t f ) {
|
||||||
MSG_WriteBits( &psmsg, MAX_CLIENTS - 1, 6 );
|
MSG_WriteBits( &psmsg, MAX_CLIENTS - 1, 6 );
|
||||||
MSG_WriteBits( &psmsg, 0, 1 );
|
MSG_WriteBits( &psmsg, 0, 1 );
|
||||||
|
|
||||||
SVD_WriteInt( f, psmsg.cursize );
|
SVD_WriteBlock( f, psmsg.data, psmsg.cursize );
|
||||||
FS_Write( psmsg.data, psmsg.cursize, f );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// configstring changes
|
// configstring changes
|
||||||
|
|
@ -367,8 +442,10 @@ static qboolean SVD_StartRecording( const char *demoname ) {
|
||||||
return qfalse;
|
return qfalse;
|
||||||
}
|
}
|
||||||
|
|
||||||
Com_Printf( "Recording server demo to %s\n", path );
|
Com_Printf( "Recording server demo to %s%s\n", path,
|
||||||
|
Cvar_VariableIntegerValue("svdemo_compress") ? " (LZ4)" : "" );
|
||||||
demo.recording = qtrue;
|
demo.recording = qtrue;
|
||||||
|
demo.compressed = Cvar_VariableIntegerValue("svdemo_compress") ? qtrue : qfalse;
|
||||||
|
|
||||||
// clear delta state for fresh recording
|
// clear delta state for fresh recording
|
||||||
Com_Memset( demo.prevEntities, 0, sizeof(demo.prevEntities) );
|
Com_Memset( demo.prevEntities, 0, sizeof(demo.prevEntities) );
|
||||||
|
|
@ -515,7 +592,10 @@ static qboolean SVD_ReadHeader( fileHandle_t f ) {
|
||||||
return qfalse;
|
return qfalse;
|
||||||
}
|
}
|
||||||
|
|
||||||
SVD_ReadInt( f ); // flags (reserved)
|
{
|
||||||
|
int flags = SVD_ReadInt( f );
|
||||||
|
demo.compressed = ( flags & SVDEMO_FLAG_COMPRESSED ) ? qtrue : qfalse;
|
||||||
|
}
|
||||||
|
|
||||||
demo.playMaxClients = SVD_ReadInt( f );
|
demo.playMaxClients = SVD_ReadInt( f );
|
||||||
demo.playFps = SVD_ReadInt( f );
|
demo.playFps = SVD_ReadInt( f );
|
||||||
|
|
@ -606,12 +686,11 @@ static qboolean SVD_ReadFrame( fileHandle_t f ) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// read entity message
|
// read entity message (optionally LZ4 compressed)
|
||||||
blockLen = SVD_ReadInt( f );
|
blockLen = SVD_ReadBlock( f, msgBuf, sizeof(msgBuf) );
|
||||||
if ( blockLen <= 0 || blockLen > (int)sizeof(msgBuf) ) {
|
if ( blockLen <= 0 ) {
|
||||||
return qfalse;
|
return qfalse;
|
||||||
}
|
}
|
||||||
FS_Read( msgBuf, blockLen, f );
|
|
||||||
MSG_Init( &msg, msgBuf, sizeof(msgBuf) );
|
MSG_Init( &msg, msgBuf, sizeof(msgBuf) );
|
||||||
msg.cursize = blockLen;
|
msg.cursize = blockLen;
|
||||||
|
|
||||||
|
|
@ -655,20 +734,40 @@ static qboolean SVD_ReadFrame( fileHandle_t f ) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// read svFlags
|
// read PVS fields
|
||||||
if ( MSG_ReadBits( &msg, 1 ) ) {
|
{
|
||||||
|
qboolean pvsChanged = MSG_ReadBits( &msg, 1 );
|
||||||
|
if ( pvsChanged ) {
|
||||||
demo.playPrevEntities[entNum].svFlags = MSG_ReadLong( &msg );
|
demo.playPrevEntities[entNum].svFlags = MSG_ReadLong( &msg );
|
||||||
|
demo.playPrevEntities[entNum].linked = MSG_ReadLong( &msg );
|
||||||
|
demo.playPrevEntities[entNum].currentOrigin[0] = MSG_ReadFloat( &msg );
|
||||||
|
demo.playPrevEntities[entNum].currentOrigin[1] = MSG_ReadFloat( &msg );
|
||||||
|
demo.playPrevEntities[entNum].currentOrigin[2] = MSG_ReadFloat( &msg );
|
||||||
|
demo.playPrevEntities[entNum].absmin[0] = MSG_ReadFloat( &msg );
|
||||||
|
demo.playPrevEntities[entNum].absmin[1] = MSG_ReadFloat( &msg );
|
||||||
|
demo.playPrevEntities[entNum].absmin[2] = MSG_ReadFloat( &msg );
|
||||||
|
demo.playPrevEntities[entNum].absmax[0] = MSG_ReadFloat( &msg );
|
||||||
|
demo.playPrevEntities[entNum].absmax[1] = MSG_ReadFloat( &msg );
|
||||||
|
demo.playPrevEntities[entNum].absmax[2] = MSG_ReadFloat( &msg );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
demo.playPrevEntities[entNum].es = newEs;
|
demo.playPrevEntities[entNum].es = newEs;
|
||||||
demo.playPrevEntities[entNum].active = qtrue;
|
demo.playPrevEntities[entNum].active = qtrue;
|
||||||
|
|
||||||
// apply to server entity (entity linking done in G_RunFrame
|
// apply to server entity
|
||||||
// which has BG_EvaluateTrajectory for computing currentOrigin)
|
|
||||||
ent = SV_GentityNum( entNum );
|
ent = SV_GentityNum( entNum );
|
||||||
ent->s = newEs;
|
ent->s = newEs;
|
||||||
ent->s.number = entNum;
|
ent->s.number = entNum;
|
||||||
ent->r.svFlags = demo.playPrevEntities[entNum].svFlags;
|
ent->r.svFlags = demo.playPrevEntities[entNum].svFlags;
|
||||||
|
ent->r.linked = demo.playPrevEntities[entNum].linked;
|
||||||
|
VectorCopy( demo.playPrevEntities[entNum].currentOrigin, ent->r.currentOrigin );
|
||||||
|
VectorCopy( demo.playPrevEntities[entNum].absmin, ent->r.absmin );
|
||||||
|
VectorCopy( demo.playPrevEntities[entNum].absmax, ent->r.absmax );
|
||||||
|
|
||||||
|
if ( ent->r.linked ) {
|
||||||
|
SV_LinkEntity( ent );
|
||||||
|
}
|
||||||
|
|
||||||
if ( entNum + 1 > sv.num_entities ) {
|
if ( entNum + 1 > sv.num_entities ) {
|
||||||
sv.num_entities = entNum + 1;
|
sv.num_entities = entNum + 1;
|
||||||
|
|
@ -681,9 +780,8 @@ static qboolean SVD_ReadFrame( fileHandle_t f ) {
|
||||||
static byte psBuf[MAX_CLIENTS * 600]; // worst case: full playerState from baseline
|
static byte psBuf[MAX_CLIENTS * 600]; // worst case: full playerState from baseline
|
||||||
int psMsgLen;
|
int psMsgLen;
|
||||||
|
|
||||||
psMsgLen = SVD_ReadInt( f );
|
psMsgLen = SVD_ReadBlock( f, psBuf, sizeof(psBuf) );
|
||||||
if ( psMsgLen > 0 && psMsgLen <= (int)sizeof(psBuf) ) {
|
if ( psMsgLen > 0 ) {
|
||||||
FS_Read( psBuf, psMsgLen, f );
|
|
||||||
MSG_Init( &psmsg, psBuf, sizeof(psBuf) );
|
MSG_Init( &psmsg, psBuf, sizeof(psBuf) );
|
||||||
psmsg.cursize = psMsgLen;
|
psmsg.cursize = psMsgLen;
|
||||||
|
|
||||||
|
|
@ -773,8 +871,8 @@ void SVD_Play_f( void ) {
|
||||||
char *s;
|
char *s;
|
||||||
int len;
|
int len;
|
||||||
|
|
||||||
if ( demo.recording ) {
|
if ( demo.playing ) {
|
||||||
Com_Printf( "Stop recording first (svdemo_stop).\n" );
|
Com_Printf( "Already playing a server demo.\n" );
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -784,11 +882,6 @@ void SVD_Play_f( void ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// stop current playback if switching demos
|
|
||||||
if ( demo.playing ) {
|
|
||||||
SVD_CleanupPlayback();
|
|
||||||
}
|
|
||||||
|
|
||||||
Com_sprintf( name, sizeof(name), "svdemos/%s.svdm", s );
|
Com_sprintf( name, sizeof(name), "svdemos/%s.svdm", s );
|
||||||
|
|
||||||
memset( &demo, 0, sizeof(demo) );
|
memset( &demo, 0, sizeof(demo) );
|
||||||
|
|
@ -807,46 +900,42 @@ void SVD_Play_f( void ) {
|
||||||
|
|
||||||
demo.playing = qtrue;
|
demo.playing = qtrue;
|
||||||
demo.endOfDemo = qfalse;
|
demo.endOfDemo = qfalse;
|
||||||
demo.needConfigstrings = qtrue;
|
|
||||||
|
|
||||||
Com_Printf( "Playing server demo: map=%s maxclients=%d fps=%d\n",
|
Com_Printf( "Playing server demo: map=%s maxclients=%d fps=%d\n",
|
||||||
demo.playMapName, demo.playMaxClients, demo.playFps );
|
demo.playMapName, demo.playMaxClients, demo.playFps );
|
||||||
|
|
||||||
// Shut down current server first so no clients carry over to
|
// Signal demo mode BEFORE map load so the game module knows
|
||||||
// reserved slots. SV_Shutdown triggers our cleanup hook, but
|
// during ClientConnect/ClientBegin to force spectator team.
|
||||||
// demo.starting prevents it from clearing our state.
|
Cvar_Set( "sv_demoplaying", "1" );
|
||||||
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_maxclients %d\n", MAX_CLIENTS) );
|
||||||
Cbuf_ExecuteText( EXEC_NOW, va("set sv_fps %d\n", demo.playFps) );
|
Cbuf_ExecuteText( EXEC_NOW, va("set sv_fps %d\n", demo.playFps) );
|
||||||
Cbuf_ExecuteText( EXEC_NOW, va("devmap %s\n", demo.playMapName) );
|
Cbuf_ExecuteText( EXEC_NOW, va("devmap %s\n", demo.playMapName) );
|
||||||
demo.starting = qfalse;
|
|
||||||
|
|
||||||
// CS_SVDEMO configstring is set by G_InitGame from the cvar
|
// Reserve recorded player slots so the connecting spectator
|
||||||
|
// doesn't land in slot 0 (which collides with recorded player 0).
|
||||||
// Reserve recorded player slots. Server is fresh (SV_Shutdown cleared
|
// Mark as CS_ZOMBIE (non-free, won't be reused by SV_DirectConnect).
|
||||||
// old clients), local client hasn't connected yet.
|
// Set rate and nextSnapshotTime so SV_SendClientMessages doesn't
|
||||||
|
// crash on division by zero or try to send them snapshots.
|
||||||
{
|
{
|
||||||
int i;
|
int i;
|
||||||
for ( i = 0; i < demo.playMaxClients; i++ ) {
|
for ( i = 0; i < demo.playMaxClients; i++ ) {
|
||||||
if ( svs.clients[i].state == CS_FREE ) {
|
if ( svs.clients[i].state == CS_FREE ) {
|
||||||
svs.clients[i].state = CS_ZOMBIE;
|
svs.clients[i].state = CS_ZOMBIE;
|
||||||
svs.clients[i].rate = 10000;
|
svs.clients[i].rate = 10000;
|
||||||
svs.clients[i].nextSnapshotTime = 0x7FFFFFFF;
|
svs.clients[i].nextSnapshotTime = 0x7FFFFFFF; // never send
|
||||||
svs.clients[i].lastPacketTime = svs.time;
|
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_CleanupPlayback( void ) {
|
static void SVD_CleanupPlayback( void ) {
|
||||||
int i;
|
int i;
|
||||||
|
|
||||||
if ( !demo.playing ) {
|
if ( !demo.playing ) {
|
||||||
|
|
@ -870,7 +959,7 @@ void SVD_CleanupPlayback( void ) {
|
||||||
}
|
}
|
||||||
|
|
||||||
memset( &demo, 0, sizeof(demo) );
|
memset( &demo, 0, sizeof(demo) );
|
||||||
Cvar_Set2( "sv_demoplaying", "0", qtrue );
|
Cvar_Set( "sv_demoplaying", "0" );
|
||||||
}
|
}
|
||||||
|
|
||||||
void SVD_StopPlay_f( void ) {
|
void SVD_StopPlay_f( void ) {
|
||||||
|
|
@ -905,11 +994,6 @@ void SVD_Pause_f( void ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
demo.paused = !demo.paused;
|
demo.paused = !demo.paused;
|
||||||
if ( !demo.paused ) {
|
|
||||||
// resuming — toggle SERVERCOUNT to reset client snapshot timing
|
|
||||||
// (drifted during pause from identical serverTimes).
|
|
||||||
svs.snapFlagServerBit ^= SNAPFLAG_SERVERCOUNT;
|
|
||||||
}
|
|
||||||
Com_Printf( "Demo playback %s.\n", demo.paused ? "paused" : "resumed" );
|
Com_Printf( "Demo playback %s.\n", demo.paused ? "paused" : "resumed" );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -969,11 +1053,6 @@ qboolean SVD_IsPaused( void ) {
|
||||||
return demo.playing && demo.paused;
|
return demo.playing && demo.paused;
|
||||||
}
|
}
|
||||||
|
|
||||||
qboolean SVD_IsStarting( void ) {
|
|
||||||
return demo.starting;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Returns qtrue if demo playback should pause (no active spectators).
|
Returns qtrue if demo playback should pause (no active spectators).
|
||||||
Controlled by svdemo_pauseEmpty cvar.
|
Controlled by svdemo_pauseEmpty cvar.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue