mirror of
https://github.com/w23/xash3d-fwgs
synced 2024-12-16 22:20:01 +01:00
5e0a0765ce
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:]]\+$//' {} \+ ```
1623 lines
37 KiB
C
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();
|
|
}
|
|
}
|