mirror of
https://git.code.sf.net/p/quake/quakeforge
synced 2024-11-26 22:31:05 +00:00
12c84046f3
This is an extremely extensive patch as it hits every cvar, and every usage of the cvars. Cvars no longer store the value they control, instead, they use a cexpr value object to reference the value and specify the value's type (currently, a null type is used for strings). Non-string cvars are passed through cexpr, allowing expressions in the cvars' settings. Also, cvars have returned to an enhanced version of the original (id quake) registration scheme. As a minor benefit, relevant code having direct access to the cvar-controlled variables is probably a slight optimization as it removed a pointer dereference, and the variables can be located for data locality. The static cvar descriptors are made private as an additional safety layer, though there's nothing stopping external modification via Cvar_FindVar (which is needed for adding listeners). While not used yet (partly due to working out the design), cvars can have a validation function. Registering a cvar allows a primary listener (and its data) to be specified: it will always be called first when the cvar is modified. The combination of proper listeners and direct access to the controlled variable greatly simplifies the more complex cvar interactions as much less null checking is required, and there's no need for one cvar's callback to call another's. nq-x11 is known to work at least well enough for the demos. More testing will come.
1292 lines
29 KiB
C
1292 lines
29 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
|
|
#ifdef HAVE_UNISTD_H
|
|
# include <unistd.h>
|
|
#endif
|
|
|
|
#ifdef HAVE_SYS_TIME_H
|
|
#include <sys/time.h>
|
|
#endif
|
|
#include <time.h>
|
|
#include <math.h>
|
|
|
|
#include "QF/cbuf.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 "compat.h"
|
|
|
|
#include "client/world.h"
|
|
|
|
#include "qw/include/cl_cam.h"
|
|
#include "qw/include/cl_demo.h"
|
|
#include "qw/include/cl_ents.h"
|
|
#include "qw/include/cl_main.h"
|
|
#include "qw/include/client.h"
|
|
#include "qw/include/host.h"
|
|
#include "qw/pmove.h"
|
|
|
|
typedef struct {
|
|
int frames;
|
|
double time;
|
|
double fps;
|
|
} td_stats_t;
|
|
|
|
static int demo_timeframes_isactive;
|
|
static int demo_timeframes_index;
|
|
static int demotime_cached;
|
|
static float cached_demotime;
|
|
static byte cached_newtime;
|
|
static float nextdemotime;
|
|
static dstring_t *demoname;
|
|
static 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);
|
|
|
|
int demo_gzip;
|
|
static cvar_t demo_gzip_cvar = {
|
|
.name = "demo_gzip",
|
|
.description =
|
|
"Compress demos using gzip. 0 = none, 1 = least compression, 9 = most "
|
|
"compression. Compressed demos (1-9) will have .gz appended to the "
|
|
"name",
|
|
.default_value = "0",
|
|
.flags = CVAR_ARCHIVE,
|
|
.value = { .type = &cexpr_int, .value = &demo_gzip },
|
|
};
|
|
float demo_speed;
|
|
static cvar_t demo_speed_cvar = {
|
|
.name = "demo_speed",
|
|
.description =
|
|
"adjust demo playback speed. 1.0 = normal, < 1 slow-mo, > 1 timelapse",
|
|
.default_value = "1.0",
|
|
.flags = CVAR_NONE,
|
|
.value = { .type = &cexpr_float, .value = &demo_speed },
|
|
};
|
|
int demo_quit;
|
|
static cvar_t demo_quit_cvar = {
|
|
.name = "demo_quit",
|
|
.description =
|
|
"automaticly quit after a timedemo has finished",
|
|
.default_value = "0",
|
|
.flags = CVAR_NONE,
|
|
.value = { .type = &cexpr_int, .value = &demo_quit },
|
|
};
|
|
int demo_timeframes;
|
|
static cvar_t demo_timeframes_cvar = {
|
|
.name = "demo_timeframes",
|
|
.description =
|
|
"write timestamps for every frame",
|
|
.default_value = "0",
|
|
.flags = CVAR_NONE,
|
|
.value = { .type = &cexpr_int, .value = &demo_timeframes },
|
|
};
|
|
|
|
#define MAX_DEMMSG (MAX_MSGLEN + 8) //+8 for header
|
|
|
|
/*
|
|
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)
|
|
{
|
|
byte c;
|
|
float f;
|
|
int len;
|
|
|
|
f = LittleFloat ((float) realtime);
|
|
Qwrite (cls.demofile, &f, sizeof (f));
|
|
|
|
c = dem_read;
|
|
Qwrite (cls.demofile, &c, sizeof (c));
|
|
|
|
len = LittleLong (msg->cursize);
|
|
Qwrite (cls.demofile, &len, 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;
|
|
cls.demoplayback2 = 0;
|
|
demotime_cached = 0;
|
|
net_blocksend = 0;
|
|
|
|
if (cls.timedemo)
|
|
CL_FinishTimeDemo ();
|
|
}
|
|
|
|
void
|
|
CL_StopRecording (void)
|
|
{
|
|
// write a disconnect message to the demo file
|
|
SZ_Clear (net_message->message);
|
|
MSG_WriteLong (net_message->message, -1); // -1 sequence means out of band
|
|
MSG_WriteByte (net_message->message, svc_disconnect);
|
|
MSG_WriteString (net_message->message, "EndOfDemo");
|
|
CL_WriteDemoMessage (net_message->message);
|
|
|
|
// finish up
|
|
Qclose (cls.demofile);
|
|
cls.demofile = NULL;
|
|
cls.demorecording = false;
|
|
Sys_Printf ("Completed demo\n");
|
|
}
|
|
|
|
#if 0
|
|
static const char *dem_names[] = {
|
|
"dem_cmd",
|
|
"dem_read",
|
|
"dem_set",
|
|
"dem_multiple",
|
|
"dem_single",
|
|
"dem_stats",
|
|
"dem_all",
|
|
"dem_invalid",
|
|
};
|
|
#endif
|
|
|
|
static void
|
|
get_demotime (float *demotime, byte *newtime)
|
|
{
|
|
// read the time from the packet
|
|
*newtime = 0;
|
|
if (demotime_cached) {
|
|
*demotime = cached_demotime;
|
|
*newtime = cached_newtime;
|
|
demotime_cached = 0;
|
|
} else {
|
|
if (cls.demoplayback2) {
|
|
Qread (cls.demofile, newtime, sizeof (*newtime));
|
|
*demotime = cls.basetime + (cls.prevtime + *newtime) * 0.001;
|
|
} else {
|
|
Qread (cls.demofile, demotime, sizeof (*demotime));
|
|
*demotime = LittleFloat (*demotime);
|
|
if (!nextdemotime)
|
|
realtime = nextdemotime = *demotime;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void
|
|
push_demotime (float demotime, byte newtime)
|
|
{
|
|
// rewind back to time
|
|
demotime_cached = 1;
|
|
cached_demotime = demotime;
|
|
cached_newtime = newtime;
|
|
}
|
|
|
|
// decide if it is time to grab the next message
|
|
static int
|
|
check_next_demopacket (void)
|
|
{
|
|
byte newtime;
|
|
float demotime;
|
|
|
|
get_demotime (&demotime, &newtime);
|
|
|
|
if (cls.timedemo) {
|
|
if (cls.td_lastframe < 0)
|
|
cls.td_lastframe = demotime;
|
|
else if (demotime > cls.td_lastframe) {
|
|
cls.td_lastframe = demotime;
|
|
push_demotime (demotime, newtime);
|
|
return 0; // already read this frame's message
|
|
}
|
|
if (!cls.td_starttime && cls.state == ca_active) {
|
|
cls.td_starttime = Sys_DoubleTime ();
|
|
cls.td_startframe = host_framecount;
|
|
}
|
|
realtime = demotime; // warp
|
|
} else if (!cl.paused && cls.state >= ca_onserver) {
|
|
// always grab until fully connected
|
|
if (!cls.demoplayback2 && realtime + 1.0 < demotime) {
|
|
// too far back
|
|
realtime = demotime - 1.0;
|
|
push_demotime (demotime, newtime);
|
|
return 0;
|
|
} else if (realtime < demotime) {
|
|
push_demotime (demotime, newtime);
|
|
return 0; // don't need another message yet
|
|
}
|
|
} else
|
|
realtime = demotime; // we're warping
|
|
|
|
if (realtime - nextdemotime > 0.0001) {
|
|
if (nextdemotime != demotime) {
|
|
if (cls.demoplayback2) {
|
|
cls.netchan.incoming_sequence++;
|
|
cls.netchan.incoming_acknowledged++;
|
|
cls.netchan.frame_latency = 0;
|
|
}
|
|
}
|
|
}
|
|
nextdemotime = demotime;
|
|
|
|
cls.prevtime += newtime;
|
|
return 1;
|
|
}
|
|
|
|
static int
|
|
read_demopacket (void)
|
|
{
|
|
unsigned r;
|
|
|
|
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);
|
|
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)
|
|
{
|
|
byte c;
|
|
float f;
|
|
int r, i, j, tracknum;
|
|
usercmd_t *pcmd;
|
|
|
|
if (!cls.demoplayback2)
|
|
nextdemotime = realtime;
|
|
if (realtime + 1.0 < nextdemotime)
|
|
realtime = nextdemotime - 1.0;
|
|
|
|
nextdemomessage:
|
|
if (!check_next_demopacket ())
|
|
return 0;
|
|
|
|
if (cls.state < ca_demostart)
|
|
Host_Error ("CL_GetDemoMessage: cls.state != ca_active");
|
|
|
|
// get the msg type
|
|
Qread (cls.demofile, &c, sizeof (c));
|
|
|
|
switch (c & 7) {
|
|
case dem_cmd:
|
|
// user sent input
|
|
net_message->message->cursize = -1;
|
|
i = cls.netchan.outgoing_sequence & UPDATE_MASK;
|
|
pcmd = &cl.frames[i].cmd;
|
|
r = Qread (cls.demofile, pcmd, sizeof (*pcmd));
|
|
if (r != sizeof (*pcmd)) {
|
|
CL_StopPlayback ();
|
|
return 0;
|
|
}
|
|
// byte order stuff
|
|
for (j = 0; j < 3; j++)
|
|
pcmd->angles[j] = LittleFloat (pcmd->angles[j]);
|
|
pcmd->forwardmove = LittleShort (pcmd->forwardmove);
|
|
pcmd->sidemove = LittleShort (pcmd->sidemove);
|
|
pcmd->upmove = LittleShort (pcmd->upmove);
|
|
cl.frames[i].senttime = nextdemotime; //FIXME was demotime
|
|
cl.frames[i].receivedtime = -1; // we haven't gotten a reply yet
|
|
cls.netchan.outgoing_sequence++;
|
|
for (i = 0; i < 3; i++) {
|
|
Qread (cls.demofile, &f, 4);
|
|
cl.viewstate.player_angles[i] = LittleFloat (f);
|
|
}
|
|
break;
|
|
|
|
case dem_read:
|
|
readit:
|
|
// get the next message
|
|
if (read_demopacket () && cls.demoplayback2) {
|
|
tracknum = Cam_TrackNum ();
|
|
|
|
if (cls.lasttype == dem_multiple) {
|
|
if (tracknum == -1)
|
|
goto nextdemomessage;
|
|
if (!(cls.lastto & (1 << tracknum)))
|
|
goto nextdemomessage;
|
|
} else if (cls.lasttype == dem_single) {
|
|
if (tracknum == -1 || cls.lastto != spec_track)
|
|
goto nextdemomessage;
|
|
}
|
|
}
|
|
break;
|
|
case dem_set:
|
|
Qread (cls.demofile, &i, 4);
|
|
cls.netchan.outgoing_sequence = LittleLong (i);
|
|
Qread (cls.demofile, &i, 4);
|
|
cls.netchan.incoming_sequence = LittleLong (i);
|
|
if (cls.demoplayback2) {
|
|
cls.netchan.incoming_acknowledged =
|
|
cls.netchan.incoming_sequence;
|
|
goto nextdemomessage;
|
|
}
|
|
break;
|
|
case dem_multiple:
|
|
r = Qread (cls.demofile, &i, 4);
|
|
if (r != 4) {
|
|
CL_StopPlayback ();
|
|
return 0;
|
|
}
|
|
cls.lastto = LittleLong (i);
|
|
cls.lasttype = dem_multiple;
|
|
goto readit;
|
|
case dem_single:
|
|
cls.lastto = c >> 3;
|
|
cls.lasttype = dem_single;
|
|
goto readit;
|
|
case dem_stats:
|
|
cls.lastto = c >> 3;
|
|
cls.lasttype = dem_stats;
|
|
goto readit;
|
|
case dem_all:
|
|
cls.lastto = 0;
|
|
cls.lasttype = dem_all;
|
|
goto readit;
|
|
default:
|
|
Sys_Printf ("Corrupted demo.\n");
|
|
CL_StopPlayback ();
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
static int
|
|
CL_GetPacket (void)
|
|
{
|
|
return NET_GetPacket ();
|
|
}
|
|
|
|
/*
|
|
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 (net_packetlog)
|
|
Log_Incoming_Packet (net_message->message->data,
|
|
net_message->message->cursize, 1);
|
|
|
|
|
|
if (cls.demorecording)
|
|
CL_WriteDemoMessage (net_message->message);
|
|
|
|
return 1;
|
|
}
|
|
|
|
/*
|
|
CL_WriteDemoCmd
|
|
|
|
Writes the current user cmd
|
|
*/
|
|
void
|
|
CL_WriteDemoCmd (usercmd_t *pcmd)
|
|
{
|
|
byte c;
|
|
float fl;
|
|
int i;
|
|
usercmd_t cmd;
|
|
|
|
fl = LittleFloat ((float) realtime);
|
|
Qwrite (cls.demofile, &fl, sizeof (fl));
|
|
|
|
c = dem_cmd;
|
|
Qwrite (cls.demofile, &c, sizeof (c));
|
|
|
|
// correct for byte order, bytes don't matter
|
|
cmd = *pcmd;
|
|
|
|
for (i = 0; i < 3; i++)
|
|
cmd.angles[i] = LittleFloat (cmd.angles[i]);
|
|
cmd.forwardmove = LittleShort (cmd.forwardmove);
|
|
cmd.sidemove = LittleShort (cmd.sidemove);
|
|
cmd.upmove = LittleShort (cmd.upmove);
|
|
|
|
Qwrite (cls.demofile, &cmd, sizeof (cmd));
|
|
|
|
for (i = 0; i < 3; i++) {
|
|
fl = LittleFloat (cl.viewstate.player_angles[i]);
|
|
Qwrite (cls.demofile, &fl, 4);
|
|
}
|
|
|
|
Qflush (cls.demofile);
|
|
}
|
|
|
|
/*
|
|
CL_Stop_f
|
|
|
|
stop recording a demo
|
|
*/
|
|
static void
|
|
CL_Stop_f (void)
|
|
{
|
|
if (!cls.demorecording) {
|
|
Sys_Printf ("Not recording a demo.\n");
|
|
return;
|
|
}
|
|
CL_StopRecording ();
|
|
}
|
|
|
|
/*
|
|
CL_Record_f
|
|
|
|
record <demoname>
|
|
*/
|
|
static void
|
|
CL_Record_f (void)
|
|
{
|
|
if (Cmd_Argc () > 2) {
|
|
// we use a demo name like year-month-day-hours-minutes-mapname.qwd
|
|
// if there is no argument
|
|
Sys_Printf ("record [demoname]\n");
|
|
return;
|
|
}
|
|
|
|
if (cls.demoplayback || cls.state != ca_active) {
|
|
Sys_Printf ("You must be connected to record.\n");
|
|
return;
|
|
}
|
|
|
|
if (cls.demorecording)
|
|
CL_Stop_f ();
|
|
if (Cmd_Argc () == 2)
|
|
CL_Record (Cmd_Argv (1), -1);
|
|
else
|
|
CL_Record (0, -1);
|
|
}
|
|
|
|
/*
|
|
CL_ReRecord_f
|
|
|
|
record <demoname>
|
|
*/
|
|
static void
|
|
CL_ReRecord_f (void)
|
|
{
|
|
dstring_t *name;
|
|
int c;
|
|
|
|
c = Cmd_Argc ();
|
|
if (c != 2) {
|
|
Sys_Printf ("rerecord <demoname>\n");
|
|
return;
|
|
}
|
|
|
|
if (!cls.servername || !cls.servername->str) {
|
|
Sys_Printf ("No server to which to reconnect...\n");
|
|
return;
|
|
}
|
|
|
|
if (cls.demorecording)
|
|
CL_Stop_f ();
|
|
|
|
name = dstring_newstr ();
|
|
dsprintf (name, "%s/%s", qfs_gamedir->dir.def, Cmd_Argv (1));
|
|
|
|
// open the demo file
|
|
QFS_DefaultExtension (name, ".qwd");
|
|
|
|
cls.demofile = QFS_WOpen (name->str, 0);
|
|
if (!cls.demofile) {
|
|
Sys_Printf ("ERROR: couldn't open.\n");
|
|
} else {
|
|
Sys_Printf ("recording to %s.\n", name->str);
|
|
cls.demorecording = true;
|
|
|
|
CL_Disconnect ();
|
|
CL_BeginServerConnect ();
|
|
}
|
|
dstring_delete (name);
|
|
}
|
|
|
|
/*
|
|
CL_WriteRecordDemoMessage
|
|
|
|
Dumps the current net message, prefixed by the length and view angles
|
|
*/
|
|
static void
|
|
CL_WriteRecordDemoMessage (sizebuf_t *msg, int seq)
|
|
{
|
|
byte c;
|
|
float fl;
|
|
int len, i;
|
|
|
|
if (!cls.demorecording)
|
|
return;
|
|
|
|
fl = LittleFloat ((float) realtime);
|
|
Qwrite (cls.demofile, &fl, sizeof (fl));
|
|
|
|
c = dem_read;
|
|
Qwrite (cls.demofile, &c, sizeof (c));
|
|
|
|
len = LittleLong (msg->cursize + 8);
|
|
Qwrite (cls.demofile, &len, 4);
|
|
|
|
i = LittleLong (seq);
|
|
Qwrite (cls.demofile, &i, 4);
|
|
Qwrite (cls.demofile, &i, 4);
|
|
|
|
Qwrite (cls.demofile, msg->data, msg->cursize);
|
|
|
|
Qflush (cls.demofile);
|
|
}
|
|
|
|
static void
|
|
CL_WriteSetDemoMessage (void)
|
|
{
|
|
byte c;
|
|
float fl;
|
|
int len;
|
|
|
|
if (!cls.demorecording)
|
|
return;
|
|
|
|
fl = LittleFloat ((float) realtime);
|
|
Qwrite (cls.demofile, &fl, sizeof (fl));
|
|
|
|
c = dem_set;
|
|
Qwrite (cls.demofile, &c, sizeof (c));
|
|
|
|
len = LittleLong (cls.netchan.outgoing_sequence);
|
|
Qwrite (cls.demofile, &len, 4);
|
|
len = LittleLong (cls.netchan.incoming_sequence);
|
|
Qwrite (cls.demofile, &len, 4);
|
|
|
|
Qflush (cls.demofile);
|
|
}
|
|
|
|
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_world.worldmodel->name
|
|
mapname = QFS_SkipPath (cl_world.worldmodel->path);
|
|
|
|
// 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)
|
|
{
|
|
byte buf_data[MAX_MSGLEN + 10]; // + 10 for header
|
|
char *s;
|
|
int n, i;
|
|
int seq = 1;
|
|
entity_state_t *es, blankes;
|
|
player_info_t *player;
|
|
sizebuf_t buf;
|
|
|
|
// serverdata
|
|
// send the info about the new client to all connected clients
|
|
memset (&buf, 0, sizeof (buf));
|
|
buf.data = buf_data;
|
|
buf.maxsize = sizeof (buf_data);
|
|
|
|
// send the serverdata
|
|
MSG_WriteByte (&buf, svc_serverdata);
|
|
MSG_WriteLong (&buf, PROTOCOL_VERSION);
|
|
MSG_WriteLong (&buf, cl.servercount);
|
|
MSG_WriteString (&buf, qfs_gamedir->gamedir);
|
|
|
|
if (cl.spectator)
|
|
MSG_WriteByte (&buf, cl.playernum | 128);
|
|
else
|
|
MSG_WriteByte (&buf, cl.playernum);
|
|
|
|
// send full levelname
|
|
MSG_WriteString (&buf, cl.levelname);
|
|
|
|
// send the movevars
|
|
MSG_WriteFloat (&buf, movevars.gravity);
|
|
MSG_WriteFloat (&buf, movevars.stopspeed);
|
|
MSG_WriteFloat (&buf, movevars.maxspeed);
|
|
MSG_WriteFloat (&buf, movevars.spectatormaxspeed);
|
|
MSG_WriteFloat (&buf, movevars.accelerate);
|
|
MSG_WriteFloat (&buf, movevars.airaccelerate);
|
|
MSG_WriteFloat (&buf, movevars.wateraccelerate);
|
|
MSG_WriteFloat (&buf, movevars.friction);
|
|
MSG_WriteFloat (&buf, movevars.waterfriction);
|
|
MSG_WriteFloat (&buf, movevars.entgravity);
|
|
|
|
// send music
|
|
MSG_WriteByte (&buf, svc_cdtrack);
|
|
MSG_WriteByte (&buf, 0); // none in demos
|
|
|
|
// send server info string
|
|
MSG_WriteByte (&buf, svc_stufftext);
|
|
MSG_WriteString (&buf, va (0, "fullserverinfo \"%s\"\n",
|
|
Info_MakeString (cl.serverinfo, 0)));
|
|
|
|
// flush packet
|
|
CL_WriteRecordDemoMessage (&buf, seq++);
|
|
SZ_Clear (&buf);
|
|
|
|
// soundlist
|
|
MSG_WriteByte (&buf, svc_soundlist);
|
|
MSG_WriteByte (&buf, 0);
|
|
|
|
n = 0;
|
|
s = cl.sound_name[n + 1];
|
|
while (*s) {
|
|
MSG_WriteString (&buf, s);
|
|
if (buf.cursize > MAX_MSGLEN / 2) {
|
|
MSG_WriteByte (&buf, 0);
|
|
MSG_WriteByte (&buf, n);
|
|
CL_WriteRecordDemoMessage (&buf, seq++);
|
|
SZ_Clear (&buf);
|
|
MSG_WriteByte (&buf, svc_soundlist);
|
|
MSG_WriteByte (&buf, n + 1);
|
|
}
|
|
n++;
|
|
s = cl.sound_name[n + 1];
|
|
}
|
|
if (buf.cursize) {
|
|
MSG_WriteByte (&buf, 0);
|
|
MSG_WriteByte (&buf, 0);
|
|
CL_WriteRecordDemoMessage (&buf, seq++);
|
|
SZ_Clear (&buf);
|
|
}
|
|
// modellist
|
|
MSG_WriteByte (&buf, svc_modellist);
|
|
MSG_WriteByte (&buf, 0);
|
|
|
|
n = 0;
|
|
s = cl.model_name[n + 1];
|
|
while (*s) {
|
|
MSG_WriteString (&buf, s);
|
|
if (buf.cursize > MAX_MSGLEN / 2) {
|
|
MSG_WriteByte (&buf, 0);
|
|
MSG_WriteByte (&buf, n);
|
|
CL_WriteRecordDemoMessage (&buf, seq++);
|
|
SZ_Clear (&buf);
|
|
MSG_WriteByte (&buf, svc_modellist);
|
|
MSG_WriteByte (&buf, n + 1);
|
|
}
|
|
n++;
|
|
s = cl.model_name[n + 1];
|
|
}
|
|
if (buf.cursize) {
|
|
MSG_WriteByte (&buf, 0);
|
|
MSG_WriteByte (&buf, 0);
|
|
CL_WriteRecordDemoMessage (&buf, seq++);
|
|
SZ_Clear (&buf);
|
|
}
|
|
// spawnstatic
|
|
for (size_t staticIndex = 0; staticIndex < cl_static_entities.size;
|
|
staticIndex++) {
|
|
entity_state_t *es = &cl_static_entities.a[staticIndex];
|
|
|
|
MSG_WriteByte (&buf, svc_spawnstatic);
|
|
|
|
MSG_WriteByte (&buf, es->modelindex);
|
|
|
|
MSG_WriteByte (&buf, es->frame);
|
|
MSG_WriteByte (&buf, 0);
|
|
MSG_WriteByte (&buf, es->skinnum);
|
|
MSG_WriteCoordAngleV (&buf, (vec_t*)&es->origin, es->angles);//FIXME
|
|
|
|
if (buf.cursize > MAX_MSGLEN / 2) {
|
|
CL_WriteRecordDemoMessage (&buf, seq++);
|
|
SZ_Clear (&buf);
|
|
}
|
|
}
|
|
|
|
// spawnstaticsound
|
|
// static sounds are skipped in demos, life is hard
|
|
|
|
// baselines
|
|
memset (&blankes, 0, sizeof (blankes));
|
|
for (i = 0; i < MAX_EDICTS; i++) {
|
|
es = qw_entstates.baseline + i;
|
|
|
|
if (memcmp (es, &blankes, sizeof (blankes))) {
|
|
MSG_WriteByte (&buf, svc_spawnbaseline);
|
|
MSG_WriteShort (&buf, i);
|
|
|
|
MSG_WriteByte (&buf, es->modelindex);
|
|
MSG_WriteByte (&buf, es->frame);
|
|
MSG_WriteByte (&buf, es->colormap);
|
|
MSG_WriteByte (&buf, es->skinnum);
|
|
MSG_WriteCoordAngleV (&buf, (vec_t*)&es->origin, es->angles);//FIXME
|
|
|
|
if (buf.cursize > MAX_MSGLEN / 2) {
|
|
CL_WriteRecordDemoMessage (&buf, seq++);
|
|
SZ_Clear (&buf);
|
|
}
|
|
}
|
|
}
|
|
|
|
MSG_WriteByte (&buf, svc_stufftext);
|
|
MSG_WriteString (&buf, va (0, "cmd spawn %i 0\n", cl.servercount));
|
|
|
|
if (buf.cursize) {
|
|
CL_WriteRecordDemoMessage (&buf, seq++);
|
|
SZ_Clear (&buf);
|
|
}
|
|
// send current status of all other players
|
|
|
|
for (i = 0; i < MAX_CLIENTS; i++) {
|
|
player = cl.players + i;
|
|
if (!player->userinfo)
|
|
continue;
|
|
|
|
MSG_WriteByte (&buf, svc_updatefrags);
|
|
MSG_WriteByte (&buf, i);
|
|
MSG_WriteShort (&buf, player->frags);
|
|
|
|
MSG_WriteByte (&buf, svc_updateping);
|
|
MSG_WriteByte (&buf, i);
|
|
MSG_WriteShort (&buf, player->ping);
|
|
|
|
MSG_WriteByte (&buf, svc_updatepl);
|
|
MSG_WriteByte (&buf, i);
|
|
MSG_WriteByte (&buf, player->pl);
|
|
|
|
MSG_WriteByte (&buf, svc_updateentertime);
|
|
MSG_WriteByte (&buf, i);
|
|
MSG_WriteFloat (&buf, realtime - player->entertime);
|
|
|
|
MSG_WriteByte (&buf, svc_updateuserinfo);
|
|
MSG_WriteByte (&buf, i);
|
|
MSG_WriteLong (&buf, player->userid);
|
|
MSG_WriteString (&buf, Info_MakeString (player->userinfo, 0));
|
|
|
|
if (buf.cursize > MAX_MSGLEN / 2) {
|
|
CL_WriteRecordDemoMessage (&buf, seq++);
|
|
SZ_Clear (&buf);
|
|
}
|
|
}
|
|
|
|
// send all current light styles
|
|
for (i = 0; i < MAX_LIGHTSTYLES; i++) {
|
|
MSG_WriteByte (&buf, svc_lightstyle);
|
|
MSG_WriteByte (&buf, (char) i);
|
|
MSG_WriteString (&buf, cl.lightstyle[i].map);
|
|
}
|
|
|
|
for (i = 0; i < MAX_CL_STATS; i++) {
|
|
MSG_WriteByte (&buf, svc_updatestatlong);
|
|
MSG_WriteByte (&buf, i);
|
|
MSG_WriteLong (&buf, cl.stats[i]);
|
|
if (buf.cursize > MAX_MSGLEN / 2) {
|
|
CL_WriteRecordDemoMessage (&buf, seq++);
|
|
SZ_Clear (&buf);
|
|
}
|
|
}
|
|
|
|
// get the client to check and download skins
|
|
// when that is completed, a begin command will be issued
|
|
MSG_WriteByte (&buf, svc_stufftext);
|
|
MSG_WriteString (&buf, va (0, "skins\n"));
|
|
|
|
CL_WriteRecordDemoMessage (&buf, seq++);
|
|
|
|
CL_WriteSetDemoMessage ();
|
|
|
|
// done
|
|
}
|
|
|
|
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) {
|
|
QFS_DefaultExtension (name, ".qwd.gz");
|
|
cls.demofile = QFS_WOpen (name->str, demo_gzip);
|
|
} else
|
|
#endif
|
|
{
|
|
QFS_DefaultExtension (name, ".qwd");
|
|
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 inline uint32_t
|
|
get_ulong (const byte *buf)
|
|
{
|
|
return buf[0] | (buf[1] << 8) | (buf[2] << 16) | (buf[3] << 24);
|
|
}
|
|
|
|
static inline float
|
|
get_float (const byte *buf)
|
|
{
|
|
union {
|
|
uint32_t u;
|
|
float f;
|
|
} uf;
|
|
uf.u = get_ulong (buf);
|
|
return uf.f;
|
|
}
|
|
|
|
static int
|
|
demo_check_qwd_mvd (void)
|
|
{
|
|
byte buf[22];
|
|
size_t bytes;
|
|
int c, ret = 0;
|
|
float f;
|
|
uint32_t u;
|
|
|
|
bytes = Qread (cls.demofile, buf, sizeof (buf));
|
|
if (bytes != sizeof (buf))
|
|
goto done;
|
|
|
|
if ((f = get_float (buf + 0)) >= 0 && !isinf (f) && !isnan (f)
|
|
&& buf[4] == dem_read && get_ulong (buf + 5) <= MAX_DEMMSG
|
|
&& get_ulong (buf + 9) == 1 && get_ulong (buf + 13) == 1
|
|
&& buf[17] == svc_serverdata
|
|
&& (u = get_ulong (buf + 18)) >= 26 && u <= PROTOCOL_VERSION) {
|
|
ret = 1;
|
|
goto done;
|
|
}
|
|
if ((f = get_float (buf + 0)) >= 0 && !isinf (f) && !isnan (f)
|
|
&& buf[4] == dem_read && get_ulong (buf + 5) == 5
|
|
&& get_ulong (buf + 9) == 0xffffffff && buf[13] == S2C_CONNECTION) {
|
|
//FIXME for now, assuming this is qwd (recorded by qizmo)
|
|
//the particular sequence seems to be:
|
|
// dem_read S2C_CONNECTION
|
|
// dem_cmd
|
|
// dem_read real start packet (with print, even:P)
|
|
ret = 1;
|
|
goto done;
|
|
}
|
|
if (buf[0] != 0 || (buf[1] != dem_read && buf[1] != dem_all)
|
|
|| (u = get_ulong (buf + 2)) > MAX_DEMMSG)
|
|
goto done;
|
|
Qseek (cls.demofile, 6, SEEK_SET);
|
|
net_message->message->cursize = u;
|
|
bytes = Qread (cls.demofile, net_message->message->data, u);
|
|
if (bytes != u)
|
|
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_serverdata:
|
|
if (MSG_ReadLong (net_message) != PROTOCOL_VERSION)
|
|
goto done;
|
|
ret = 2;
|
|
break;
|
|
default:
|
|
goto done;
|
|
}
|
|
}
|
|
done:
|
|
Qseek (cls.demofile, 0, SEEK_SET);
|
|
return ret;
|
|
}
|
|
|
|
static void
|
|
CL_StartDemo (void)
|
|
{
|
|
dstring_t *name;
|
|
int type;
|
|
|
|
// open the demo file
|
|
name = dstring_strdup (demoname->str);
|
|
|
|
QFS_DefaultExtension (name, ".mvd");
|
|
cls.demofile = QFS_FOpenFile (name->str);
|
|
if (!cls.demofile) {
|
|
dstring_copystr (name, demoname->str);
|
|
QFS_DefaultExtension (name, ".qwd");
|
|
cls.demofile = QFS_FOpenFile (name->str);
|
|
}
|
|
|
|
if (!cls.demofile) {
|
|
Sys_Printf ("ERROR: couldn't open.\n");
|
|
cls.demonum = -1; // stop demo loop
|
|
dstring_delete (name);
|
|
return;
|
|
}
|
|
if (!(type = demo_check_qwd_mvd ())) {
|
|
Sys_Printf ("%s is not a valid .qwd or .mvd file.\n", name->str);
|
|
cls.demonum = -1; // stop demo loop
|
|
dstring_delete (name);
|
|
Qclose (cls.demofile);
|
|
cls.demofile = 0;
|
|
return;
|
|
}
|
|
Sys_Printf ("Playing demo from %s.\n", name->str);
|
|
|
|
cls.demoplayback = true;
|
|
net_blocksend = 1;
|
|
if (type == 2) {
|
|
cls.demoplayback2 = true;
|
|
Sys_MaskPrintf (SYS_dev, "mvd\n");
|
|
} else {
|
|
Sys_MaskPrintf (SYS_dev, "qwd\n");
|
|
}
|
|
CL_SetState (ca_demostart);
|
|
Netchan_Setup (&cls.netchan, net_from, 0, NC_QPORT_SEND);
|
|
realtime = 0;
|
|
cls.findtrack = true;
|
|
cls.lasttype = 0;
|
|
cls.lastto = 0;
|
|
cls.prevtime = 0;
|
|
cls.basetime = 0;
|
|
demotime_cached = 0;
|
|
nextdemotime = 0;
|
|
CL_ClearPredict ();
|
|
|
|
dstring_delete (name);
|
|
}
|
|
|
|
/*
|
|
CL_PlayDemo_f
|
|
|
|
play [demoname]
|
|
*/
|
|
static void
|
|
CL_PlayDemo_f (void)
|
|
{
|
|
switch (Cmd_Argc ()) {
|
|
case 1:
|
|
if (!demoname->str[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
|
|
// disconnect from server
|
|
CL_Disconnect ();
|
|
|
|
if (Cmd_Argc () > 1)
|
|
dstring_copystr (demoname, Cmd_Argv (1));
|
|
CL_StartDemo ();
|
|
}
|
|
|
|
static void
|
|
CL_StartTimeDemo (void)
|
|
{
|
|
CL_StartDemo ();
|
|
|
|
if (cls.state != ca_demostart)
|
|
return;
|
|
|
|
// 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)
|
|
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)
|
|
Cbuf_InsertText (cl_cbuf, "quit\n");
|
|
}
|
|
}
|
|
|
|
/*
|
|
CL_TimeDemo_f
|
|
|
|
timedemo [demoname]
|
|
*/
|
|
static void
|
|
CL_TimeDemo_f (void)
|
|
{
|
|
int count = 1;
|
|
|
|
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
|
|
// disconnect from server
|
|
CL_Disconnect ();
|
|
|
|
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));
|
|
dstring_copystr (demoname, Cmd_Argv (1));
|
|
CL_StartTimeDemo ();
|
|
timedemo_runs = timedemo_count = max (count, 1);
|
|
timedemo_data = calloc (timedemo_runs, sizeof (td_stats_t));
|
|
}
|
|
|
|
void
|
|
CL_Demo_Init (void)
|
|
{
|
|
demoname = dstring_newstr ();
|
|
|
|
demo_timeframes_isactive = 0;
|
|
demo_timeframes_index = 0;
|
|
demo_timeframes_array = NULL;
|
|
|
|
Cvar_Register (&demo_gzip_cvar, 0, 0);
|
|
Cvar_Register (&demo_speed_cvar, 0, 0);
|
|
Cvar_Register (&demo_quit_cvar, 0, 0);
|
|
Cvar_Register (&demo_timeframes_cvar, 0, 0);
|
|
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 ("rerecord", CL_ReRecord_f, "Rerecord a demo on the same "
|
|
"server");
|
|
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);
|
|
}
|