]> git.xonotic.org Git - xonotic/darkplaces.git/blob - cl_demo.c
host: Add two hooks for disconnecting and toggling the menu
[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_client, 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         cls.demonum = -1;
104
105 }
106
107 /*
108 ====================
109 CL_WriteDemoMessage
110
111 Dumps the current net message, prefixed by the length and view angles
112 #====================
113 */
114 void CL_WriteDemoMessage (sizebuf_t *message)
115 {
116         int             len;
117         int             i;
118         float   f;
119
120         if (cls.demopaused) // LadyHavoc: pausedemo
121                 return;
122
123         len = LittleLong (message->cursize);
124         FS_Write (cls.demofile, &len, 4);
125         for (i=0 ; i<3 ; i++)
126         {
127                 f = LittleFloat (cl.viewangles[i]);
128                 FS_Write (cls.demofile, &f, 4);
129         }
130         FS_Write (cls.demofile, message->data, message->cursize);
131 }
132
133 /*
134 ====================
135 CL_CutDemo
136
137 Dumps the current demo to a buffer, and resets the demo to its starting point.
138 Used to insert csprogs.dat files as a download to the beginning of a demo file.
139 ====================
140 */
141 void CL_CutDemo (unsigned char **buf, fs_offset_t *filesize)
142 {
143         *buf = NULL;
144         *filesize = 0;
145
146         FS_Close(cls.demofile);
147         *buf = FS_LoadFile(cls.demoname, tempmempool, false, filesize);
148
149         // restart the demo recording
150         cls.demofile = FS_OpenRealFile(cls.demoname, "wb", false);
151         if(!cls.demofile)
152                 Sys_Error("failed to reopen the demo file");
153         FS_Printf(cls.demofile, "%i\n", cls.forcetrack);
154 }
155
156 /*
157 ====================
158 CL_PasteDemo
159
160 Adds the cut stuff back to the demo. Also frees the buffer.
161 Used to insert csprogs.dat files as a download to the beginning of a demo file.
162 ====================
163 */
164 void CL_PasteDemo (unsigned char **buf, fs_offset_t *filesize)
165 {
166         fs_offset_t startoffset = 0;
167
168         if(!*buf)
169                 return;
170
171         // skip cdtrack
172         while(startoffset < *filesize && ((char *)(*buf))[startoffset] != '\n')
173                 ++startoffset;
174         if(startoffset < *filesize)
175                 ++startoffset;
176
177         FS_Write(cls.demofile, *buf + startoffset, *filesize - startoffset);
178
179         Mem_Free(*buf);
180         *buf = NULL;
181         *filesize = 0;
182 }
183
184 /*
185 ====================
186 CL_ReadDemoMessage
187
188 Handles playback of demos
189 ====================
190 */
191 void CL_ReadDemoMessage(void)
192 {
193         int i;
194         float f;
195
196         if (!cls.demoplayback)
197                 return;
198
199         // LadyHavoc: pausedemo
200         if (cls.demopaused)
201                 return;
202
203         for (;;)
204         {
205                 // decide if it is time to grab the next message
206                 // always grab until fully connected
207                 if (cls.signon == SIGNONS)
208                 {
209                         if (cls.timedemo)
210                         {
211                                 cls.td_frames++;
212                                 cls.td_onesecondframes++;
213                                 // if this is the first official frame we can now grab the real
214                                 // td_starttime so the bogus time on the first frame doesn't
215                                 // count against the final report
216                                 if (cls.td_frames == 0)
217                                 {
218                                         cls.td_starttime = host.realtime;
219                                         cls.td_onesecondnexttime = cl.time + 1;
220                                         cls.td_onesecondrealtime = host.realtime;
221                                         cls.td_onesecondframes = 0;
222                                         cls.td_onesecondminfps = 0;
223                                         cls.td_onesecondmaxfps = 0;
224                                         cls.td_onesecondavgfps = 0;
225                                         cls.td_onesecondavgcount = 0;
226                                 }
227                                 if (cl.time >= cls.td_onesecondnexttime)
228                                 {
229                                         double fps = cls.td_onesecondframes / (host.realtime - cls.td_onesecondrealtime);
230                                         if (cls.td_onesecondavgcount == 0)
231                                         {
232                                                 cls.td_onesecondminfps = fps;
233                                                 cls.td_onesecondmaxfps = fps;
234                                         }
235                                         cls.td_onesecondrealtime = host.realtime;
236                                         cls.td_onesecondminfps = min(cls.td_onesecondminfps, fps);
237                                         cls.td_onesecondmaxfps = max(cls.td_onesecondmaxfps, fps);
238                                         cls.td_onesecondavgfps += fps;
239                                         cls.td_onesecondavgcount++;
240                                         cls.td_onesecondframes = 0;
241                                         cls.td_onesecondnexttime++;
242                                 }
243                         }
244                         else if (cl.time < cl.mtime[0])
245                         {
246                                 // don't need another message yet
247                                 return;
248                         }
249                 }
250
251                 // get the next message
252                 FS_Read(cls.demofile, &cl_message.cursize, 4);
253                 cl_message.cursize = LittleLong(cl_message.cursize);
254                 if(cl_message.cursize & DEMOMSG_CLIENT_TO_SERVER) // This is a client->server message! Ignore for now!
255                 {
256                         // skip over demo packet
257                         FS_Seek(cls.demofile, 12 + (cl_message.cursize & (~DEMOMSG_CLIENT_TO_SERVER)), SEEK_CUR);
258                         continue;
259                 }
260                 if (cl_message.cursize > cl_message.maxsize)
261                 {
262                         Con_Printf("Demo message (%i) > cl_message.maxsize (%i)", cl_message.cursize, cl_message.maxsize);
263                         cl_message.cursize = 0;
264                         CL_Disconnect();
265                         return;
266                 }
267                 VectorCopy(cl.mviewangles[0], cl.mviewangles[1]);
268                 for (i = 0;i < 3;i++)
269                 {
270                         FS_Read(cls.demofile, &f, 4);
271                         cl.mviewangles[0][i] = LittleFloat(f);
272                 }
273
274                 if (FS_Read(cls.demofile, cl_message.data, cl_message.cursize) == cl_message.cursize)
275                 {
276                         MSG_BeginReading(&cl_message);
277                         CL_ParseServerMessage();
278
279                         if (cls.signon != SIGNONS)
280                                 Cbuf_Execute((&cmd_client)->cbuf); // immediately execute svc_stufftext if in the demo before connect!
281
282                         // In case the demo contains a "svc_disconnect" message
283                         if (!cls.demoplayback)
284                                 return;
285
286                         if (cls.timedemo)
287                                 return;
288                 }
289                 else
290                 {
291                         CL_Disconnect();
292                         return;
293                 }
294         }
295 }
296
297
298 /*
299 ====================
300 CL_Stop_f
301
302 stop recording a demo
303 ====================
304 */
305 void CL_Stop_f(cmd_state_t *cmd)
306 {
307         sizebuf_t buf;
308         unsigned char bufdata[64];
309
310         if (!cls.demorecording)
311         {
312                 Con_Print("Not recording a demo.\n");
313                 return;
314         }
315
316 // write a disconnect message to the demo file
317         // LadyHavoc: don't replace the cl_message when doing this
318         buf.data = bufdata;
319         buf.maxsize = sizeof(bufdata);
320         SZ_Clear(&buf);
321         MSG_WriteByte(&buf, svc_disconnect);
322         CL_WriteDemoMessage(&buf);
323
324 // finish up
325         if(cl_autodemo.integer && (cl_autodemo_delete.integer & 1))
326         {
327                 FS_RemoveOnClose(cls.demofile);
328                 Con_Print("Completed and deleted demo\n");
329         }
330         else
331                 Con_Print("Completed demo\n");
332         FS_Close (cls.demofile);
333         cls.demofile = NULL;
334         cls.demorecording = false;
335 }
336
337 /*
338 ====================
339 CL_Record_f
340
341 record <demoname> <map> [cd track]
342 ====================
343 */
344 void CL_Record_f(cmd_state_t *cmd)
345 {
346         int c, track;
347         char name[MAX_OSPATH];
348         char vabuf[1024];
349
350         c = Cmd_Argc(cmd);
351         if (c != 2 && c != 3 && c != 4)
352         {
353                 Con_Print("record <demoname> [<map> [cd track]]\n");
354                 return;
355         }
356
357         if (strstr(Cmd_Argv(cmd, 1), ".."))
358         {
359                 Con_Print("Relative pathnames are not allowed.\n");
360                 return;
361         }
362
363         if (c == 2 && cls.state == ca_connected)
364         {
365                 Con_Print("Can not record - already connected to server\nClient demo recording must be started before connecting\n");
366                 return;
367         }
368
369         if (cls.state == ca_connected)
370                 CL_Disconnect();
371
372         // write the forced cd track number, or -1
373         if (c == 4)
374         {
375                 track = atoi(Cmd_Argv(cmd, 3));
376                 Con_Printf("Forcing CD track to %i\n", cls.forcetrack);
377         }
378         else
379                 track = -1;
380
381         // get the demo name
382         strlcpy (name, Cmd_Argv(cmd, 1), sizeof (name));
383         FS_DefaultExtension (name, ".dem", sizeof (name));
384
385         // start the map up
386         if (c > 2)
387                 Cmd_ExecuteString ( cmd, va(vabuf, sizeof(vabuf), "map %s", Cmd_Argv(cmd, 2)), src_local, false);
388
389         // open the demo file
390         Con_Printf("recording to %s.\n", name);
391         cls.demofile = FS_OpenRealFile(name, "wb", false);
392         if (!cls.demofile)
393         {
394                 Con_Print(CON_ERROR "ERROR: couldn't open.\n");
395                 return;
396         }
397         strlcpy(cls.demoname, name, sizeof(cls.demoname));
398
399         cls.forcetrack = track;
400         FS_Printf(cls.demofile, "%i\n", cls.forcetrack);
401
402         cls.demorecording = true;
403         cls.demo_lastcsprogssize = -1;
404         cls.demo_lastcsprogscrc = -1;
405 }
406
407
408 /*
409 ====================
410 CL_PlayDemo_f
411
412 play [demoname]
413 ====================
414 */
415 void CL_PlayDemo_f(cmd_state_t *cmd)
416 {
417         char    name[MAX_QPATH];
418         int c;
419         qbool neg = false;
420         qfile_t *f;
421
422         if (Cmd_Argc(cmd) != 2)
423         {
424                 Con_Print("play <demoname> : plays a demo\n");
425                 return;
426         }
427
428         // open the demo file
429         strlcpy (name, Cmd_Argv(cmd, 1), sizeof (name));
430         FS_DefaultExtension (name, ".dem", sizeof (name));
431         f = FS_OpenVirtualFile(name, false);
432         if (!f)
433         {
434                 Con_Printf(CON_ERROR "ERROR: couldn't open %s.\n", name);
435                 cls.demonum = -1;               // stop demo loop
436                 return;
437         }
438
439         cls.demostarting = true;
440
441         // disconnect from server
442         CL_Disconnect ();
443         SV_Shutdown ();
444
445         // update networking ports (this is mainly just needed at startup)
446         NetConn_UpdateSockets();
447
448         cls.protocol = PROTOCOL_QUAKE;
449
450         Con_Printf("Playing demo %s.\n", name);
451         cls.demofile = f;
452         strlcpy(cls.demoname, name, sizeof(cls.demoname));
453
454         cls.demoplayback = true;
455         cls.state = ca_connected;
456         cls.forcetrack = 0;
457
458         while ((c = FS_Getc (cls.demofile)) != '\n')
459                 if (c == '-')
460                         neg = true;
461                 else
462                         cls.forcetrack = cls.forcetrack * 10 + (c - '0');
463
464         if (neg)
465                 cls.forcetrack = -cls.forcetrack;
466
467         cls.demostarting = false;
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_client, 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_f (cmd);
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_f (cmd);
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         SV_Shutdown ();
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 }