|
|
|
|
@ -90,9 +90,6 @@ typedef struct {
|
|
|
|
|
char serverCmds[SVD_MAX_SERVERCMDS][SVD_MAX_SERVERCMD_LEN];
|
|
|
|
|
int numServerCmds;
|
|
|
|
|
qboolean mapRestarted; // set by SVD_ResetDeltaState, written as frame flag
|
|
|
|
|
qboolean isKeyframe; // next frame is a keyframe (delta from baseline)
|
|
|
|
|
int keyframeInterval; // frames between keyframes (0 = disabled)
|
|
|
|
|
int framesSinceKeyframe; // counter for next keyframe
|
|
|
|
|
|
|
|
|
|
// playback
|
|
|
|
|
fileHandle_t playFile;
|
|
|
|
|
@ -105,15 +102,8 @@ typedef struct {
|
|
|
|
|
qboolean needConfigstrings; // apply saved configstrings on first frame
|
|
|
|
|
qboolean starting; // SVD_Play_f is running devmap internally
|
|
|
|
|
qboolean paused;
|
|
|
|
|
qboolean seeked; // just seeked, next frame needs RESET
|
|
|
|
|
svdEntityState_t playPrevEntities[MAX_GENTITIES]; // previous frame for delta read
|
|
|
|
|
svdPlayerState_t playPrevPlayers[MAX_CLIENTS]; // previous frame player states
|
|
|
|
|
|
|
|
|
|
// keyframe index (shared by recording and playback)
|
|
|
|
|
int numKeyframes;
|
|
|
|
|
int maxKeyframes; // allocated size
|
|
|
|
|
int *keyframeTimes; // serverTime of each keyframe
|
|
|
|
|
int *keyframeOffsets; // file offset of each keyframe
|
|
|
|
|
} svDemo_t;
|
|
|
|
|
|
|
|
|
|
static svDemo_t demo;
|
|
|
|
|
@ -199,17 +189,13 @@ static void SVD_WriteFrame( fileHandle_t f ) {
|
|
|
|
|
SVD_WriteInt( f, svs.time );
|
|
|
|
|
SVD_WriteShort( f, (short)sv.num_entities );
|
|
|
|
|
|
|
|
|
|
// frame flags: bit 0 = map restarted, bit 1 = keyframe
|
|
|
|
|
// frame flags: bit 0 = map restarted
|
|
|
|
|
{
|
|
|
|
|
byte frameFlags = 0;
|
|
|
|
|
if ( demo.mapRestarted ) {
|
|
|
|
|
frameFlags |= 1;
|
|
|
|
|
demo.mapRestarted = qfalse;
|
|
|
|
|
}
|
|
|
|
|
if ( demo.isKeyframe ) {
|
|
|
|
|
frameFlags |= 2;
|
|
|
|
|
demo.isKeyframe = qfalse;
|
|
|
|
|
}
|
|
|
|
|
FS_Write( &frameFlags, 1, f );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -388,19 +374,6 @@ static qboolean SVD_StartRecording( const char *demoname ) {
|
|
|
|
|
Com_Memset( demo.prevEntities, 0, sizeof(demo.prevEntities) );
|
|
|
|
|
Com_Memset( demo.prevPlayers, 0, sizeof(demo.prevPlayers) );
|
|
|
|
|
|
|
|
|
|
// keyframe interval from cvar (seconds to frames at sv_fps)
|
|
|
|
|
{
|
|
|
|
|
int secs = Cvar_VariableIntegerValue( "svdemo_keyframeInterval" );
|
|
|
|
|
if ( secs > 0 ) {
|
|
|
|
|
demo.keyframeInterval = secs * sv_fps->integer;
|
|
|
|
|
} else {
|
|
|
|
|
demo.keyframeInterval = 0;
|
|
|
|
|
}
|
|
|
|
|
// first frame is always a keyframe (makes beginning seekable)
|
|
|
|
|
demo.framesSinceKeyframe = demo.keyframeInterval;
|
|
|
|
|
demo.numKeyframes = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
SVD_WriteHeader( demo.recordFile );
|
|
|
|
|
return qtrue;
|
|
|
|
|
}
|
|
|
|
|
@ -459,21 +432,6 @@ void SVD_StopRecord_f( void ) {
|
|
|
|
|
// write end marker
|
|
|
|
|
SVD_WriteInt( demo.recordFile, -1 );
|
|
|
|
|
|
|
|
|
|
// write keyframe index after the end marker.
|
|
|
|
|
// layout: [numKf][time0 off0 time1 off1 ...][numKf_copy]
|
|
|
|
|
// numKf_copy at the very end lets playback find the table
|
|
|
|
|
// by seeking to fileLen - 4.
|
|
|
|
|
{
|
|
|
|
|
int kf;
|
|
|
|
|
SVD_WriteInt( demo.recordFile, demo.numKeyframes );
|
|
|
|
|
for ( kf = 0; kf < demo.numKeyframes; kf++ ) {
|
|
|
|
|
SVD_WriteInt( demo.recordFile, demo.keyframeTimes[kf] );
|
|
|
|
|
SVD_WriteInt( demo.recordFile, demo.keyframeOffsets[kf] );
|
|
|
|
|
}
|
|
|
|
|
SVD_WriteInt( demo.recordFile, demo.numKeyframes ); // copy at end
|
|
|
|
|
Com_Printf( "Wrote %d keyframes.\n", demo.numKeyframes );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
FS_FCloseFile( demo.recordFile );
|
|
|
|
|
demo.recordFile = 0;
|
|
|
|
|
demo.recording = qfalse;
|
|
|
|
|
@ -486,11 +444,6 @@ void SVD_StopRecord_f( void ) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// free keyframe index
|
|
|
|
|
if ( demo.keyframeTimes ) { Z_Free( demo.keyframeTimes ); demo.keyframeTimes = NULL; }
|
|
|
|
|
if ( demo.keyframeOffsets ) { Z_Free( demo.keyframeOffsets ); demo.keyframeOffsets = NULL; }
|
|
|
|
|
demo.numKeyframes = demo.maxKeyframes = 0;
|
|
|
|
|
|
|
|
|
|
Com_Printf( "Server demo recording stopped.\n" );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -540,37 +493,6 @@ void SVD_RecordFrame( void ) {
|
|
|
|
|
if ( !demo.recording ) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// periodic keyframe: reset delta state so this frame is decodable
|
|
|
|
|
// from baseline. record file offset for the keyframe index.
|
|
|
|
|
if ( demo.keyframeInterval > 0 ) {
|
|
|
|
|
demo.framesSinceKeyframe++;
|
|
|
|
|
if ( demo.framesSinceKeyframe >= demo.keyframeInterval ) {
|
|
|
|
|
demo.framesSinceKeyframe = 0;
|
|
|
|
|
demo.isKeyframe = qtrue;
|
|
|
|
|
Com_Memset( demo.prevEntities, 0, sizeof(demo.prevEntities) );
|
|
|
|
|
Com_Memset( demo.prevPlayers, 0, sizeof(demo.prevPlayers) );
|
|
|
|
|
// store keyframe: file offset before writing, serverTime
|
|
|
|
|
if ( demo.numKeyframes >= demo.maxKeyframes ) {
|
|
|
|
|
int newMax = demo.maxKeyframes ? demo.maxKeyframes * 2 : 256;
|
|
|
|
|
int *newTimes = Z_Malloc( newMax * sizeof(int) );
|
|
|
|
|
int *newOffsets = Z_Malloc( newMax * sizeof(int) );
|
|
|
|
|
if ( demo.keyframeTimes ) {
|
|
|
|
|
Com_Memcpy( newTimes, demo.keyframeTimes, demo.numKeyframes * sizeof(int) );
|
|
|
|
|
Com_Memcpy( newOffsets, demo.keyframeOffsets, demo.numKeyframes * sizeof(int) );
|
|
|
|
|
Z_Free( demo.keyframeTimes );
|
|
|
|
|
Z_Free( demo.keyframeOffsets );
|
|
|
|
|
}
|
|
|
|
|
demo.keyframeTimes = newTimes;
|
|
|
|
|
demo.keyframeOffsets = newOffsets;
|
|
|
|
|
demo.maxKeyframes = newMax;
|
|
|
|
|
}
|
|
|
|
|
demo.keyframeTimes[demo.numKeyframes] = svs.time;
|
|
|
|
|
demo.keyframeOffsets[demo.numKeyframes] = FS_FTell( demo.recordFile );
|
|
|
|
|
demo.numKeyframes++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
SVD_WriteFrame( demo.recordFile );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -672,17 +594,15 @@ static qboolean SVD_ReadFrame( fileHandle_t f ) {
|
|
|
|
|
{
|
|
|
|
|
byte frameFlags;
|
|
|
|
|
FS_Read( &frameFlags, 1, f );
|
|
|
|
|
if ( frameFlags & 3 ) {
|
|
|
|
|
// bit 0 = map restart, bit 1 = keyframe.
|
|
|
|
|
// both mean: delta state was reset during recording,
|
|
|
|
|
// so reset playback delta state to decode from baseline.
|
|
|
|
|
if ( frameFlags & 1 ) {
|
|
|
|
|
// map was restarted — reset playback delta state to match
|
|
|
|
|
// the recording's reset (both now decode from zero baseline).
|
|
|
|
|
Com_Memset( demo.playPrevEntities, 0, sizeof(demo.playPrevEntities) );
|
|
|
|
|
Com_Memset( demo.playPrevPlayers, 0, sizeof(demo.playPrevPlayers) );
|
|
|
|
|
}
|
|
|
|
|
if ( ( frameFlags & 1 ) || demo.seeked ) {
|
|
|
|
|
// map restart or seek: reset entity interpolation in cgame
|
|
|
|
|
// set one-shot SNAPFLAG_RESET_ENTITIES so cgame snaps all
|
|
|
|
|
// entities to current position without interpolation.
|
|
|
|
|
// OR'd into snapFlagServerBit, cleared after one frame.
|
|
|
|
|
svs.snapFlagServerBit |= SNAPFLAG_RESET_ENTITIES;
|
|
|
|
|
demo.seeked = qfalse;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -889,37 +809,6 @@ void SVD_Play_f( void ) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// read keyframe index from the end of the file.
|
|
|
|
|
// layout: [frames][-1][numKf][time0 off0 ...][numKf_copy]
|
|
|
|
|
// last 4 bytes of file = numKf_copy.
|
|
|
|
|
{
|
|
|
|
|
int frameStart = FS_FTell( demo.playFile );
|
|
|
|
|
int numKf, kf;
|
|
|
|
|
|
|
|
|
|
FS_Seek( demo.playFile, len - 4, FS_SEEK_SET );
|
|
|
|
|
numKf = SVD_ReadInt( demo.playFile );
|
|
|
|
|
|
|
|
|
|
if ( numKf > 0 && numKf < 1000000 ) {
|
|
|
|
|
// seek to start of keyframe table: end - 4 - numKf*8 - 4
|
|
|
|
|
int tableStart = len - 4 - numKf * 8 - 4;
|
|
|
|
|
FS_Seek( demo.playFile, tableStart + 4, FS_SEEK_SET ); // skip numKf
|
|
|
|
|
|
|
|
|
|
demo.numKeyframes = numKf;
|
|
|
|
|
demo.maxKeyframes = numKf;
|
|
|
|
|
demo.keyframeTimes = Z_Malloc( numKf * sizeof(int) );
|
|
|
|
|
demo.keyframeOffsets = Z_Malloc( numKf * sizeof(int) );
|
|
|
|
|
|
|
|
|
|
for ( kf = 0; kf < numKf; kf++ ) {
|
|
|
|
|
demo.keyframeTimes[kf] = SVD_ReadInt( demo.playFile );
|
|
|
|
|
demo.keyframeOffsets[kf] = SVD_ReadInt( demo.playFile );
|
|
|
|
|
}
|
|
|
|
|
Com_Printf( "Loaded %d keyframes.\n", numKf );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// seek back to start of frame data
|
|
|
|
|
FS_Seek( demo.playFile, frameStart, FS_SEEK_SET );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
demo.playing = qtrue;
|
|
|
|
|
demo.endOfDemo = qfalse;
|
|
|
|
|
demo.needConfigstrings = qtrue;
|
|
|
|
|
@ -984,10 +873,6 @@ void SVD_CleanupPlayback( void ) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// free keyframe index
|
|
|
|
|
if ( demo.keyframeTimes ) { Z_Free( demo.keyframeTimes ); }
|
|
|
|
|
if ( demo.keyframeOffsets ) { Z_Free( demo.keyframeOffsets ); }
|
|
|
|
|
|
|
|
|
|
memset( &demo, 0, sizeof(demo) );
|
|
|
|
|
Cvar_Set2( "sv_demoplaying", "0", qtrue );
|
|
|
|
|
}
|
|
|
|
|
@ -1027,158 +912,14 @@ void SVD_Pause_f( void ) {
|
|
|
|
|
if ( !demo.paused ) {
|
|
|
|
|
// resuming — toggle SERVERCOUNT to reset client snapshot timing
|
|
|
|
|
// (drifted during pause from identical serverTimes).
|
|
|
|
|
// also reset entities so fast-moving objects (rockets) snap to
|
|
|
|
|
// position instead of interpolating through the pause gap.
|
|
|
|
|
svs.snapFlagServerBit ^= SNAPFLAG_SERVERCOUNT;
|
|
|
|
|
svs.snapFlagServerBit |= SNAPFLAG_RESET_ENTITIES;
|
|
|
|
|
}
|
|
|
|
|
Com_Printf( "Demo playback %s.\n", demo.paused ? "paused" : "resumed" );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SVD_Seek_f( void ) {
|
|
|
|
|
int targetTime, i, bestKf;
|
|
|
|
|
float seconds;
|
|
|
|
|
|
|
|
|
|
if ( !demo.playing ) {
|
|
|
|
|
Com_Printf( "Not playing a server demo.\n" );
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( Cmd_Argc() < 2 ) {
|
|
|
|
|
Com_Printf( "Usage: svdemo_seek <seconds>\n" );
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( demo.numKeyframes <= 0 ) {
|
|
|
|
|
Com_Printf( "No keyframes in this demo — seeking not available.\n" );
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
seconds = atof( Cmd_Argv(1) );
|
|
|
|
|
targetTime = svs.time + (int)(seconds * 1000);
|
|
|
|
|
|
|
|
|
|
// find nearest keyframe at or before target time
|
|
|
|
|
bestKf = -1;
|
|
|
|
|
for ( i = 0; i < demo.numKeyframes; i++ ) {
|
|
|
|
|
if ( demo.keyframeTimes[i] <= targetTime ) {
|
|
|
|
|
bestKf = i;
|
|
|
|
|
} else {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( bestKf < 0 ) {
|
|
|
|
|
// target is before the first keyframe — seek to first
|
|
|
|
|
bestKf = 0;
|
|
|
|
|
targetTime = demo.keyframeTimes[0];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// seek to keyframe file position
|
|
|
|
|
FS_Seek( demo.playFile, demo.keyframeOffsets[bestKf], FS_SEEK_SET );
|
|
|
|
|
|
|
|
|
|
// reset delta state (keyframe is encoded from baseline)
|
|
|
|
|
Com_Memset( demo.playPrevEntities, 0, sizeof(demo.playPrevEntities) );
|
|
|
|
|
Com_Memset( demo.playPrevPlayers, 0, sizeof(demo.playPrevPlayers) );
|
|
|
|
|
|
|
|
|
|
// set svs.time to the keyframe time so the SV_Frame loop
|
|
|
|
|
// doesn't advance from the old time before reading
|
|
|
|
|
svs.time = demo.keyframeTimes[bestKf];
|
|
|
|
|
|
|
|
|
|
// toggle SERVERCOUNT to reset client time delta
|
|
|
|
|
svs.snapFlagServerBit ^= SNAPFLAG_SERVERCOUNT;
|
|
|
|
|
|
|
|
|
|
demo.seeked = qtrue;
|
|
|
|
|
demo.endOfDemo = qfalse;
|
|
|
|
|
|
|
|
|
|
// read the keyframe directly (works even when paused)
|
|
|
|
|
svs.snapFlagServerBit &= ~SNAPFLAG_RESET_ENTITIES;
|
|
|
|
|
if ( !SVD_ReadFrame( demo.playFile ) ) {
|
|
|
|
|
demo.endOfDemo = qtrue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// reset client snapshot timing
|
|
|
|
|
{
|
|
|
|
|
int j;
|
|
|
|
|
for ( j = 0; j < sv_maxclients->integer; j++ ) {
|
|
|
|
|
if ( svs.clients[j].state >= CS_ACTIVE ) {
|
|
|
|
|
svs.clients[j].nextSnapshotTime = svs.time;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ensure one frame runs on next SV_Frame (for G_RunFrame + snapshot)
|
|
|
|
|
sv.timeResidual = 1000 / sv_fps->integer;
|
|
|
|
|
|
|
|
|
|
Com_Printf( "Seeked to time %d.\n", svs.time );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SVD_SeekExact_f( void ) {
|
|
|
|
|
int targetTime, i, bestKf;
|
|
|
|
|
float seconds;
|
|
|
|
|
|
|
|
|
|
if ( !demo.playing ) {
|
|
|
|
|
Com_Printf( "Not playing a server demo.\n" );
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( Cmd_Argc() < 2 ) {
|
|
|
|
|
Com_Printf( "Usage: svdemo_seekexact <seconds>\n" );
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( demo.numKeyframes <= 0 ) {
|
|
|
|
|
Com_Printf( "No keyframes in this demo.\n" );
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
seconds = atof( Cmd_Argv(1) );
|
|
|
|
|
targetTime = svs.time + (int)(seconds * 1000);
|
|
|
|
|
|
|
|
|
|
// find nearest keyframe at or before target time
|
|
|
|
|
bestKf = -1;
|
|
|
|
|
for ( i = 0; i < demo.numKeyframes; i++ ) {
|
|
|
|
|
if ( demo.keyframeTimes[i] <= targetTime ) {
|
|
|
|
|
bestKf = i;
|
|
|
|
|
} else {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( bestKf < 0 ) {
|
|
|
|
|
bestKf = 0;
|
|
|
|
|
targetTime = demo.keyframeTimes[0];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// seek to keyframe
|
|
|
|
|
FS_Seek( demo.playFile, demo.keyframeOffsets[bestKf], FS_SEEK_SET );
|
|
|
|
|
Com_Memset( demo.playPrevEntities, 0, sizeof(demo.playPrevEntities) );
|
|
|
|
|
Com_Memset( demo.playPrevPlayers, 0, sizeof(demo.playPrevPlayers) );
|
|
|
|
|
svs.time = demo.keyframeTimes[bestKf];
|
|
|
|
|
svs.snapFlagServerBit ^= SNAPFLAG_SERVERCOUNT;
|
|
|
|
|
demo.seeked = qtrue;
|
|
|
|
|
demo.endOfDemo = qfalse;
|
|
|
|
|
|
|
|
|
|
// read forward from keyframe to target time
|
|
|
|
|
while ( svs.time < targetTime ) {
|
|
|
|
|
if ( !SVD_ReadFrame( demo.playFile ) ) {
|
|
|
|
|
demo.endOfDemo = qtrue;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// reset client snapshot timing
|
|
|
|
|
{
|
|
|
|
|
int j;
|
|
|
|
|
for ( j = 0; j < sv_maxclients->integer; j++ ) {
|
|
|
|
|
if ( svs.clients[j].state >= CS_ACTIVE ) {
|
|
|
|
|
svs.clients[j].nextSnapshotTime = svs.time;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sv.timeResidual = 1000 / sv_fps->integer;
|
|
|
|
|
|
|
|
|
|
Com_Printf( "Seeked to time %d (read forward %d ms from keyframe).\n",
|
|
|
|
|
svs.time, svs.time - demo.keyframeTimes[bestKf] );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
Called from SV_Frame() to advance playback by one frame.
|
|
|
|
|
Returns qtrue if a frame was read, qfalse if demo ended.
|
|
|
|
|
@ -1188,7 +929,6 @@ qboolean SVD_PlaybackFrame( void ) {
|
|
|
|
|
return qfalse;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// manual pause — don't consume demo data
|
|
|
|
|
if ( demo.paused ) {
|
|
|
|
|
return qfalse;
|
|
|
|
|
|