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