quakeforge/nq/source/cl_demo.c
Bill Currie ace8d9ebc5 Implement dynamic IMTs.
Now the user can create and destroy IMTs at will, though currently
destroying IMTs is currently all or nothing (imt_drop_all).

An IMT is created via imt_create which takes the keydest name (key_game
etc), the name of the IMT (must be unique for all IMTs) and optionally the
name of the IMT to which the key binding search will fall back if there is
no binding in the current IMT, but must be already defined and on the same
keydest. This means that IMTs now have user determined fallback paths. The
requirements for the fallback IMT prevent loops and other weird behaviour.

Actual key binding via in_bind is unaffected. This is why the IMT name must
be unique across all IMTs.

The "imt" command works with the key_game keydest, but imt_keydest is
provided for specifying the active IMT for a specific keydest.

At startup, default IMTs are setup to emulate the previous static IMTs so
old configs will continue to work (mostly). New config files will be
written with commands to drop all of the current IMTs and build new ones,
with the bindings and active IMT set as well.
2013-01-16 19:48:54 +09:00

706 lines
16 KiB
C

/*
cl_demo.c
demo playback support
Copyright (C) 1996-1997 Id Software, Inc.
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 2
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.
You should have received a copy of the GNU General Public License
along with this program; if not, write to:
Free Software Foundation, Inc.
59 Temple Place - Suite 330
Boston, MA 02111-1307, USA
*/
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#ifdef HAVE_STRING_H
# include <string.h>
#endif
#ifdef HAVE_STRINGS_H
# include <strings.h>
#endif
#include <time.h>
#include <ctype.h>
#include "QF/cmd.h"
#include "QF/cvar.h"
#include "QF/dstring.h"
#include "QF/keys.h"
#include "QF/msg.h"
#include "QF/qendian.h"
#include "QF/sys.h"
#include "QF/va.h"
#include "client.h"
#include "compat.h"
#include "host.h"
typedef struct {
int frames;
double time;
double fps;
} td_stats_t;
int demo_timeframes_isactive;
int demo_timeframes_index;
char demoname[1024];
double *demo_timeframes_array;
#define CL_TIMEFRAMES_ARRAYBLOCK 4096
int timedemo_count;
int timedemo_runs;
td_stats_t *timedemo_data;
static void CL_FinishTimeDemo (void);
static void CL_TimeFrames_DumpLog (void);
static void CL_TimeFrames_AddTimestamp (void);
static void CL_TimeFrames_Reset (void);
cvar_t *demo_gzip;
cvar_t *demo_speed;
cvar_t *demo_quit;
cvar_t *demo_timeframes;
#define MAX_DEMMSG (MAX_MSGLEN)
/*
DEMO CODE
When a demo is playing back, all NET_SendMessages are skipped, and
NET_GetMessages are read from the demo file.
Whenever cl.time gets past the last received message, another message is
read from the demo file.
*/
/*
CL_WriteDemoMessage
Dumps the current net message, prefixed by the length and view angles
*/
static void
CL_WriteDemoMessage (sizebuf_t *msg)
{
float f;
int len;
int i;
len = LittleLong (msg->cursize);
Qwrite (cls.demofile, &len, 4);
for (i = 0; i < 3; i++) {
f = LittleFloat (cl.viewangles[i]);
Qwrite (cls.demofile, &f, 4);
}
Qwrite (cls.demofile, msg->data, msg->cursize);
Qflush (cls.demofile);
}
/*
CL_StopPlayback
Called when a demo file runs out, or the user starts a game
*/
void
CL_StopPlayback (void)
{
if (!cls.demoplayback)
return;
Qclose (cls.demofile);
cls.demofile = NULL;
CL_SetState (ca_disconnected);
cls.demo_capture = 0;
cls.demoplayback = 0;
if (cls.timedemo)
CL_FinishTimeDemo ();
}
void
CL_StopRecording (void)
{
// write a disconnect message to the demo file
SZ_Clear (net_message->message);
MSG_WriteByte (net_message->message, svc_disconnect);
CL_WriteDemoMessage (net_message->message);
// finish up
Qclose (cls.demofile);
cls.demofile = NULL;
cls.demorecording = false;
Sys_Printf ("Completed demo\n");
}
// decide if it is time to grab the next message
static int
check_next_demopacket (void)
{
if (cls.state == ca_active) { // always grab until fully connected
if (cls.timedemo) {
if (host_framecount == cls.td_lastframe)
return 0; // 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 = Sys_DoubleTime ();
} else if (cl.time <= cl.mtime[0]) {
return 0; // don't need another message yet
}
}
return 1;
}
// get the next message
static int
read_demopacket (void)
{
int i, r;
float f;
Qread (cls.demofile, &net_message->message->cursize, 4);
net_message->message->cursize =
LittleLong (net_message->message->cursize);
if (net_message->message->cursize > MAX_DEMMSG)
Host_Error ("Demo message > MAX_DEMMSG: %d/%d",
net_message->message->cursize, MAX_DEMMSG);
VectorCopy (cl.mviewangles[0], cl.mviewangles[1]);
for (i = 0; i < 3; i++) {
r = Qread (cls.demofile, &f, 4);
cl.mviewangles[0][i] = LittleFloat (f);
}
r = Qread (cls.demofile, net_message->message->data,
net_message->message->cursize);
if (r != net_message->message->cursize) {
CL_StopPlayback ();
return 0;
}
return 1;
}
static int
CL_GetDemoMessage (void)
{
if (!check_next_demopacket ())
return 0;
return read_demopacket ();
}
static int
CL_GetPacket (void)
{
int r;
while (1) {
r = NET_GetMessage (cls.netcon);
if (r != 1 && r != 2)
return r;
// discard nop keepalive message
if (net_message->message->cursize == 1
&& net_message->message->data[0] == svc_nop)
Sys_Printf ("<-- server to client keepalive\n");
else
break;
}
return r;
}
/*
CL_GetMessage
Handles recording and playback of demos, on top of NET_ code
*/
int
CL_GetMessage (void)
{
if (cls.demoplayback) {
int ret = CL_GetDemoMessage ();
if (!ret && demo_timeframes_isactive && cls.td_starttime) {
CL_TimeFrames_AddTimestamp ();
}
return ret;
}
if (!CL_GetPacket ())
return 0;
if (cls.demorecording)
CL_WriteDemoMessage (net_message->message);
return 1;
}
/*
CL_Stop_f
stop recording a demo
*/
static void
CL_Stop_f (void)
{
if (cmd_source != src_command)
return;
if (!cls.demorecording) {
Sys_Printf ("Not recording a demo.\n");
return;
}
CL_StopRecording ();
}
/*
CL_Record_f
record <demoname> <map> [cd track]
*/
static void
CL_Record_f (void)
{
int c;
int track;
if (cmd_source != src_command)
return;
c = Cmd_Argc ();
if (c > 4) {
Sys_Printf ("record [<demoname> [<map> [cd track]]]\n");
return;
}
if (c >= 2 && strstr (Cmd_Argv (1), "..")) {
Sys_Printf ("Relative pathnames are not allowed.\n");
return;
}
if (c == 2 && cls.state >= ca_connected) {
Sys_Printf
("Can not record - already connected to server\nClient demo recording must be started before connecting\n");
return;
}
// write the forced cd track number, or -1
if (c == 4) {
track = atoi (Cmd_Argv (3));
Sys_Printf ("Forcing CD track to %i\n", cls.forcetrack);
} else
track = -1;
// start up the map
if (c > 2)
Cmd_ExecuteString (va ("map %s", Cmd_Argv (2)), src_command);
CL_Record (Cmd_Argv (1), track);
}
static dstring_t *
demo_default_name (const char *argv1)
{
dstring_t *name;
const char *mapname;
int mapname_len;
char timestring[20];
time_t tim;
name = dstring_new ();
if (argv1) {
dsprintf (name, "%s/%s", qfs_gamedir->dir.def, argv1);
return name;
}
// Get time to a useable format
time (&tim);
strftime (timestring, 19, "%Y-%m-%d-%H-%M", localtime (&tim));
// the leading path-name is to be removed from cl.worldmodel->name
mapname = QFS_SkipPath (cl.worldmodel->name);
// the map name is cut off after any "." because this would prevent
// an extension being appended
for (mapname_len = 0; mapname[mapname_len]; mapname_len++)
if (mapname[mapname_len] == '.')
break;
dsprintf (name, "%s/%s-%.*s", qfs_gamedir->dir.def, timestring,
mapname_len, mapname);
return name;
}
static void
demo_start_recording (int track)
{
cls.forcetrack = track;
Qprintf (cls.demofile, "%i\n", cls.forcetrack);
}
void
CL_Record (const char *argv1, int track)
{
dstring_t *name;
name = demo_default_name (argv1);
// open the demo file
#ifdef HAVE_ZLIB
if (demo_gzip->int_val) {
QFS_DefaultExtension (name, ".dem.gz");
cls.demofile = QFS_WOpen (name->str, demo_gzip->int_val);
} else
#endif
{
QFS_DefaultExtension (name, ".dem");
cls.demofile = QFS_WOpen (name->str, 0);
}
if (!cls.demofile) {
Sys_Printf ("ERROR: couldn't open.\n");
dstring_delete (name);
return;
}
Sys_Printf ("recording to %s.\n", name->str);
dstring_delete (name);
cls.demorecording = true;
demo_start_recording (track);
}
static int
demo_check_dem (void)
{
int c, ret = 0;
uint32_t len, bytes;
// .dem demo files begin with an ascii integer (possibly negative)
// representing the forced bgm track, followed by a newline
c = Qgetc (cls.demofile);
if (c == '-')
c = Qgetc (cls.demofile);
while (isdigit (c))
c = Qgetc (cls.demofile);
if (c != '\n')
goto done;
// After the bgm track comes the packet length plus 3 floats for view
// angles (not included in the packet length)
Qread (cls.demofile, &len, sizeof (len));
len = LittleLong (len);
if (len > MAX_DEMMSG)
goto done;
Qseek (cls.demofile, 3 * 4, SEEK_CUR); // 3 * float (angles)
// Normally, svc_serverinfo is the first command in the packet, but some
// servers (eg, fitzquake) add in an svc_print command first. Allow
// multiple svc_print commands (but nothing else) before the svc_serverinfo
net_message->message->cursize = len;
bytes = Qread (cls.demofile, net_message->message->data, len);
if (bytes != len)
goto done;
MSG_BeginReading (net_message);
while (!ret) {
if (net_message->badread)
goto done;
c = MSG_ReadByte (net_message);
switch (c) {
case svc_print:
MSG_ReadString (net_message);
break;
case svc_serverinfo:
ret = 1;
break;
default:
goto done;
}
}
done:
Qseek (cls.demofile, 0, SEEK_SET);
return ret;
}
static void
CL_StartDemo (void)
{
dstring_t *name;
int c;
qboolean neg = false;
// disconnect from server
CL_Disconnect ();
// open the demo file
name = dstring_strdup (demoname);
QFS_DefaultExtension (name, ".dem");
Sys_Printf ("Playing demo from %s.\n", name->str);
QFS_FOpenFile (name->str, &cls.demofile);
if (!cls.demofile) {
Sys_Printf ("ERROR: couldn't open.\n");
cls.demonum = -1; // stop demo loop
dstring_delete (name);
return;
}
if (!demo_check_dem ()) {
Sys_Printf ("%s is not a valid .dem file.\n", name->str);
cls.demonum = -1; // stop demo loop
dstring_delete (name);
Qclose (cls.demofile);
cls.demofile = 0;
return;
}
cls.demoplayback = true;
CL_SetState (ca_connected);
cls.forcetrack = 0;
Key_SetKeyDest (key_demo);
while ((c = Qgetc (cls.demofile)) != '\n')
if (c == '-')
neg = true;
else
cls.forcetrack = cls.forcetrack * 10 + (c - '0');
if (neg)
cls.forcetrack = -cls.forcetrack;
dstring_delete (name);
}
/*
CL_PlayDemo_f
play [demoname]
*/
static void
CL_PlayDemo_f (void)
{
if (cmd_source != src_command)
return;
switch (Cmd_Argc ()) {
case 1:
if (!demoname[0])
goto playdemo_error;
// fall through
case 2:
cls.demo_capture = 0;
break;
case 3:
if (!strcmp (Cmd_Argv (2), "rec")) {
cls.demo_capture = 1;
break;
}
// fall through
default:
playdemo_error:
Sys_Printf ("play <demoname> : plays a demo\n");
return;
}
timedemo_runs = timedemo_count = 1; // make sure looped timedemos stop
if (Cmd_Argc () > 1)
strncpy (demoname, Cmd_Argv (1), sizeof (demoname));
CL_StartDemo ();
}
static void
CL_StartTimeDemo (void)
{
CL_StartDemo ();
// 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 = 0;
cls.td_startframe = host_framecount;
cls.td_lastframe = -1; // get a new message this frame
CL_TimeFrames_Reset ();
if (demo_timeframes->int_val)
demo_timeframes_isactive = 1;
}
static inline double
sqr (double x)
{
return x * x;
}
static void
CL_FinishTimeDemo (void)
{
int frames;
float time;
cls.timedemo = false;
// the first frame didn't count
frames = (host_framecount - cls.td_startframe) - 1;
time = Sys_DoubleTime () - cls.td_starttime;
if (!time)
time = 1;
Sys_Printf ("%i frame%s %.4g seconds %.4g fps\n", frames,
frames == 1 ? "" : "s", time, frames / time);
CL_TimeFrames_DumpLog ();
demo_timeframes_isactive = 0;
timedemo_count--;
if (timedemo_data) {
timedemo_data[timedemo_count].frames = frames;
timedemo_data[timedemo_count].time = time;
timedemo_data[timedemo_count].fps = frames / time;
}
if (timedemo_count > 0) {
CL_StartTimeDemo ();
} else {
if (--timedemo_runs > 0 && timedemo_data) {
double average = 0;
double variance = 0;
double min, max;
int i;
min = max = timedemo_data[0].fps;
for (i = 0; i < timedemo_runs; i++) {
average += timedemo_data[i].fps;
min = min (min, timedemo_data[i].fps);
max = max (max, timedemo_data[i].fps);
}
average /= timedemo_runs;
for (i = 0; i < timedemo_runs; i++)
variance += sqr (timedemo_data[i].fps - average);
variance /= timedemo_runs;
Sys_Printf ("timedemo stats for %d runs:\n", timedemo_runs);
Sys_Printf (" average fps: %.3f\n", average);
Sys_Printf (" min/max fps: %.3f/%.3f\n", min, max);
Sys_Printf ("std deviation: %.3f fps\n", sqrt (variance));
}
if (demo_quit->int_val)
Cbuf_InsertText (host_cbuf, "quit\n");
}
}
/*
CL_TimeDemo_f
timedemo [demoname]
*/
static void
CL_TimeDemo_f (void)
{
int count = 1;
if (cmd_source != src_command)
return;
if (Cmd_Argc () < 2 || Cmd_Argc () > 3) {
Sys_Printf ("timedemo <demoname> [count]: gets demo speeds\n");
return;
}
timedemo_runs = timedemo_count = 1; // make sure looped timedemos stop
if (Cmd_Argc () == 3)
count = atoi (Cmd_Argv (2));
if (timedemo_data) {
free (timedemo_data);
timedemo_data = 0;
}
timedemo_data = calloc (timedemo_runs, sizeof (td_stats_t));
strncpy (demoname, Cmd_Argv (1), sizeof (demoname));
CL_StartTimeDemo ();
timedemo_runs = timedemo_count = max (count, 1);
timedemo_data = calloc (timedemo_runs, sizeof (td_stats_t));
}
void
CL_Demo_Init (void)
{
demo_timeframes_isactive = 0;
demo_timeframes_index = 0;
demo_timeframes_array = NULL;
demo_gzip = Cvar_Get ("demo_gzip", "0", CVAR_ARCHIVE, NULL,
"Compress demos using gzip. 0 = none, 1 = least "
"compression, 9 = most compression. Compressed "
" demos (1-9) will have .gz appended to the name");
demo_speed = Cvar_Get ("demo_speed", "1.0", CVAR_NONE, NULL,
"adjust demo playback speed. 1.0 = normal, "
"< 1 slow-mo, > 1 timelapse");
demo_quit = Cvar_Get ("demo_quit", "0", CVAR_NONE, NULL,
"automaticly quit after a timedemo has finished");
demo_timeframes = Cvar_Get ("demo_timeframes", "0", CVAR_NONE, NULL,
"write timestamps for every frame");
Cmd_AddCommand ("record", CL_Record_f, "Record a demo, if no filename "
"argument is given\n"
"the demo will be called Year-Month-Day-Hour-Minute-"
"Mapname");
Cmd_AddCommand ("stop", CL_Stop_f, "Stop recording a demo");
Cmd_AddCommand ("playdemo", CL_PlayDemo_f, "Play a recorded demo");
Cmd_AddCommand ("timedemo", CL_TimeDemo_f, "Play a demo as fast as your "
"hardware can. Useful for benchmarking.");
}
static void
CL_TimeFrames_Reset (void)
{
demo_timeframes_index = 0;
free (demo_timeframes_array);
demo_timeframes_array = NULL;
}
static void
CL_TimeFrames_AddTimestamp (void)
{
if (!(demo_timeframes_index % CL_TIMEFRAMES_ARRAYBLOCK))
demo_timeframes_array = realloc
(demo_timeframes_array, sizeof (demo_timeframes_array[0]) *
((demo_timeframes_index / CL_TIMEFRAMES_ARRAYBLOCK) + 1) *
CL_TIMEFRAMES_ARRAYBLOCK);
if (demo_timeframes_array == NULL)
Sys_Error ("Unable to allocate timeframes buffer");
demo_timeframes_array[demo_timeframes_index] = Sys_DoubleTime ();
demo_timeframes_index++;
}
static void
CL_TimeFrames_DumpLog (void)
{
const char *filename = "timeframes.txt";
int i;
long frame;
QFile *outputfile;
if (demo_timeframes_isactive == 0)
return;
Sys_Printf ("Dumping Timed Frames log: %s\n", filename);
outputfile = QFS_Open (filename, "w");
if (!outputfile) {
Sys_Printf ("Could not open: %s\n", filename);
return;
}
for (i = 1; i < demo_timeframes_index; i++) {
frame = (demo_timeframes_array[i] - demo_timeframes_array[i - 1]) * 1e6;
Qprintf (outputfile, "%09ld\n", frame);
}
Qclose (outputfile);
}