]> git.xonotic.org Git - xonotic/darkplaces.git/blob - cl_demo.c
Merge branch 'master' into doombringer
[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_isauto = false;
400         cls.demorecording = true;
401         cls.demo_lastcsprogssize = -1;
402         cls.demo_lastcsprogscrc = -1;
403 }
404
405 void CL_PlayDemo(const char *demo)
406 {
407         char name[MAX_QPATH];
408         int c;
409         qbool neg = false;
410         qfile_t *f;
411
412         // open the demo file
413         strlcpy (name, demo, sizeof (name));
414         FS_DefaultExtension (name, ".dem", sizeof (name));
415         f = FS_OpenVirtualFile(name, false);
416         if (!f)
417         {
418                 Con_Printf(CON_ERROR "ERROR: couldn't open %s.\n", name);
419                 cls.demonum = -1;               // stop demo loop
420                 return;
421         }
422
423         cls.demostarting = true;
424
425         // disconnect from server
426         CL_Disconnect();
427
428         // update networking ports (this is mainly just needed at startup)
429         NetConn_UpdateSockets();
430
431         cls.protocol = PROTOCOL_QUAKE;
432
433         Con_Printf("Playing demo %s.\n", name);
434         cls.demofile = f;
435         strlcpy(cls.demoname, name, sizeof(cls.demoname));
436
437         cls.demoplayback = true;
438         cls.state = ca_connected;
439         cls.forcetrack = 0;
440
441         while ((c = FS_Getc (cls.demofile)) != '\n')
442                 if (c == '-')
443                         neg = true;
444                 else
445                         cls.forcetrack = cls.forcetrack * 10 + (c - '0');
446
447         if (neg)
448                 cls.forcetrack = -cls.forcetrack;
449
450         cls.demostarting = false;
451 }
452
453 /*
454 ====================
455 CL_PlayDemo_f
456
457 playdemo [demoname]
458 ====================
459 */
460 void CL_PlayDemo_f(cmd_state_t *cmd)
461 {
462         if (Cmd_Argc(cmd) != 2)
463         {
464                 Con_Print("playdemo <demoname> : plays a demo\n");
465                 return;
466         }
467
468         CL_PlayDemo(Cmd_Argv(cmd, 1));
469 }
470
471 typedef struct
472 {
473         int frames;
474         double time, totalfpsavg;
475         double fpsmin, fpsavg, fpsmax;
476 }
477 benchmarkhistory_t;
478 static size_t doublecmp_offset;
479 static int doublecmp_withoffset(const void *a_, const void *b_)
480 {
481         const double *a = (const double *) ((const char *) a_ + doublecmp_offset);
482         const double *b = (const double *) ((const char *) b_ + doublecmp_offset);
483         if(*a > *b)
484                 return +1;
485         if(*a < *b)
486                 return -1;
487         return 0;
488 }
489
490 /*
491 ====================
492 CL_FinishTimeDemo
493
494 ====================
495 */
496 static void CL_FinishTimeDemo (void)
497 {
498         int frames;
499         int i;
500         double time, totalfpsavg;
501         double fpsmin, fpsavg, fpsmax; // report min/avg/max fps
502         static int benchmark_runs = 0;
503         char vabuf[1024];
504
505         cls.timedemo = host.restless = false;
506
507         frames = cls.td_frames;
508         time = host.realtime - cls.td_starttime;
509         totalfpsavg = time > 0 ? frames / time : 0;
510         fpsmin = cls.td_onesecondminfps;
511         fpsavg = cls.td_onesecondavgcount ? cls.td_onesecondavgfps / cls.td_onesecondavgcount : 0;
512         fpsmax = cls.td_onesecondmaxfps;
513         // LadyHavoc: timedemo now prints out 7 digits of fraction, and min/avg/max
514         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);
515         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);
516         if (Sys_CheckParm("-benchmark"))
517         {
518                 ++benchmark_runs;
519                 i = Sys_CheckParm("-benchmarkruns");
520                 if(i && i + 1 < sys.argc)
521                 {
522                         static benchmarkhistory_t *history = NULL;
523                         if(!history)
524                                 history = (benchmarkhistory_t *)Z_Malloc(sizeof(*history) * atoi(sys.argv[i + 1]));
525
526                         history[benchmark_runs - 1].frames = frames;
527                         history[benchmark_runs - 1].time = time;
528                         history[benchmark_runs - 1].totalfpsavg = totalfpsavg;
529                         history[benchmark_runs - 1].fpsmin = fpsmin;
530                         history[benchmark_runs - 1].fpsavg = fpsavg;
531                         history[benchmark_runs - 1].fpsmax = fpsmax;
532
533                         if(atoi(sys.argv[i + 1]) > benchmark_runs)
534                         {
535                                 // restart the benchmark
536                                 Cbuf_AddText(cmd_local, va(vabuf, sizeof(vabuf), "timedemo %s\n", cls.demoname));
537                                 // cannot execute here
538                         }
539                         else
540                         {
541                                 // print statistics
542                                 int first = Sys_CheckParm("-benchmarkruns_skipfirst") ? 1 : 0;
543                                 if(benchmark_runs > first)
544                                 {
545 #define DO_MIN(f) \
546                                         for(i = first; i < benchmark_runs; ++i) if((i == first) || (history[i].f < f)) f = history[i].f
547
548 #define DO_MAX(f) \
549                                         for(i = first; i < benchmark_runs; ++i) if((i == first) || (history[i].f > f)) f = history[i].f
550
551 #define DO_MED(f) \
552                                         doublecmp_offset = (char *)&history->f - (char *)history; \
553                                         qsort(history + first, benchmark_runs - first, sizeof(*history), doublecmp_withoffset); \
554                                         if((first + benchmark_runs) & 1) \
555                                                 f = history[(first + benchmark_runs - 1) / 2].f; \
556                                         else \
557                                                 f = (history[(first + benchmark_runs - 2) / 2].f + history[(first + benchmark_runs) / 2].f) / 2
558
559                                         DO_MIN(frames);
560                                         DO_MAX(time);
561                                         DO_MIN(totalfpsavg);
562                                         DO_MIN(fpsmin);
563                                         DO_MIN(fpsavg);
564                                         DO_MIN(fpsmax);
565                                         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);
566
567                                         DO_MED(frames);
568                                         DO_MED(time);
569                                         DO_MED(totalfpsavg);
570                                         DO_MED(fpsmin);
571                                         DO_MED(fpsavg);
572                                         DO_MED(fpsmax);
573                                         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);
574
575                                         DO_MAX(frames);
576                                         DO_MIN(time);
577                                         DO_MAX(totalfpsavg);
578                                         DO_MAX(fpsmin);
579                                         DO_MAX(fpsavg);
580                                         DO_MAX(fpsmax);
581                                         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);
582                                 }
583                                 Z_Free(history);
584                                 history = NULL;
585                                 host.state = host_shutdown;
586                         }
587                 }
588                 else
589                         host.state = host_shutdown;
590         }
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
625 /*
626 ===============================================================================
627
628 DEMO LOOP CONTROL
629
630 ===============================================================================
631 */
632
633
634 /*
635 ==================
636 CL_Startdemos_f
637 ==================
638 */
639 static void CL_Startdemos_f(cmd_state_t *cmd)
640 {
641         int             i, c;
642
643         if (cls.state == ca_dedicated || Sys_CheckParm("-listen") || Sys_CheckParm("-benchmark") || Sys_CheckParm("-demo") || Sys_CheckParm("-capturedemo"))
644                 return;
645
646         c = Cmd_Argc(cmd) - 1;
647         if (c > MAX_DEMOS)
648         {
649                 Con_Printf("Max %i demos in demoloop\n", MAX_DEMOS);
650                 c = MAX_DEMOS;
651         }
652         Con_DPrintf("%i demo(s) in loop\n", c);
653
654         for (i=1 ; i<c+1 ; i++)
655                 strlcpy (cls.demos[i-1], Cmd_Argv(cmd, i), sizeof (cls.demos[i-1]));
656
657         // LadyHavoc: clear the remaining slots
658         for (;i <= MAX_DEMOS;i++)
659                 cls.demos[i-1][0] = 0;
660
661         if (!sv.active && cls.demonum != -1 && !cls.demoplayback)
662         {
663                 cls.demonum = 0;
664                 CL_NextDemo ();
665         }
666         else
667                 cls.demonum = -1;
668 }
669
670
671 /*
672 ==================
673 CL_Demos_f
674
675 Return to looping demos
676 ==================
677 */
678 static void CL_Demos_f(cmd_state_t *cmd)
679 {
680         if (cls.state == ca_dedicated)
681                 return;
682         if (cls.demonum == -1)
683                 cls.demonum = 1;
684         CL_Disconnect();
685         CL_NextDemo();
686 }
687
688 /*
689 ==================
690 CL_Stopdemo_f
691
692 Return to looping demos
693 ==================
694 */
695 static void CL_Stopdemo_f(cmd_state_t *cmd)
696 {
697         if (!cls.demoplayback)
698                 return;
699         CL_Disconnect();
700 }
701
702 // LadyHavoc: pausedemo command
703 static void CL_PauseDemo_f(cmd_state_t *cmd)
704 {
705         cls.demopaused = !cls.demopaused;
706         if (cls.demopaused)
707                 Con_Print("Demo paused\n");
708         else
709                 Con_Print("Demo unpaused\n");
710 }
711
712 void CL_Demo_Init(void)
713 {
714         Cmd_AddCommand(CF_CLIENT, "record", CL_Record_f, "record a demo");
715         Cmd_AddCommand(CF_CLIENT, "stop", CL_Stop_f, "stop recording or playing a demo");
716         Cmd_AddCommand(CF_CLIENT, "playdemo", CL_PlayDemo_f, "watch a demo file");
717         Cmd_AddCommand(CF_CLIENT, "timedemo", CL_TimeDemo_f, "play back a demo as fast as possible and save statistics to benchmark.log");
718         Cmd_AddCommand(CF_CLIENT, "startdemos", CL_Startdemos_f, "start playing back the selected demos sequentially (used at end of startup script)");
719         Cmd_AddCommand(CF_CLIENT, "demos", CL_Demos_f, "restart looping demos defined by the last startdemos command");
720         Cmd_AddCommand(CF_CLIENT, "stopdemo", CL_Stopdemo_f, "stop playing or recording demo (like stop command) and return to looping demos");
721         // LadyHavoc: added pausedemo
722         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)");
723         Cvar_RegisterVariable (&cl_autodemo);
724         Cvar_RegisterVariable (&cl_autodemo_nameformat);
725         Cvar_RegisterVariable (&cl_autodemo_delete);
726 }