Compare commits
10 commits
6e13c747ba
...
3d8291658f
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d8291658f | |||
| 72d5df4ec9 | |||
| f871cc004f | |||
| 74e2fc39c8 | |||
| fe628a2cc4 | |||
| 5a98ef02cf | |||
| 2ce4aa88f2 | |||
| c4dca5f950 | |||
| 41f0ca2e50 | |||
| 490fcd9bde |
12 changed files with 165 additions and 168 deletions
|
|
@ -1650,6 +1650,16 @@ 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,17 +288,29 @@ void CL_ParseSnapshot( msg_t *msg ) {
|
||||||
cl.snapshots[oldMessageNum & PACKET_MASK].valid = qfalse;
|
cl.snapshots[oldMessageNum & PACKET_MASK].valid = qfalse;
|
||||||
}
|
}
|
||||||
|
|
||||||
// copy to the current good spot
|
{
|
||||||
cl.snap = newSnap;
|
int oldSnapFlags = cl.snap.snapFlags;
|
||||||
cl.snap.ping = 999;
|
|
||||||
// calculate ping time
|
// copy to the current good spot
|
||||||
for ( i = 0 ; i < PACKET_BACKUP ; i++ ) {
|
cl.snap = newSnap;
|
||||||
packetNum = ( clc.netchan.outgoingSequence - 1 - i ) & PACKET_MASK;
|
cl.snap.ping = 999;
|
||||||
if ( cl.snap.ps.commandTime >= cl.outPackets[ packetNum ].p_serverTime ) {
|
// calculate ping time
|
||||||
cl.snap.ping = cls.realtime - cl.outPackets[ packetNum ].p_realtime;
|
for ( i = 0 ; i < PACKET_BACKUP ; i++ ) {
|
||||||
break;
|
packetNum = ( clc.netchan.outgoingSequence - 1 - i ) & PACKET_MASK;
|
||||||
|
if ( cl.snap.ps.commandTime >= cl.outPackets[ packetNum ].p_serverTime ) {
|
||||||
|
cl.snap.ping = cls.realtime - cl.outPackets[ packetNum ].p_realtime;
|
||||||
|
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,6 +1034,12 @@ 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,7 +1013,6 @@ 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 ( trap_Cvar_VariableIntegerValue( "sv_demoplaying" ) ) {
|
if ( g_svDemoPlaying.integer ) {
|
||||||
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 ( trap_Cvar_VariableIntegerValue( "sv_demoplaying" ) ) {
|
if ( g_svDemoPlaying.integer ) {
|
||||||
trap_SetConfigstring( CS_SVDEMO, "1" );
|
trap_SetConfigstring( CS_SVDEMO, "1" );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1761,6 +1761,17 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
@ -1781,6 +1792,27 @@ 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,6 +183,12 @@ 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,8 +997,6 @@
|
||||||
</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,12 +352,14 @@ 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,7 +1419,12 @@ 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 ) {
|
||||||
continue;
|
// 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
SV_ClientThink (cl, &cmds[ i ]);
|
SV_ClientThink (cl, &cmds[ i ]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -351,11 +351,17 @@ void SV_SpawnServer( char *server, qboolean killBots ) {
|
||||||
char systemInfo[16384];
|
char systemInfo[16384];
|
||||||
const char *p;
|
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() ) {
|
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();
|
||||||
|
|
@ -623,7 +629,6 @@ 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
|
||||||
|
|
@ -681,12 +686,13 @@ 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() ) {
|
if ( SVD_IsPlaying() && !SVD_IsStarting() ) {
|
||||||
SVD_StopPlay_f();
|
SVD_CleanupPlayback();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( svs.clients && !com_errorEntered ) {
|
if ( svs.clients && !com_errorEntered ) {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,9 @@ 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
|
||||||
|
|
@ -51,24 +53,19 @@ snapshot pipeline delivers them to a spectator client.
|
||||||
//
|
//
|
||||||
|
|
||||||
#define SVDEMO_MAGIC (('S') | ('V' << 8) | ('D' << 16) | ('M' << 24))
|
#define SVDEMO_MAGIC (('S') | ('V' << 8) | ('D' << 16) | ('M' << 24))
|
||||||
#define SVDEMO_VERSION 2 // v2: optional LZ4 compression
|
#define SVDEMO_VERSION 3 // v3: removed PVS data, svFlags only
|
||||||
#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 (entityState + PVS fields)
|
// per-entity data stored for delta compression
|
||||||
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;
|
||||||
|
|
||||||
|
|
@ -85,7 +82,6 @@ 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
|
||||||
|
|
@ -104,13 +100,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
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
|
@ -135,58 +131,6 @@ 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
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
|
@ -197,7 +141,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, demo.compressed ? SVDEMO_FLAG_COMPRESSED : 0 );
|
SVD_WriteInt( f, 0 ); // flags (reserved)
|
||||||
SVD_WriteInt( f, sv_maxclients->integer );
|
SVD_WriteInt( f, sv_maxclients->integer );
|
||||||
SVD_WriteInt( f, sv_fps->integer );
|
SVD_WriteInt( f, sv_fps->integer );
|
||||||
|
|
||||||
|
|
@ -275,38 +219,17 @@ static void SVD_WriteFrame( fileHandle_t f ) {
|
||||||
}
|
}
|
||||||
MSG_WriteDeltaEntity( &msg, from, &ent->s, qtrue );
|
MSG_WriteDeltaEntity( &msg, from, &ent->s, qtrue );
|
||||||
|
|
||||||
// write PVS fields only if changed from previous frame
|
// write svFlags only if changed (rarely changes)
|
||||||
{
|
if ( ent->r.svFlags != demo.prevEntities[i].svFlags ) {
|
||||||
qboolean pvsChanged = !demo.prevEntities[i].active
|
MSG_WriteBits( &msg, 1, 1 );
|
||||||
|| ent->r.svFlags != demo.prevEntities[i].svFlags
|
MSG_WriteLong( &msg, ent->r.svFlags );
|
||||||
|| ent->r.linked != demo.prevEntities[i].linked
|
} else {
|
||||||
|| !VectorCompare( ent->r.currentOrigin, demo.prevEntities[i].currentOrigin )
|
MSG_WriteBits( &msg, 0, 1 );
|
||||||
|| !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.linked );
|
|
||||||
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
|
||||||
|
|
@ -319,8 +242,9 @@ 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 (optionally LZ4 compressed)
|
// write entity message to file
|
||||||
SVD_WriteBlock( f, msg.data, msg.cursize );
|
SVD_WriteInt( f, msg.cursize );
|
||||||
|
FS_Write( msg.data, msg.cursize, f );
|
||||||
|
|
||||||
// write player states (delta compressed)
|
// write player states (delta compressed)
|
||||||
{
|
{
|
||||||
|
|
@ -371,7 +295,8 @@ 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_WriteBlock( f, psmsg.data, psmsg.cursize );
|
SVD_WriteInt( f, psmsg.cursize );
|
||||||
|
FS_Write( psmsg.data, psmsg.cursize, f );
|
||||||
}
|
}
|
||||||
|
|
||||||
// configstring changes
|
// configstring changes
|
||||||
|
|
@ -442,10 +367,8 @@ static qboolean SVD_StartRecording( const char *demoname ) {
|
||||||
return qfalse;
|
return qfalse;
|
||||||
}
|
}
|
||||||
|
|
||||||
Com_Printf( "Recording server demo to %s%s\n", path,
|
Com_Printf( "Recording server demo to %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) );
|
||||||
|
|
@ -592,10 +515,7 @@ 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 );
|
||||||
|
|
@ -686,11 +606,12 @@ static qboolean SVD_ReadFrame( fileHandle_t f ) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// read entity message (optionally LZ4 compressed)
|
// read entity message
|
||||||
blockLen = SVD_ReadBlock( f, msgBuf, sizeof(msgBuf) );
|
blockLen = SVD_ReadInt( f );
|
||||||
if ( blockLen <= 0 ) {
|
if ( blockLen <= 0 || blockLen > (int)sizeof(msgBuf) ) {
|
||||||
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;
|
||||||
|
|
||||||
|
|
@ -734,40 +655,20 @@ static qboolean SVD_ReadFrame( fileHandle_t f ) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// read PVS fields
|
// read svFlags
|
||||||
{
|
if ( MSG_ReadBits( &msg, 1 ) ) {
|
||||||
qboolean pvsChanged = MSG_ReadBits( &msg, 1 );
|
demo.playPrevEntities[entNum].svFlags = MSG_ReadLong( &msg );
|
||||||
if ( pvsChanged ) {
|
|
||||||
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
|
// apply to server entity (entity linking done in G_RunFrame
|
||||||
|
// 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;
|
||||||
|
|
@ -780,8 +681,9 @@ 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_ReadBlock( f, psBuf, sizeof(psBuf) );
|
psMsgLen = SVD_ReadInt( f );
|
||||||
if ( psMsgLen > 0 ) {
|
if ( psMsgLen > 0 && psMsgLen <= (int)sizeof(psBuf) ) {
|
||||||
|
FS_Read( psBuf, psMsgLen, f );
|
||||||
MSG_Init( &psmsg, psBuf, sizeof(psBuf) );
|
MSG_Init( &psmsg, psBuf, sizeof(psBuf) );
|
||||||
psmsg.cursize = psMsgLen;
|
psmsg.cursize = psMsgLen;
|
||||||
|
|
||||||
|
|
@ -871,8 +773,8 @@ void SVD_Play_f( void ) {
|
||||||
char *s;
|
char *s;
|
||||||
int len;
|
int len;
|
||||||
|
|
||||||
if ( demo.playing ) {
|
if ( demo.recording ) {
|
||||||
Com_Printf( "Already playing a server demo.\n" );
|
Com_Printf( "Stop recording first (svdemo_stop).\n" );
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -882,6 +784,11 @@ 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) );
|
||||||
|
|
@ -900,42 +807,46 @@ 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 );
|
||||||
|
|
||||||
// Signal demo mode BEFORE map load so the game module knows
|
// Shut down current server first so no clients carry over to
|
||||||
// during ClientConnect/ClientBegin to force spectator team.
|
// reserved slots. SV_Shutdown triggers our cleanup hook, but
|
||||||
Cvar_Set( "sv_demoplaying", "1" );
|
// 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_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;
|
||||||
|
|
||||||
// Reserve recorded player slots so the connecting spectator
|
// CS_SVDEMO configstring is set by G_InitGame from the cvar
|
||||||
// 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).
|
// Reserve recorded player slots. Server is fresh (SV_Shutdown cleared
|
||||||
// Set rate and nextSnapshotTime so SV_SendClientMessages doesn't
|
// old clients), local client hasn't connected yet.
|
||||||
// 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; // never send
|
svs.clients[i].nextSnapshotTime = 0x7FFFFFFF;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void SVD_CleanupPlayback( void ) {
|
void SVD_CleanupPlayback( void ) {
|
||||||
int i;
|
int i;
|
||||||
|
|
||||||
if ( !demo.playing ) {
|
if ( !demo.playing ) {
|
||||||
|
|
@ -959,7 +870,7 @@ static void SVD_CleanupPlayback( void ) {
|
||||||
}
|
}
|
||||||
|
|
||||||
memset( &demo, 0, sizeof(demo) );
|
memset( &demo, 0, sizeof(demo) );
|
||||||
Cvar_Set( "sv_demoplaying", "0" );
|
Cvar_Set2( "sv_demoplaying", "0", qtrue );
|
||||||
}
|
}
|
||||||
|
|
||||||
void SVD_StopPlay_f( void ) {
|
void SVD_StopPlay_f( void ) {
|
||||||
|
|
@ -994,6 +905,11 @@ 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" );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1053,6 +969,11 @@ 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