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