]> git.xonotic.org Git - xonotic/darkplaces.git/blob - cl_demo.c
cl_demo: Move demo playback functionality to a separate function
[xonotic/darkplaces.git] / cl_demo.c
1 /*
2 Copyright (C) 1996-1997 Id Software, Inc.
3
4 This program is free software; you can redistribute it and/or
5 modify it under the terms of the GNU General Public License
6 as published by the Free Software Foundation; either version 2
7 of the License, or (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12
13 See the GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
18
19 */
20
21 #include "quakedef.h"
22
23 #ifdef CONFIG_VIDEO_CAPTURE
24 extern cvar_t cl_capturevideo;
25 extern cvar_t cl_capturevideo_demo_stop;
26 #endif
27 int old_vsync = 0;
28
29 static void CL_FinishTimeDemo (void);
30
31 /*
32 ==============================================================================
33
34 DEMO CODE
35
36 When a demo is playing back, all outgoing network messages are skipped, and
37 incoming messages are read from the demo file.
38
39 Whenever cl.time gets past the last received message, another message is
40 read from the demo file.
41 ==============================================================================
42 */
43
44 /*
45 =====================
46 CL_NextDemo
47
48 Called to play the next demo in the demo loop
49 =====================
50 */
51 void CL_NextDemo (void)
52 {
53         char    str[MAX_INPUTLINE];
54
55         if (cls.demonum == -1)
56                 return;         // don't play demos
57
58         if (!cls.demos[cls.demonum][0] || cls.demonum == MAX_DEMOS)
59         {
60                 cls.demonum = 0;
61                 if (!cls.demos[cls.demonum][0])
62                 {
63                         Con_Print("No demos listed with startdemos\n");
64                         cls.demonum = -1;
65                         return;
66                 }
67         }
68
69         dpsnprintf (str, sizeof(str), "playdemo %s\n", cls.demos[cls.demonum]);
70         Cbuf_InsertText(cmd_local, str);
71         cls.demonum++;
72 }
73
74 /*
75 ==============
76 CL_StopPlayback
77
78 Called when a demo file runs out, or the user starts a game
79 ==============
80 */
81 // LadyHavoc: now called only by CL_Disconnect
82 void CL_StopPlayback (void)
83 {
84 #ifdef CONFIG_VIDEO_CAPTURE
85         if (cl_capturevideo_demo_stop.integer)
86                 Cvar_Set(&cvars_all, "cl_capturevideo", "0");
87 #endif
88
89         if (!cls.demoplayback)
90                 return;
91
92         FS_Close (cls.demofile);
93         cls.demoplayback = false;
94         cls.demofile = NULL;
95
96         if (cls.timedemo)
97                 CL_FinishTimeDemo ();
98
99         if (!cls.demostarting) // only quit if not starting another demo
100                 if (Sys_CheckParm("-demo") || Sys_CheckParm("-capturedemo"))
101                         host.state = host_shutdown;
102 }
103
104 /*
105 ====================
106 CL_WriteDemoMessage
107
108 Dumps the current net message, prefixed by the length and view angles
109 #====================
110 */
111 void CL_WriteDemoMessage (sizebuf_t *message)
112 {
113         int             len;
114         int             i;
115         float   f;
116
117         if (cls.demopaused) // LadyHavoc: pausedemo
118                 return;
119
120         len = LittleLong (message->cursize);
121         FS_Write (cls.demofile, &len, 4);
122         for (i=0 ; i<3 ; i++)
123         {
124                 f = LittleFloat (cl.viewangles[i]);
125                 FS_Write (cls.demofile, &f, 4);
126         }
127         FS_Write (cls.demofile, message->data, message->cursize);
128 }
129
130 /*
131 ====================
132 CL_CutDemo
133
134 Dumps the current demo to a buffer, and resets the demo to its starting point.
135 Used to insert csprogs.dat files as a download to the beginning of a demo file.
136 ====================
137 */
138 void CL_CutDemo (unsigned char **buf, fs_offset_t *filesize)
139 {
140         *buf = NULL;
141         *filesize = 0;
142
143         FS_Close(cls.demofile);
144         *buf = FS_LoadFile(cls.demoname, tempmempool, false, filesize);
145
146         // restart the demo recording
147         cls.demofile = FS_OpenRealFile(cls.demoname, "wb", false);
148         if(!cls.demofile)
149                 Sys_Error("failed to reopen the demo file");
150         FS_Printf(cls.demofile, "%i\n", cls.forcetrack);
151 }
152
153 /*
154 ====================
155 CL_PasteDemo
156
157 Adds the cut stuff back to the demo. Also frees the buffer.
158 Used to insert csprogs.dat files as a download to the beginning of a demo file.
159 ====================
160 */
161 void CL_PasteDemo (unsigned char **buf, fs_offset_t *filesize)
162 {
163         fs_offset_t startoffset = 0;
164
165         if(!*buf)
166                 return;
167
168         // skip cdtrack
169         while(startoffset < *filesize && ((char *)(*buf))[startoffset] != '\n')
170                 ++startoffset;
171         if(startoffset < *filesize)
172                 ++startoffset;
173
174         FS_Write(cls.demofile, *buf + startoffset, *filesize - startoffset);
175
176         Mem_Free(*buf);
177         *buf = NULL;
178         *filesize = 0;
179 }
180
181 /*
182 ====================
183 CL_ReadDemoMessage
184
185 Handles playback of demos
186 ====================
187 */
188 void CL_ReadDemoMessage(void)
189 {
190         int i;
191         float f;
192
193         if (!cls.demoplayback)
194                 return;
195
196         // LadyHavoc: pausedemo
197         if (cls.demopaused)
198                 return;
199
200         for (;;)
201         {
202                 // decide if it is time to grab the next message
203                 // always grab until fully connected
204                 if (cls.signon == SIGNONS)
205                 {
206                         if (cls.timedemo)
207                         {
208                                 cls.td_frames++;
209                                 cls.td_onesecondframes++;
210                                 // if this is the first official frame we can now grab the real
211                                 // td_starttime so the bogus time on the first frame doesn't
212                                 // count against the final report
213                                 if (cls.td_frames == 0)
214                                 {
215                                         cls.td_starttime = host.realtime;
216                                         cls.td_onesecondnexttime = cl.time + 1;
217                                         cls.td_onesecondrealtime = host.realtime;
218                                         cls.td_onesecondframes = 0;
219                                         cls.td_onesecondminfps = 0;
220                                         cls.td_onesecondmaxfps = 0;
221                                         cls.td_onesecondavgfps = 0;
222                                         cls.td_onesecondavgcount = 0;
223                                 }
224                                 if (cl.time >= cls.td_onesecondnexttime)
225                                 {
226                                         double fps = cls.td_onesecondframes / (host.realtime - cls.td_onesecondrealtime);
227                                         if (cls.td_onesecondavgcount == 0)
228                                         {
229                                                 cls.td_onesecondminfps = fps;
230                                                 cls.td_onesecondmaxfps = fps;
231                                         }
232                                         cls.td_onesecondrealtime = host.realtime;
233                                         cls.td_onesecondminfps = min(cls.td_onesecondminfps, fps);
234                                         cls.td_onesecondmaxfps = max(cls.td_onesecondmaxfps, fps);
235                                         cls.td_onesecondavgfps += fps;
236                                         cls.td_onesecondavgcount++;
237                                         cls.td_onesecondframes = 0;
238                                         cls.td_onesecondnexttime++;
239                                 }
240                         }
241                         else if (cl.time < cl.mtime[0])
242                         {
243                                 // don't need another message yet
244                                 return;
245                         }
246                 }
247
248                 // get the next message
249                 FS_Read(cls.demofile, &cl_message.cursize, 4);
250                 cl_message.cursize = LittleLong(cl_message.cursize);
251                 if(cl_message.cursize & DEMOMSG_CLIENT_TO_SERVER) // This is a client->server message! Ignore for now!
252                 {
253                         // skip over demo packet
254                         FS_Seek(cls.demofile, 12 + (cl_message.cursize & (~DEMOMSG_CLIENT_TO_SERVER)), SEEK_CUR);
255                         continue;
256                 }
257                 if (cl_message.cursize > cl_message.maxsize)
258                 {
259                         Con_Printf("Demo message (%i) > cl_message.maxsize (%i)", cl_message.cursize, cl_message.maxsize);
260                         cl_message.cursize = 0;
261                         CL_Disconnect();
262                         return;
263                 }
264                 VectorCopy(cl.mviewangles[0], cl.mviewangles[1]);
265                 for (i = 0;i < 3;i++)
266                 {
267                         FS_Read(cls.demofile, &f, 4);
268                         cl.mviewangles[0][i] = LittleFloat(f);
269                 }
270
271                 if (FS_Read(cls.demofile, cl_message.data, cl_message.cursize) == cl_message.cursize)
272                 {
273                         MSG_BeginReading(&cl_message);
274                         CL_ParseServerMessage();
275
276                         if (cls.signon != SIGNONS)
277                                 Cbuf_Execute((cmd_local)->cbuf); // immediately execute svc_stufftext if in the demo before connect!
278
279                         // In case the demo contains a "svc_disconnect" message
280                         if (!cls.demoplayback)
281                                 return;
282
283                         if (cls.timedemo)
284                                 return;
285                 }
286                 else
287                 {
288                         CL_Disconnect();
289                         return;
290                 }
291         }
292 }
293
294
295 /*
296 ====================
297 CL_Stop_f
298
299 stop recording a demo
300 ====================
301 */
302 void CL_Stop_f(cmd_state_t *cmd)
303 {
304         sizebuf_t buf;
305         unsigned char bufdata[64];
306
307         if (!cls.demorecording)
308         {
309                 Con_Print("Not recording a demo.\n");
310                 return;
311         }
312
313 // write a disconnect message to the demo file
314         // LadyHavoc: don't replace the cl_message when doing this
315         buf.data = bufdata;
316         buf.maxsize = sizeof(bufdata);
317         SZ_Clear(&buf);
318         MSG_WriteByte(&buf, svc_disconnect);
319         CL_WriteDemoMessage(&buf);
320
321 // finish up
322         if(cl_autodemo.integer && (cl_autodemo_delete.integer & 1))
323         {
324                 FS_RemoveOnClose(cls.demofile);
325                 Con_Print("Completed and deleted demo\n");
326         }
327         else
328                 Con_Print("Completed demo\n");
329         FS_Close (cls.demofile);
330         cls.demofile = NULL;
331         cls.demorecording = false;
332 }
333
334 /*
335 ====================
336 CL_Record_f
337
338 record <demoname> <map> [cd track]
339 ====================
340 */
341 void CL_Record_f(cmd_state_t *cmd)
342 {
343         int c, track;
344         char name[MAX_OSPATH];
345         char vabuf[1024];
346
347         c = Cmd_Argc(cmd);
348         if (c != 2 && c != 3 && c != 4)
349         {
350                 Con_Print("record <demoname> [<map> [cd track]]\n");
351                 return;
352         }
353
354         if (strstr(Cmd_Argv(cmd, 1), ".."))
355         {
356                 Con_Print("Relative pathnames are not allowed.\n");
357                 return;
358         }
359
360         if (c == 2 && cls.state == ca_connected)
361         {
362                 Con_Print("Can not record - already connected to server\nClient demo recording must be started before connecting\n");
363                 return;
364         }
365
366         if (cls.state == ca_connected)
367                 CL_Disconnect();
368
369         // write the forced cd track number, or -1
370         if (c == 4)
371         {
372                 track = atoi(Cmd_Argv(cmd, 3));
373                 Con_Printf("Forcing CD track to %i\n", cls.forcetrack);
374         }
375         else
376                 track = -1;
377
378         // get the demo name
379         strlcpy (name, Cmd_Argv(cmd, 1), sizeof (name));
380         FS_DefaultExtension (name, ".dem", sizeof (name));
381
382         // start the map up
383         if (c > 2)
384                 Cmd_ExecuteString ( cmd, va(vabuf, sizeof(vabuf), "map %s", Cmd_Argv(cmd, 2)), src_local, false);
385
386         // open the demo file
387         Con_Printf("recording to %s.\n", name);
388         cls.demofile = FS_OpenRealFile(name, "wb", false);
389         if (!cls.demofile)
390         {
391                 Con_Print(CON_ERROR "ERROR: couldn't open.\n");
392                 return;
393         }
394         strlcpy(cls.demoname, name, sizeof(cls.demoname));
395
396         cls.forcetrack = track;
397         FS_Printf(cls.demofile, "%i\n", cls.forcetrack);
398
399         cls.demorecording = true;
400         cls.demo_lastcsprogssize = -1;
401         cls.demo_lastcsprogscrc = -1;
402 }
403
404 void CL_PlayDemo(const char *demo)
405 {
406         char name[MAX_QPATH];
407         int c;
408         qbool neg = false;
409         qfile_t *f;
410
411         // open the demo file
412         strlcpy (name, demo, sizeof (name));
413         FS_DefaultExtension (name, ".dem", sizeof (name));
414         f = FS_OpenVirtualFile(name, false);
415         if (!f)
416         {
417                 Con_Printf(CON_ERROR "ERROR: couldn't open %s.\n", name);
418                 cls.demonum = -1;               // stop demo loop
419                 return;
420         }
421
422         cls.demostarting = true;
423
424         // disconnect from server
425         if(cls.state == ca_connected)
426                 CL_Disconnect();
427         if(sv.active)
428                 SV_Shutdown();
429
430         // update networking ports (this is mainly just needed at startup)
431         NetConn_UpdateSockets();
432
433         cls.protocol = PROTOCOL_QUAKE;
434
435         Con_Printf("Playing demo %s.\n", name);
436         cls.demofile = f;
437         strlcpy(cls.demoname, name, sizeof(cls.demoname));
438
439         cls.demoplayback = true;
440         cls.state = ca_connected;
441         cls.forcetrack = 0;
442
443         while ((c = FS_Getc (cls.demofile)) != '\n')
444                 if (c == '-')
445                         neg = true;
446                 else
447                         cls.forcetrack = cls.forcetrack * 10 + (c - '0');
448
449         if (neg)
450                 cls.forcetrack = -cls.forcetrack;
451
452         cls.demostarting = false;
453 }
454
455 /*
456 ====================
457 CL_PlayDemo_f
458
459 playdemo [demoname]
460 ====================
461 */
462 void CL_PlayDemo_f(cmd_state_t *cmd)
463 {
464         if (Cmd_Argc(cmd) != 2)
465         {
466                 Con_Print("playdemo <demoname> : plays a demo\n");
467                 return;
468         }
469
470         CL_PlayDemo(Cmd_Argv(cmd, 1));
471 }
472
473 typedef struct
474 {
475         int frames;
476         double time, totalfpsavg;
477         double fpsmin, fpsavg, fpsmax;
478 }
479 benchmarkhistory_t;
480 static size_t doublecmp_offset;
481 static int doublecmp_withoffset(const void *a_, const void *b_)
482 {
483         const double *a = (const double *) ((const char *) a_ + doublecmp_offset);
484         const double *b = (const double *) ((const char *) b_ + doublecmp_offset);
485         if(*a > *b)
486                 return +1;
487         if(*a < *b)
488                 return -1;
489         return 0;
490 }
491
492 /*
493 ====================
494 CL_FinishTimeDemo
495
496 ====================
497 */
498 static void CL_FinishTimeDemo (void)
499 {
500         int frames;
501         int i;
502         double time, totalfpsavg;
503         double fpsmin, fpsavg, fpsmax; // report min/avg/max fps
504         static int benchmark_runs = 0;
505         char vabuf[1024];
506
507         cls.timedemo = host.restless = false;
508
509         frames = cls.td_frames;
510         time = host.realtime - cls.td_starttime;
511         totalfpsavg = time > 0 ? frames / time : 0;
512         fpsmin = cls.td_onesecondminfps;
513         fpsavg = cls.td_onesecondavgcount ? cls.td_onesecondavgfps / cls.td_onesecondavgcount : 0;
514         fpsmax = cls.td_onesecondmaxfps;
515         // LadyHavoc: timedemo now prints out 7 digits of fraction, and min/avg/max
516         Con_Printf("%i frames %5.7f seconds %5.7f fps, one-second fps min/avg/max: %.0f %.0f %.0f (%i seconds)\n", frames, time, totalfpsavg, fpsmin, fpsavg, fpsmax, cls.td_onesecondavgcount);
517         Log_Printf("benchmark.log", "date %s | enginedate %s | demo %s | commandline %s | run %d | result %i frames %5.7f seconds %5.7f fps, one-second fps min/avg/max: %.0f %.0f %.0f (%i seconds)\n", Sys_TimeString("%Y-%m-%d %H:%M:%S"), buildstring, cls.demoname, cmdline.string, benchmark_runs + 1, frames, time, totalfpsavg, fpsmin, fpsavg, fpsmax, cls.td_onesecondavgcount);
518         if (Sys_CheckParm("-benchmark"))
519         {
520                 ++benchmark_runs;
521                 i = Sys_CheckParm("-benchmarkruns");
522                 if(i && i + 1 < sys.argc)
523                 {
524                         static benchmarkhistory_t *history = NULL;
525                         if(!history)
526                                 history = (benchmarkhistory_t *)Z_Malloc(sizeof(*history) * atoi(sys.argv[i + 1]));
527
528                         history[benchmark_runs - 1].frames = frames;
529                         history[benchmark_runs - 1].time = time;
530                         history[benchmark_runs - 1].totalfpsavg = totalfpsavg;
531                         history[benchmark_runs - 1].fpsmin = fpsmin;
532                         history[benchmark_runs - 1].fpsavg = fpsavg;
533                         history[benchmark_runs - 1].fpsmax = fpsmax;
534
535                         if(atoi(sys.argv[i + 1]) > benchmark_runs)
536                         {
537                                 // restart the benchmark
538                                 Cbuf_AddText(cmd_local, va(vabuf, sizeof(vabuf), "timedemo %s\n", cls.demoname));
539                                 // cannot execute here
540                         }
541                         else
542                         {
543                                 // print statistics
544                                 int first = Sys_CheckParm("-benchmarkruns_skipfirst") ? 1 : 0;
545                                 if(benchmark_runs > first)
546                                 {
547 #define DO_MIN(f) \
548                                         for(i = first; i < benchmark_runs; ++i) if((i == first) || (history[i].f < f)) f = history[i].f
549
550 #define DO_MAX(f) \
551                                         for(i = first; i < benchmark_runs; ++i) if((i == first) || (history[i].f > f)) f = history[i].f
552
553 #define DO_MED(f) \
554                                         doublecmp_offset = (char *)&history->f - (char *)history; \
555                                         qsort(history + first, benchmark_runs - first, sizeof(*history), doublecmp_withoffset); \
556                                         if((first + benchmark_runs) & 1) \
557                                                 f = history[(first + benchmark_runs - 1) / 2].f; \
558                                         else \
559                                                 f = (history[(first + benchmark_runs - 2) / 2].f + history[(first + benchmark_runs) / 2].f) / 2
560
561                                         DO_MIN(frames);
562                                         DO_MAX(time);
563                                         DO_MIN(totalfpsavg);
564                                         DO_MIN(fpsmin);
565                                         DO_MIN(fpsavg);
566                                         DO_MIN(fpsmax);
567                                         Con_Printf("MIN: %i frames %5.7f seconds %5.7f fps, one-second fps min/avg/max: %.0f %.0f %.0f (%i seconds)\n", frames, time, totalfpsavg, fpsmin, fpsavg, fpsmax, cls.td_onesecondavgcount);
568
569                                         DO_MED(frames);
570                                         DO_MED(time);
571                                         DO_MED(totalfpsavg);
572                                         DO_MED(fpsmin);
573                                         DO_MED(fpsavg);
574                                         DO_MED(fpsmax);
575                                         Con_Printf("MED: %i frames %5.7f seconds %5.7f fps, one-second fps min/avg/max: %.0f %.0f %.0f (%i seconds)\n", frames, time, totalfpsavg, fpsmin, fpsavg, fpsmax, cls.td_onesecondavgcount);
576
577                                         DO_MAX(frames);
578                                         DO_MIN(time);
579                                         DO_MAX(totalfpsavg);
580                                         DO_MAX(fpsmin);
581                                         DO_MAX(fpsavg);
582                                         DO_MAX(fpsmax);
583                                         Con_Printf("MAX: %i frames %5.7f seconds %5.7f fps, one-second fps min/avg/max: %.0f %.0f %.0f (%i seconds)\n", frames, time, totalfpsavg, fpsmin, fpsavg, fpsmax, cls.td_onesecondavgcount);
584                                 }
585                                 Z_Free(history);
586                                 history = NULL;
587                                 host.state = host_shutdown;
588                         }
589                 }
590                 else
591                         host.state = host_shutdown;
592         }
593 }
594
595 /*
596 ====================
597 CL_TimeDemo_f
598
599 timedemo [demoname]
600 ====================
601 */
602 void CL_TimeDemo_f(cmd_state_t *cmd)
603 {
604         if (Cmd_Argc(cmd) != 2)
605         {
606                 Con_Print("timedemo <demoname> : gets demo speeds\n");
607                 return;
608         }
609
610         srand(0); // predictable random sequence for benchmarking
611
612         CL_PlayDemo(Cmd_Argv(cmd, 1));
613
614 // cls.td_starttime will be grabbed at the second frame of the demo, so
615 // all the loading time doesn't get counted
616
617         // instantly hide console and deactivate it
618         key_dest = key_game;
619         key_consoleactive = 0;
620         scr_con_current = 0;
621
622         cls.timedemo = host.restless = true;
623         cls.td_frames = -2;             // skip the first frame
624         cls.demonum = -1;               // stop demo loop
625 }
626
627 /*
628 ===============================================================================
629
630 DEMO LOOP CONTROL
631
632 ===============================================================================
633 */
634
635
636 /*
637 ==================
638 CL_Startdemos_f
639 ==================
640 */
641 static void CL_Startdemos_f(cmd_state_t *cmd)
642 {
643         int             i, c;
644
645         if (cls.state == ca_dedicated || Sys_CheckParm("-listen") || Sys_CheckParm("-benchmark") || Sys_CheckParm("-demo") || Sys_CheckParm("-capturedemo"))
646                 return;
647
648         c = Cmd_Argc(cmd) - 1;
649         if (c > MAX_DEMOS)
650         {
651                 Con_Printf("Max %i demos in demoloop\n", MAX_DEMOS);
652                 c = MAX_DEMOS;
653         }
654         Con_DPrintf("%i demo(s) in loop\n", c);
655
656         for (i=1 ; i<c+1 ; i++)
657                 strlcpy (cls.demos[i-1], Cmd_Argv(cmd, i), sizeof (cls.demos[i-1]));
658
659         // LadyHavoc: clear the remaining slots
660         for (;i <= MAX_DEMOS;i++)
661                 cls.demos[i-1][0] = 0;
662
663         if (!sv.active && cls.demonum != -1 && !cls.demoplayback)
664         {
665                 cls.demonum = 0;
666                 CL_NextDemo ();
667         }
668         else
669                 cls.demonum = -1;
670 }
671
672
673 /*
674 ==================
675 CL_Demos_f
676
677 Return to looping demos
678 ==================
679 */
680 static void CL_Demos_f(cmd_state_t *cmd)
681 {
682         if (cls.state == ca_dedicated)
683                 return;
684         if (cls.demonum == -1)
685                 cls.demonum = 1;
686         CL_Disconnect_f (cmd);
687         CL_NextDemo ();
688 }
689
690 /*
691 ==================
692 CL_Stopdemo_f
693
694 Return to looping demos
695 ==================
696 */
697 static void CL_Stopdemo_f(cmd_state_t *cmd)
698 {
699         if (!cls.demoplayback)
700                 return;
701         CL_Disconnect();
702 }
703
704 // LadyHavoc: pausedemo command
705 static void CL_PauseDemo_f(cmd_state_t *cmd)
706 {
707         cls.demopaused = !cls.demopaused;
708         if (cls.demopaused)
709                 Con_Print("Demo paused\n");
710         else
711                 Con_Print("Demo unpaused\n");
712 }
713
714 void CL_Demo_Init(void)
715 {
716         Cmd_AddCommand(CF_CLIENT, "record", CL_Record_f, "record a demo");
717         Cmd_AddCommand(CF_CLIENT, "stop", CL_Stop_f, "stop recording or playing a demo");
718         Cmd_AddCommand(CF_CLIENT, "playdemo", CL_PlayDemo_f, "watch a demo file");
719         Cmd_AddCommand(CF_CLIENT, "timedemo", CL_TimeDemo_f, "play back a demo as fast as possible and save statistics to benchmark.log");
720         Cmd_AddCommand(CF_CLIENT, "startdemos", CL_Startdemos_f, "start playing back the selected demos sequentially (used at end of startup script)");
721         Cmd_AddCommand(CF_CLIENT, "demos", CL_Demos_f, "restart looping demos defined by the last startdemos command");
722         Cmd_AddCommand(CF_CLIENT, "stopdemo", CL_Stopdemo_f, "stop playing or recording demo (like stop command) and return to looping demos");
723         // LadyHavoc: added pausedemo
724         Cmd_AddCommand(CF_CLIENT, "pausedemo", CL_PauseDemo_f, "pause demo playback (can also safely pause demo recording if using QUAKE, QUAKEDP or NEHAHRAMOVIE protocol, useful for making movies)");
725         Cvar_RegisterVariable (&cl_autodemo);
726         Cvar_RegisterVariable (&cl_autodemo_nameformat);
727         Cvar_RegisterVariable (&cl_autodemo_delete);
728 }