xash3d-fwgs/engine/client/cl_demo.c
Gleb Mazovetskiy 5e0a0765ce Trim all trailing whitespace
The `.editorconfig` file in this repo is configured to trim all trailing
whitespace regardless of whether the line is modified.

Trims all trailing whitespace in the repository to make the codebase easier
to work with in editors that respect `.editorconfig`.

`git blame` becomes less useful on these lines but it already isn't very useful.

Commands:

```
find . -type f -name '*.h' -exec sed --in-place 's/[[:space:]]\+$//' {} \+
find . -type f -name '*.c' -exec sed --in-place 's/[[:space:]]\+$//' {} \+
```
2021-01-04 20:55:10 +03:00

1623 lines
37 KiB
C

/*
cl_demo.c - demo record & playback
Copyright (C) 2007 Uncle Mike
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
*/
#include "common.h"
#include "client.h"
#include "net_encode.h"
#define dem_unknown 0 // unknown command
#define dem_norewind 1 // startup message
#define dem_read 2 // it's a normal network packet
#define dem_jumptime 3 // move the demostart time value forward by this amount
#define dem_userdata 4 // userdata from the client.dll
#define dem_usercmd 5 // read usercmd_t
#define dem_stop 6 // end of time
#define dem_lastcmd dem_stop
#define DEMO_STARTUP 0 // this lump contains startup info needed to spawn into the server
#define DEMO_NORMAL 1 // this lump contains playback info of messages, etc., needed during playback.
// Demo flags
#define FDEMO_TITLE 0x01 // Show title
#define FDEMO_PLAY 0x04 // Playing cd track
#define FDEMO_FADE_IN_SLOW 0x08 // Fade in (slow)
#define FDEMO_FADE_IN_FAST 0x10 // Fade in (fast)
#define FDEMO_FADE_OUT_SLOW 0x20 // Fade out (slow)
#define FDEMO_FADE_OUT_FAST 0x40 // Fade out (fast)
#define IDEMOHEADER (('M'<<24)+('E'<<16)+('D'<<8)+'I') // little-endian "IDEM"
#define DEMO_PROTOCOL 3
const char *demo_cmd[dem_lastcmd+1] =
{
"dem_unknown",
"dem_norewind",
"dem_read",
"dem_jumptime",
"dem_userdata",
"dem_usercmd",
"dem_stop",
};
#pragma pack( push, 1 )
typedef struct
{
int id; // should be IDEM
int dem_protocol; // should be DEMO_PROTOCOL
int net_protocol; // should be PROTOCOL_VERSION
double host_fps; // fps for demo playing
char mapname[64]; // name of map
char comment[64]; // comment for demo
char gamedir[64]; // name of game directory (FS_Gamedir())
int directory_offset; // offset of Entry Directory.
} demoheader_t;
#pragma pack( pop )
typedef struct
{
int entrytype; // DEMO_STARTUP or DEMO_NORMAL
float playback_time; // time of track
int playback_frames; // # of frames in track
int offset; // file offset of track data
int length; // length of track
int flags; // FX-flags
char description[64]; // entry description
} demoentry_t;
typedef struct
{
demoentry_t *entries; // track entry info
int numentries; // number of tracks
} demodirectory_t;
// add angles
typedef struct
{
float starttime;
vec3_t viewangles;
} demoangle_t;
// private demo states
struct
{
demoheader_t header;
demoentry_t *entry;
demodirectory_t directory;
int framecount;
float starttime;
float realstarttime;
float timestamp;
float lasttime;
int entryIndex;
// interpolation stuff
demoangle_t cmds[ANGLE_BACKUP];
int angle_position;
} demo;
/*
====================
CL_StartupDemoHeader
spooling demo header in case
we record a demo on this level
====================
*/
void CL_StartupDemoHeader( void )
{
if( cls.demoheader )
{
FS_Close( cls.demoheader );
}
// Note: this is replacing tmpfile()
cls.demoheader = FS_Open( "demoheader.tmp", "w+b", true );
if( !cls.demoheader )
{
Con_DPrintf( S_ERROR "couldn't open temporary header file.\n" );
return;
}
Con_Printf( "Spooling demo header.\n" );
}
/*
====================
CL_CloseDemoHeader
close demoheader file on engine shutdown
====================
*/
void CL_CloseDemoHeader( void )
{
if( !cls.demoheader )
return;
FS_Close( cls.demoheader );
}
/*
====================
CL_GetDemoRecordClock
write time while demo is recording
====================
*/
float CL_GetDemoRecordClock( void )
{
return cl.mtime[0];
}
/*
====================
CL_GetDemoPlaybackClock
overwrite host.realtime
====================
*/
float CL_GetDemoPlaybackClock( void )
{
return host.realtime + host.frametime;
}
/*
====================
CL_GetDemoFramerate
overwrite host.frametime
====================
*/
double CL_GetDemoFramerate( void )
{
if( cls.timedemo )
return 0.0;
return bound( MIN_FPS, demo.header.host_fps, MAX_FPS );
}
/*
====================
CL_WriteDemoCmdHeader
Writes the demo command header and time-delta
====================
*/
void CL_WriteDemoCmdHeader( byte cmd, file_t *file )
{
float dt;
Assert( cmd >= 1 && cmd <= dem_lastcmd );
if( !file ) return;
// command
FS_Write( file, &cmd, sizeof( byte ));
// time offset
dt = (float)(CL_GetDemoRecordClock() - demo.starttime);
FS_Write( file, &dt, sizeof( float ));
}
/*
====================
CL_WriteDemoJumpTime
Update level time on a next level
====================
*/
void CL_WriteDemoJumpTime( void )
{
if( cls.demowaiting || !cls.demofile )
return;
demo.starttime = CL_GetDemoRecordClock(); // setup the demo starttime
// demo playback should read this as an incoming message.
// write the client's realtime value out so we can synchronize the reads.
CL_WriteDemoCmdHeader( dem_jumptime, cls.demofile );
}
/*
====================
CL_WriteDemoUserCmd
Writes the current user cmd
====================
*/
void CL_WriteDemoUserCmd( int cmdnumber )
{
sizebuf_t buf;
word bytes;
byte data[1024];
if( !cls.demorecording || !cls.demofile )
return;
CL_WriteDemoCmdHeader( dem_usercmd, cls.demofile );
FS_Write( cls.demofile, &cls.netchan.outgoing_sequence, sizeof( int ));
FS_Write( cls.demofile, &cmdnumber, sizeof( int ));
// write usercmd_t
MSG_Init( &buf, "UserCmd", data, sizeof( data ));
CL_WriteUsercmd( &buf, -1, cmdnumber ); // always no delta
bytes = MSG_GetNumBytesWritten( &buf );
FS_Write( cls.demofile, &bytes, sizeof( word ));
FS_Write( cls.demofile, data, bytes );
}
/*
====================
CL_WriteDemoSequence
Save state of cls.netchan sequences
so that we can play the demo correctly.
====================
*/
void CL_WriteDemoSequence( file_t *file )
{
Assert( file != NULL );
FS_Write( file, &cls.netchan.incoming_sequence, sizeof( int ));
FS_Write( file, &cls.netchan.incoming_acknowledged, sizeof( int ));
FS_Write( file, &cls.netchan.incoming_reliable_acknowledged, sizeof( int ));
FS_Write( file, &cls.netchan.incoming_reliable_sequence, sizeof( int ));
FS_Write( file, &cls.netchan.outgoing_sequence, sizeof( int ));
FS_Write( file, &cls.netchan.reliable_sequence, sizeof( int ));
FS_Write( file, &cls.netchan.last_reliable_sequence, sizeof( int ));
}
/*
====================
CL_WriteDemoMessage
Dumps the current net message, prefixed by the length
====================
*/
void CL_WriteDemoMessage( qboolean startup, int start, sizebuf_t *msg )
{
file_t *file = startup ? cls.demoheader : cls.demofile;
int swlen;
byte c;
if( !file ) return;
// past the start but not recording a demo.
if( !startup && !cls.demorecording )
return;
swlen = MSG_GetNumBytesWritten( msg ) - start;
if( swlen <= 0 ) return;
if( !startup ) demo.framecount++;
// demo playback should read this as an incoming message.
c = (cls.state != ca_active) ? dem_norewind : dem_read;
CL_WriteDemoCmdHeader( c, file );
CL_WriteDemoSequence( file );
// write the length out.
FS_Write( file, &swlen, sizeof( int ));
// output the buffer. Skip the network packet stuff.
FS_Write( file, MSG_GetData( msg ) + start, swlen );
}
/*
====================
CL_WriteDemoUserMessage
Dumps the user message (demoaction)
====================
*/
void CL_WriteDemoUserMessage( const byte *buffer, size_t size )
{
if( !cls.demorecording || cls.demowaiting )
return;
if( !cls.demofile || !buffer || size <= 0 )
return;
CL_WriteDemoCmdHeader( dem_userdata, cls.demofile );
// write the length out.
FS_Write( cls.demofile, &size, sizeof( int ));
// output the buffer.
FS_Write( cls.demofile, buffer, size );
}
/*
====================
CL_WriteDemoHeader
Write demo header
====================
*/
void CL_WriteDemoHeader( const char *name )
{
int copysize;
int savepos;
int curpos;
Con_Printf( "recording to %s.\n", name );
cls.demofile = FS_Open( name, "wb", false );
cls.demotime = 0.0;
if( !cls.demofile )
{
Con_Printf( S_ERROR "couldn't open %s.\n", name );
return;
}
cls.demorecording = true;
cls.demowaiting = true; // don't start saving messages until a non-delta compressed message is received
memset( &demo.header, 0, sizeof( demo.header ));
demo.header.id = IDEMOHEADER;
demo.header.dem_protocol = DEMO_PROTOCOL;
demo.header.net_protocol = PROTOCOL_VERSION;
demo.header.host_fps = bound( MIN_FPS, host_maxfps->value, MAX_FPS );
Q_strncpy( demo.header.mapname, clgame.mapname, sizeof( demo.header.mapname ));
Q_strncpy( demo.header.comment, clgame.maptitle, sizeof( demo.header.comment ));
Q_strncpy( demo.header.gamedir, FS_Gamedir(), sizeof( demo.header.gamedir ));
// write header
FS_Write( cls.demofile, &demo.header, sizeof( demo.header ));
demo.directory.numentries = 2;
demo.directory.entries = Mem_Calloc( cls.mempool, sizeof( demoentry_t ) * demo.directory.numentries );
// DIRECTORY ENTRY # 0
demo.entry = &demo.directory.entries[0]; // only one here.
demo.entry->entrytype = DEMO_STARTUP;
demo.entry->playback_time = 0.0f; // startup takes 0 time.
demo.entry->offset = FS_Tell( cls.demofile ); // position for this chunk.
// finish off the startup info.
CL_WriteDemoCmdHeader( dem_stop, cls.demoheader );
// now copy the stuff we cached from the server.
copysize = savepos = FS_Tell( cls.demoheader );
FS_Seek( cls.demoheader, 0, SEEK_SET );
FS_FileCopy( cls.demofile, cls.demoheader, copysize );
// jump back to end, in case we record another demo for this session.
FS_Seek( cls.demoheader, savepos, SEEK_SET );
demo.starttime = CL_GetDemoRecordClock(); // setup the demo starttime
demo.realstarttime = demo.starttime;
demo.framecount = 0;
cls.td_startframe = host.framecount;
cls.td_lastframe = -1; // get a new message this frame
// now move on to entry # 1, the first data chunk.
curpos = FS_Tell( cls.demofile );
demo.entry->length = curpos - demo.entry->offset;
// now we are writing the first real lump.
demo.entry = &demo.directory.entries[1]; // first real data lump
demo.entry->entrytype = DEMO_NORMAL;
demo.entry->playback_time = 0.0f; // startup takes 0 time.
demo.entry->offset = FS_Tell( cls.demofile );
// demo playback should read this as an incoming message.
// write the client's realtime value out so we can synchronize the reads.
CL_WriteDemoCmdHeader( dem_jumptime, cls.demofile );
if( clgame.hInstance ) clgame.dllFuncs.pfnReset();
Cbuf_InsertText( "fullupdate\n" );
Cbuf_Execute();
}
/*
=================
CL_StopRecord
finish recording demo
=================
*/
void CL_StopRecord( void )
{
int i, curpos;
float stoptime;
int frames;
if( !cls.demorecording ) return;
// demo playback should read this as an incoming message.
CL_WriteDemoCmdHeader( dem_stop, cls.demofile );
stoptime = CL_GetDemoRecordClock();
if( clgame.hInstance ) clgame.dllFuncs.pfnReset();
curpos = FS_Tell( cls.demofile );
demo.entry->length = curpos - demo.entry->offset;
demo.entry->playback_time = stoptime - demo.realstarttime;
demo.entry->playback_frames = demo.framecount;
// Now write out the directory and free it and touch up the demo header.
FS_Write( cls.demofile, &demo.directory.numentries, sizeof( int ));
for( i = 0; i < demo.directory.numentries; i++ )
FS_Write( cls.demofile, &demo.directory.entries[i], sizeof( demoentry_t ));
Mem_Free( demo.directory.entries );
demo.directory.numentries = 0;
demo.header.directory_offset = curpos;
FS_Seek( cls.demofile, 0, SEEK_SET );
FS_Write( cls.demofile, &demo.header, sizeof( demo.header ));
FS_Close( cls.demofile );
cls.demofile = NULL;
cls.demorecording = false;
cls.demoname[0] = '\0';
cls.td_lastframe = host.framecount;
gameui.globals->demoname[0] = '\0';
demo.header.host_fps = 0.0;
frames = cls.td_lastframe - cls.td_startframe;
Con_Printf( "Completed demo\nRecording time: %02d:%02d, frames %i\n", (int)(cls.demotime / 60.0f), (int)fmod(cls.demotime, 60.0f), frames );
cls.demotime = 0.0;
}
/*
=================
CL_DrawDemoRecording
=================
*/
void CL_DrawDemoRecording( void )
{
char string[64];
rgba_t color = { 255, 255, 255, 255 };
int pos;
int len;
if(!( host_developer.value && cls.demorecording ))
return;
pos = FS_Tell( cls.demofile );
Q_snprintf( string, sizeof( string ), "^1RECORDING:^7 %s: %s time: %02d:%02d", cls.demoname,
Q_memprint( pos ), (int)(cls.demotime / 60.0f ), (int)fmod( cls.demotime, 60.0f ));
Con_DrawStringLen( string, &len, NULL );
Con_DrawString(( refState.width - len ) >> 1, refState.height >> 4, string, color );
}
/*
=======================================================================
CLIENT SIDE DEMO PLAYBACK
=======================================================================
*/
/*
=================
CL_ReadDemoCmdHeader
read the demo command
=================
*/
void CL_ReadDemoCmdHeader( byte *cmd, float *dt )
{
// read the command
FS_Read( cls.demofile, cmd, sizeof( byte ));
Assert( *cmd >= 1 && *cmd <= dem_lastcmd );
// read the timestamp
FS_Read( cls.demofile, dt, sizeof( float ));
}
/*
=================
CL_ReadDemoUserCmd
read the demo usercmd for predicting
and smooth movement during playback the demo
=================
*/
void CL_ReadDemoUserCmd( qboolean discard )
{
byte data[1024];
int cmdnumber;
int outgoing_sequence;
runcmd_t *pcmd;
word bytes;
FS_Read( cls.demofile, &outgoing_sequence, sizeof( int ));
FS_Read( cls.demofile, &cmdnumber, sizeof( int ));
FS_Read( cls.demofile, &bytes, sizeof( short ));
FS_Read( cls.demofile, data, bytes );
if( !discard )
{
usercmd_t nullcmd;
sizebuf_t buf;
demoangle_t *a;
memset( &nullcmd, 0, sizeof( nullcmd ));
MSG_Init( &buf, "UserCmd", data, sizeof( data ));
pcmd = &cl.commands[cmdnumber & CL_UPDATE_MASK];
pcmd->processedfuncs = false;
pcmd->senttime = 0.0f;
pcmd->receivedtime = 0.1f;
pcmd->frame_lerp = 0.1f;
pcmd->heldback = false;
pcmd->sendsize = 1;
// always delta'ing from null
cl.cmd = &pcmd->cmd;
MSG_ReadDeltaUsercmd( &buf, &nullcmd, cl.cmd );
// make sure what interp info contain angles from different frames
// or lerping will stop working
if( demo.lasttime != demo.timestamp )
{
// select entry into circular buffer
demo.angle_position = (demo.angle_position + 1) & ANGLE_MASK;
a = &demo.cmds[demo.angle_position];
// record update
a->starttime = demo.timestamp;
VectorCopy( cl.cmd->viewangles, a->viewangles );
demo.lasttime = demo.timestamp;
}
// NOTE: we need to have the current outgoing sequence correct
// so we can do prediction correctly during playback
cls.netchan.outgoing_sequence = outgoing_sequence;
}
}
/*
=================
CL_ReadDemoSequence
read netchan sequences
=================
*/
void CL_ReadDemoSequence( qboolean discard )
{
int incoming_sequence;
int incoming_acknowledged;
int incoming_reliable_acknowledged;
int incoming_reliable_sequence;
int outgoing_sequence;
int reliable_sequence;
int last_reliable_sequence;
FS_Read( cls.demofile, &incoming_sequence, sizeof( int ));
FS_Read( cls.demofile, &incoming_acknowledged, sizeof( int ));
FS_Read( cls.demofile, &incoming_reliable_acknowledged, sizeof( int ));
FS_Read( cls.demofile, &incoming_reliable_sequence, sizeof( int ));
FS_Read( cls.demofile, &outgoing_sequence, sizeof( int ));
FS_Read( cls.demofile, &reliable_sequence, sizeof( int ));
FS_Read( cls.demofile, &last_reliable_sequence, sizeof( int ));
if( discard ) return;
cls.netchan.incoming_sequence = incoming_sequence;
cls.netchan.incoming_acknowledged = incoming_acknowledged;
cls.netchan.incoming_reliable_acknowledged = incoming_reliable_acknowledged;
cls.netchan.incoming_reliable_sequence = incoming_reliable_sequence;
cls.netchan.outgoing_sequence = outgoing_sequence;
cls.netchan.reliable_sequence = reliable_sequence;
cls.netchan.last_reliable_sequence = last_reliable_sequence;
}
/*
=================
CL_DemoStartPlayback
=================
*/
void CL_DemoStartPlayback( int mode )
{
if( cls.changedemo )
{
S_StopAllSounds( true );
SCR_BeginLoadingPlaque( false );
CL_ClearState ();
CL_InitEdicts (); // re-arrange edicts
}
else
{
// NOTE: at this point demo is still valid
CL_Disconnect();
Host_ShutdownServer();
Con_FastClose();
UI_SetActiveMenu( false );
}
cls.demoplayback = mode;
cls.state = ca_connected;
cl.background = (cls.demonum != -1) ? true : false;
cls.spectator = false;
cls.signon = 0;
demo.starttime = CL_GetDemoPlaybackClock(); // for determining whether to read another message
Netchan_Setup( NS_CLIENT, &cls.netchan, net_from, Cvar_VariableInteger( "net_qport" ), NULL, CL_GetFragmentSize );
memset( demo.cmds, 0, sizeof( demo.cmds ));
demo.angle_position = 1;
demo.framecount = 0;
cls.lastoutgoingcommand = -1;
cls.nextcmdtime = host.realtime;
cl.last_command_ack = -1;
}
/*
=================
CL_DemoAborted
=================
*/
void CL_DemoAborted( void )
{
if( cls.demofile )
FS_Close( cls.demofile );
cls.demoplayback = false;
cls.changedemo = false;
cls.timedemo = false;
demo.framecount = 0;
cls.demofile = NULL;
cls.demonum = -1;
Cvar_SetValue( "v_dark", 0.0f );
}
/*
=================
CL_DemoCompleted
=================
*/
void CL_DemoCompleted( void )
{
if( cls.demonum != -1 )
cls.changedemo = true;
CL_StopPlayback();
if( !CL_NextDemo() && !cls.changedemo )
UI_SetActiveMenu( true );
Cvar_SetValue( "v_dark", 0.0f );
}
/*
=================
CL_DemoMoveToNextSection
returns true on success, false on failure
g-cont. probably captain obvious mode is ON
=================
*/
qboolean CL_DemoMoveToNextSection( void )
{
if( ++demo.entryIndex >= demo.directory.numentries )
{
// done
CL_DemoCompleted();
return false;
}
// switch to next section, we got a dem_stop
demo.entry = &demo.directory.entries[demo.entryIndex];
// ready to continue reading, reset clock.
FS_Seek( cls.demofile, demo.entry->offset, SEEK_SET );
// time is now relative to this chunk's clock.
demo.starttime = CL_GetDemoPlaybackClock();
demo.framecount = 0;
return true;
}
qboolean CL_ReadRawNetworkData( byte *buffer, size_t *length )
{
int msglen = 0;
Assert( buffer != NULL );
Assert( length != NULL );
*length = 0; // assume we fail
FS_Read( cls.demofile, &msglen, sizeof( int ));
if( msglen < 0 )
{
Con_Reportf( S_ERROR "Demo message length < 0\n" );
CL_DemoCompleted();
return false;
}
if( msglen > MAX_INIT_MSG )
{
Con_Reportf( S_ERROR "Demo message %i > %i\n", msglen, MAX_INIT_MSG );
CL_DemoCompleted();
return false;
}
if( msglen > 0 )
{
if( FS_Read( cls.demofile, buffer, msglen ) != msglen )
{
Con_Reportf( S_ERROR "Error reading demo message data\n" );
CL_DemoCompleted();
return false;
}
}
cls.netchan.last_received = host.realtime;
cls.netchan.total_received += msglen;
*length = msglen;
if( cls.state != ca_active )
Cbuf_Execute();
return true;
}
/*
=================
CL_DemoReadMessageQuake
reads demo data and write it to client
=================
*/
qboolean CL_DemoReadMessageQuake( byte *buffer, size_t *length )
{
vec3_t viewangles;
int msglen = 0;
demoangle_t *a;
*length = 0; // assume we fail
// decide if it is time to grab the next message
if( cls.signon == SIGNONS ) // allways grab until fully connected
{
if( cls.timedemo )
{
if( host.framecount == cls.td_lastframe )
return false; // already read this frame's message
cls.td_lastframe = host.framecount;
// if this is the second frame, grab the real td_starttime
// so the bogus time on the first frame doesn't count
if( host.framecount == cls.td_startframe + 1 )
cls.td_starttime = host.realtime;
}
else if( cl.time <= cl.mtime[0] )
{
// don't need another message yet
return false;
}
}
// get the next message
FS_Read( cls.demofile, &msglen, sizeof( int ));
FS_Read( cls.demofile, &viewangles[0], sizeof( float ));
FS_Read( cls.demofile, &viewangles[1], sizeof( float ));
FS_Read( cls.demofile, &viewangles[2], sizeof( float ));
cls.netchan.incoming_sequence++;
demo.timestamp = cl.mtime[0];
cl.skip_interp = false;
// make sure what interp info contain angles from different frames
// or lerping will stop working
if( demo.lasttime != demo.timestamp )
{
// select entry into circular buffer
demo.angle_position = (demo.angle_position + 1) & ANGLE_MASK;
a = &demo.cmds[demo.angle_position];
// record update
a->starttime = demo.timestamp;
VectorCopy( viewangles, a->viewangles );
demo.lasttime = demo.timestamp;
}
if( msglen < 0 )
{
Con_Reportf( S_ERROR "Demo message length < 0\n" );
CL_DemoCompleted();
return false;
}
if( msglen > MAX_INIT_MSG )
{
Con_Reportf( S_ERROR "Demo message %i > %i\n", msglen, MAX_INIT_MSG );
CL_DemoCompleted();
return false;
}
if( msglen > 0 )
{
if( FS_Read( cls.demofile, buffer, msglen ) != msglen )
{
Con_Reportf( S_ERROR "Error reading demo message data\n" );
CL_DemoCompleted();
return false;
}
}
cls.netchan.last_received = host.realtime;
cls.netchan.total_received += msglen;
*length = msglen;
if( cls.state != ca_active )
Cbuf_Execute();
return true;
}
/*
=================
CL_DemoReadMessage
reads demo data and write it to client
=================
*/
qboolean CL_DemoReadMessage( byte *buffer, size_t *length )
{
size_t curpos = 0, lastpos = 0;
float fElapsedTime = 0.0f;
qboolean swallowmessages = true;
static int tdlastdemoframe = 0;
byte *userbuf = NULL;
size_t size;
byte cmd;
if( !cls.demofile )
{
CL_DemoCompleted();
return false;
}
if(( !cl.background && ( cl.paused || cls.key_dest != key_game )) || cls.key_dest == key_console )
{
demo.starttime += host.frametime;
return false; // paused
}
if( cls.demoplayback == DEMO_QUAKE1 )
return CL_DemoReadMessageQuake( buffer, length );
do
{
qboolean bSkipMessage = false;
if( !cls.demofile ) break;
curpos = FS_Tell( cls.demofile );
CL_ReadDemoCmdHeader( &cmd, &demo.timestamp );
fElapsedTime = CL_GetDemoPlaybackClock() - demo.starttime;
if( !cls.timedemo ) bSkipMessage = ((demo.timestamp - cl_serverframetime()) >= fElapsedTime) ? true : false;
if( cls.changelevel ) demo.framecount = 1;
// changelevel issues
if( demo.framecount <= 2 && ( fElapsedTime - demo.timestamp ) > host.frametime )
demo.starttime = CL_GetDemoPlaybackClock();
// not ready for a message yet, put it back on the file.
if( cmd != dem_norewind && cmd != dem_stop && bSkipMessage )
{
// never skip first message
if( demo.framecount != 0 )
{
FS_Seek( cls.demofile, curpos, SEEK_SET );
return false; // not time yet.
}
}
// we already have the usercmd_t for this frame
// don't read next usercmd_t so predicting will work properly
if( cmd == dem_usercmd && lastpos != 0 && demo.framecount != 0 )
{
FS_Seek( cls.demofile, lastpos, SEEK_SET );
return false; // not time yet.
}
// COMMAND HANDLERS
switch( cmd )
{
case dem_jumptime:
demo.starttime = CL_GetDemoPlaybackClock();
return false; // time is changed, skip frame
case dem_stop:
CL_DemoMoveToNextSection();
return false; // header is ended, skip frame
case dem_userdata:
FS_Read( cls.demofile, &size, sizeof( int ));
userbuf = Mem_Malloc( cls.mempool, size );
FS_Read( cls.demofile, userbuf, size );
if( clgame.hInstance )
clgame.dllFuncs.pfnDemo_ReadBuffer( size, userbuf );
Mem_Free( userbuf );
userbuf = NULL;
break;
case dem_usercmd:
CL_ReadDemoUserCmd( false );
lastpos = FS_Tell( cls.demofile );
break;
default:
swallowmessages = false;
break;
}
} while( swallowmessages );
// If we are playing back a timedemo, and we've already passed on a
// frame update for this host_frame tag, then we'll just skip this message.
if( cls.timedemo && ( tdlastdemoframe == host.framecount ))
{
FS_Seek( cls.demofile, FS_Tell ( cls.demofile ) - 5, SEEK_SET );
return false;
}
tdlastdemoframe = host.framecount;
if( !cls.demofile )
return false;
// if not on "LOADING" section, check a few things
if( demo.entryIndex )
{
// We are now on the second frame of a new section,
// if so, reset start time (unless in a timedemo)
if( demo.framecount == 1 && !cls.timedemo )
{
// cheat by moving the relative start time forward.
demo.starttime = CL_GetDemoPlaybackClock();
}
}
demo.framecount++;
CL_ReadDemoSequence( false );
return CL_ReadRawNetworkData( buffer, length );
}
void CL_DemoFindInterpolatedViewAngles( float t, float *frac, demoangle_t **prev, demoangle_t **next )
{
int i, i0, i1, imod;
float at;
if( cls.timedemo ) return;
imod = demo.angle_position - 1;
i0 = (imod + 1) & ANGLE_MASK;
i1 = (imod + 0) & ANGLE_MASK;
if( demo.cmds[i0].starttime >= t )
{
for( i = 0; i < ANGLE_BACKUP - 2; i++ )
{
at = demo.cmds[imod & ANGLE_MASK].starttime;
if( at == 0.0f ) break;
if( at < t )
{
i0 = (imod + 1) & ANGLE_MASK;
i1 = (imod + 0) & ANGLE_MASK;
break;
}
imod--;
}
}
*next = &demo.cmds[i0];
*prev = &demo.cmds[i1];
// avoid division by zero (probably this should never happens)
if((*prev)->starttime == (*next)->starttime )
{
*prev = *next;
*frac = 0.0f;
return;
}
// time spans the two entries
*frac = ( t - (*prev)->starttime ) / ((*next)->starttime - (*prev)->starttime );
*frac = bound( 0.0f, *frac, 1.0f );
}
/*
==============
CL_DemoInterpolateAngles
We can predict or inpolate player movement with standed client code
but viewangles interpolate here
==============
*/
void CL_DemoInterpolateAngles( void )
{
demoangle_t *prev = NULL, *next = NULL;
float frac = 0.0f;
float curtime;
if( cls.demoplayback == DEMO_QUAKE1 )
{
// manually select next & prev states
next = &demo.cmds[(demo.angle_position - 0) & ANGLE_MASK];
prev = &demo.cmds[(demo.angle_position - 1) & ANGLE_MASK];
if( cl.skip_interp ) *prev = *next; // camera was teleported
frac = cl.lerpFrac;
}
else
{
curtime = (CL_GetDemoPlaybackClock() - demo.starttime) - host.frametime;
if( curtime > demo.timestamp )
curtime = demo.timestamp; // don't run too far
CL_DemoFindInterpolatedViewAngles( curtime, &frac, &prev, &next );
}
if( prev && next )
{
vec4_t q, q1, q2;
AngleQuaternion( next->viewangles, q1, false );
AngleQuaternion( prev->viewangles, q2, false );
QuaternionSlerp( q2, q1, frac, q );
QuaternionAngle( q, cl.viewangles );
}
else if( cl.cmd != NULL )
VectorCopy( cl.cmd->viewangles, cl.viewangles );
}
/*
==============
CL_FinishTimeDemo
show stats
==============
*/
void CL_FinishTimeDemo( void )
{
int frames;
double time;
cls.timedemo = false;
// the first frame didn't count
frames = (host.framecount - cls.td_startframe) - 1;
time = host.realtime - cls.td_starttime;
if( !time ) time = 1.0;
Con_Printf( "%i frames %5.3f seconds %5.3f fps\n", frames, time, frames / time );
}
/*
==============
CL_StopPlayback
Called when a demo file runs out, or the user starts a game
==============
*/
void CL_StopPlayback( void )
{
if( !cls.demoplayback ) return;
// release demofile
FS_Close( cls.demofile );
cls.demoplayback = false;
demo.framecount = 0;
cls.demofile = NULL;
cls.olddemonum = Q_max( -1, cls.demonum - 1 );
if( demo.directory.entries != NULL )
Mem_Free( demo.directory.entries );
cls.td_lastframe = host.framecount;
demo.directory.numentries = 0;
demo.directory.entries = NULL;
demo.header.host_fps = 0.0;
demo.entry = NULL;
cls.demoname[0] = '\0'; // clear demoname too
gameui.globals->demoname[0] = '\0';
if( cls.timedemo )
CL_FinishTimeDemo();
if( cls.changedemo )
{
S_StopAllSounds( true );
S_StopBackgroundTrack();
}
else
{
// let game known about demo state
Cvar_FullSet( "cl_background", "0", FCVAR_READ_ONLY );
cls.state = ca_disconnected;
memset( &cls.serveradr, 0, sizeof( cls.serveradr ) );
cls.set_lastdemo = false;
S_StopBackgroundTrack();
cls.connect_time = 0;
cls.demonum = -1;
cls.signon = 0;
// and finally clear the state
CL_ClearState ();
}
}
/*
==================
CL_GetDemoComment
==================
*/
int GAME_EXPORT CL_GetDemoComment( const char *demoname, char *comment )
{
file_t *demfile;
demoheader_t demohdr;
demodirectory_t directory;
demoentry_t entry;
float playtime = 0.0f;
int i;
if( !comment ) return false;
demfile = FS_Open( demoname, "rb", false );
if( !demfile )
{
comment[0] = '\0';
return false;
}
// read in the m_DemoHeader
FS_Read( demfile, &demohdr, sizeof( demoheader_t ));
if( demohdr.id != IDEMOHEADER )
{
FS_Close( demfile );
Q_strncpy( comment, "<corrupted>", MAX_STRING );
return false;
}
if( demohdr.net_protocol != PROTOCOL_VERSION || demohdr.dem_protocol != DEMO_PROTOCOL )
{
FS_Close( demfile );
Q_strncpy( comment, "<invalid protocol>", MAX_STRING );
return false;
}
// now read in the directory structure.
FS_Seek( demfile, demohdr.directory_offset, SEEK_SET );
FS_Read( demfile, &directory.numentries, sizeof( int ));
if( directory.numentries < 1 || directory.numentries > 1024 )
{
FS_Close( demfile );
Q_strncpy( comment, "<corrupted>", MAX_STRING );
return false;
}
for( i = 0; i < directory.numentries; i++ )
{
FS_Read( demfile, &entry, sizeof( demoentry_t ));
playtime += entry.playback_time;
}
// split comment to sections
Q_strncpy( comment, demohdr.mapname, CS_SIZE );
Q_strncpy( comment + CS_SIZE, demohdr.comment, CS_SIZE );
Q_snprintf( comment + CS_SIZE * 2, CS_TIME, "%g sec", playtime );
// all done
FS_Close( demfile );
return true;
}
/*
==================
CL_NextDemo
Called when a demo finishes
==================
*/
qboolean CL_NextDemo( void )
{
char str[MAX_QPATH];
if( cls.demonum == -1 )
return false; // don't play demos
S_StopAllSounds( true );
if( !cls.demos[cls.demonum][0] || cls.demonum == MAX_DEMOS )
{
cls.demonum = 0;
if( !cls.demos[cls.demonum][0] )
{
Con_Printf( "no demos listed with startdemos\n" );
cls.demonum = -1;
return false;
}
}
Q_snprintf( str, MAX_STRING, "playdemo %s\n", cls.demos[cls.demonum] );
Cbuf_InsertText( str );
cls.demonum++;
return true;
}
/*
==================
CL_CheckStartupDemos
queue demos loop after movie playing
==================
*/
void CL_CheckStartupDemos( void )
{
if( !cls.demos_pending )
return; // no demos in loop
if( cls.movienum != -1 )
return; // wait until movies finished
if( GameState->nextstate != STATE_RUNFRAME || cls.demoplayback )
{
// commandline override
cls.demos_pending = false;
cls.demonum = -1;
return;
}
// run demos loop in background mode
Cvar_SetValue( "v_dark", 1.0f );
cls.demos_pending = false;
cls.demonum = 0;
CL_NextDemo ();
}
/*
==================
CL_DemoGetName
==================
*/
static void CL_DemoGetName( int lastnum, char *filename )
{
if( lastnum < 0 || lastnum > 9999 )
{
// bound
Q_strcpy( filename, "demo9999" );
return;
}
Q_sprintf( filename, "demo%04d", lastnum );
}
/*
====================
CL_Record_f
record <demoname>
Begins recording a demo from the current position
====================
*/
void CL_Record_f( void )
{
string demoname, demopath;
const char *name;
int n;
if( Cmd_Argc() == 1 )
{
name = "new";
}
else if( Cmd_Argc() == 2 )
{
name = Cmd_Argv( 1 );
}
else
{
Con_Printf( S_USAGE "record <demoname>\n" );
return;
}
if( cls.demorecording )
{
Con_Printf( "Already recording.\n");
return;
}
if( cls.demoplayback )
{
Con_Printf( "Can't record during demo playback.\n");
return;
}
if( !cls.demoheader || cls.state != ca_active )
{
Con_Printf( "You must be in a level to record.\n");
return;
}
if( !Q_stricmp( name, "new" ))
{
// scan for a free filename
for( n = 0; n < 10000; n++ )
{
CL_DemoGetName( n, demoname );
if( !FS_FileExists( va( "%s.dem", demoname ), true ))
break;
}
if( n == 10000 )
{
Con_Printf( S_ERROR "no free slots for demo recording\n" );
return;
}
}
else Q_strncpy( demoname, name, sizeof( demoname ));
// open the demo file
Q_sprintf( demopath, "%s.dem", demoname );
// make sure what old demo is removed
if( FS_FileExists( demopath, false ))
FS_Delete( demopath );
Q_strncpy( cls.demoname, demoname, sizeof( cls.demoname ));
Q_strncpy( gameui.globals->demoname, demoname, sizeof( gameui.globals->demoname ));
CL_WriteDemoHeader( demopath );
}
/*
====================
CL_PlayDemo_f
playdemo <demoname>
====================
*/
void CL_PlayDemo_f( void )
{
char filename[MAX_QPATH];
char demoname[MAX_QPATH];
int i, ident;
if( Cmd_Argc() < 2 )
{
Con_Printf( S_USAGE "playdemo <demoname>\n" );
return;
}
if( cls.demoplayback )
{
CL_StopPlayback();
}
if( cls.demorecording )
{
Con_Printf( "Can't playback during demo record.\n");
return;
}
Q_strncpy( demoname, Cmd_Argv( 1 ), sizeof( demoname ));
COM_StripExtension( demoname );
Q_snprintf( filename, sizeof( filename ), "%s.dem", demoname );
// hidden parameter
if( Cmd_Argc() > 2 )
cls.set_lastdemo = Q_atoi( Cmd_Argv( 2 ));
// member last demo
if( cls.set_lastdemo )
Cvar_Set( "lastdemo", demoname );
if( !FS_FileExists( filename, true ))
{
Con_Printf( S_ERROR "couldn't open %s\n", filename );
CL_DemoAborted();
return;
}
cls.demofile = FS_Open( filename, "rb", true );
Q_strncpy( cls.demoname, demoname, sizeof( cls.demoname ));
Q_strncpy( gameui.globals->demoname, demoname, sizeof( gameui.globals->demoname ));
FS_Read( cls.demofile, &ident, sizeof( int ));
FS_Seek( cls.demofile, 0, SEEK_SET ); // rewind back to start
cls.forcetrack = 0;
// check for quake demos
if( ident != IDEMOHEADER )
{
int c, neg = false;
demo.header.host_fps = host_maxfps->value;
while(( c = FS_Getc( cls.demofile )) != '\n' )
{
if( c == '-' ) neg = true;
else cls.forcetrack = cls.forcetrack * 10 + (c - '0');
}
if( neg ) cls.forcetrack = -cls.forcetrack;
CL_DemoStartPlayback( DEMO_QUAKE1 );
return; // quake demo is started
}
// read in the demo header
FS_Read( cls.demofile, &demo.header, sizeof( demoheader_t ));
if( demo.header.id != IDEMOHEADER )
{
Con_Printf( S_ERROR "%s is not a demo file\n", demoname );
CL_DemoAborted();
return;
}
if( demo.header.net_protocol != PROTOCOL_VERSION || demo.header.dem_protocol != DEMO_PROTOCOL )
{
if( demo.header.dem_protocol != DEMO_PROTOCOL )
Con_Printf( S_ERROR "playdemo: demo protocol outdated (%i should be %i)\n", demo.header.dem_protocol, DEMO_PROTOCOL );
if( demo.header.net_protocol != PROTOCOL_VERSION )
Con_Printf( S_ERROR "playdemo: net protocol outdated (%i should be %i)\n", demo.header.net_protocol, PROTOCOL_VERSION );
CL_DemoAborted();
return;
}
// now read in the directory structure.
FS_Seek( cls.demofile, demo.header.directory_offset, SEEK_SET );
FS_Read( cls.demofile, &demo.directory.numentries, sizeof( int ));
if( demo.directory.numentries < 1 || demo.directory.numentries > 1024 )
{
Con_Printf( S_ERROR "demo had bogus # of directory entries: %i\n", demo.directory.numentries );
CL_DemoAborted();
return;
}
// allocate demo entries
demo.directory.entries = Mem_Malloc( cls.mempool, sizeof( demoentry_t ) * demo.directory.numentries );
for( i = 0; i < demo.directory.numentries; i++ )
{
FS_Read( cls.demofile, &demo.directory.entries[i], sizeof( demoentry_t ));
}
demo.entryIndex = 0;
demo.entry = &demo.directory.entries[demo.entryIndex];
FS_Seek( cls.demofile, demo.entry->offset, SEEK_SET );
CL_DemoStartPlayback( DEMO_XASH3D );
// g-cont. is this need?
Q_strncpy( cls.servername, demoname, sizeof( cls.servername ));
// begin a playback demo
}
/*
====================
CL_TimeDemo_f
timedemo <demoname>
====================
*/
void CL_TimeDemo_f( void )
{
if( Cmd_Argc() != 2 )
{
Con_Printf( S_USAGE "timedemo <demoname>\n" );
return;
}
CL_PlayDemo_f ();
// cls.td_starttime will be grabbed at the second frame of the demo, so
// all the loading time doesn't get counted
cls.timedemo = true;
cls.td_starttime = host.realtime;
cls.td_startframe = host.framecount;
cls.td_lastframe = -1; // get a new message this frame
}
/*
==================
CL_StartDemos_f
==================
*/
void CL_StartDemos_f( void )
{
int i, c;
if( cls.key_dest != key_menu )
{
Con_Printf( "'startdemos' is not valid from the console\n" );
return;
}
c = Cmd_Argc() - 1;
if( c > MAX_DEMOS )
{
Con_DPrintf( S_WARN "Host_StartDemos: max %i demos in demoloop\n", MAX_DEMOS );
c = MAX_DEMOS;
}
Con_Printf( "%i demo%s in loop\n", c, (c > 1) ? "s" : "" );
for( i = 1; i < c + 1; i++ )
Q_strncpy( cls.demos[i-1], Cmd_Argv( i ), sizeof( cls.demos[0] ));
cls.demos_pending = true;
}
/*
==================
CL_Demos_f
Return to looping demos
==================
*/
void CL_Demos_f( void )
{
if( cls.key_dest != key_menu )
{
Con_Printf( "'demos' is not valid from the console\n" );
return;
}
// demos loop are not running
if( cls.olddemonum == -1 )
return;
cls.demonum = cls.olddemonum;
// run demos loop in background mode
if( !SV_Active() && !cls.demoplayback )
CL_NextDemo ();
}
/*
====================
CL_Stop_f
stop any client activity
====================
*/
void CL_Stop_f( void )
{
// stop all
CL_StopRecord();
CL_StopPlayback();
SCR_StopCinematic();
// stop background track that was runned from the console
if( !SV_Active( ))
{
S_StopBackgroundTrack();
}
}