protocol/dp8: Implement parting messages
[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                         CL_Disconnect(false, "Demo message (%i) > cl_message.maxsize (%i)", cl_message.cursize, cl_message.maxsize);
260                         cl_message.cursize = 0;
261                         return;
262                 }
263                 VectorCopy(cl.mviewangles[0], cl.mviewangles[1]);
264                 for (i = 0;i < 3;i++)
265                 {
266                         FS_Read(cls.demofile, &f, 4);
267                         cl.mviewangles[0][i] = LittleFloat(f);
268                 }
269
270                 if (FS_Read(cls.demofile, cl_message.data, cl_message.cursize) == cl_message.cursize)
271                 {
272                         MSG_BeginReading(&cl_message);
273                         CL_ParseServerMessage();
274
275                         if (cls.signon != SIGNONS)
276                                 Cbuf_Execute((cmd_local)->cbuf); // immediately execute svc_stufftext if in the demo before connect!
277
278                         // In case the demo contains a "svc_disconnect" message
279                         if (!cls.demoplayback)
280                                 return;
281
282                         if (cls.timedemo)
283                                 return;
284                 }
285                 else
286                 {
287                         CL_Disconnect(false, NULL);
288                         return;
289                 }
290         }
291 }
292
293
294 /*
295 ====================
296 CL_Stop_f
297
298 stop recording a demo
299 ====================
300 */
301 void CL_Stop_f(cmd_state_t *cmd)
302 {
303         sizebuf_t buf;
304         unsigned char bufdata[64];
305
306         if (!cls.demorecording)
307         {
308                 Con_Print("Not recording a demo.\n");
309                 return;
310         }
311
312 // write a disconnect message to the demo file
313         // LadyHavoc: don't replace the cl_message when doing this
314         buf.data = bufdata;
315         buf.maxsize = sizeof(bufdata);
316         SZ_Clear(&buf);
317         MSG_WriteByte(&buf, svc_disconnect);
318         CL_WriteDemoMessage(&buf);
319
320 // finish up
321         if(cl_autodemo.integer && (cl_autodemo_delete.integer & 1))
322         {
323                 FS_RemoveOnClose(cls.demofile);
324                 Con_Print("Completed and deleted demo\n");
325         }
326         else
327                 Con_Print("Completed demo\n");
328         FS_Close (cls.demofile);
329         cls.demofile = NULL;
330         cls.demorecording = false;
331 }
332
333 /*
334 ====================
335 CL_Record_f
336
337 record <demoname> <map> [cd track]
338 ====================
339 */
340 void CL_Record_f(cmd_state_t *cmd)
341 {
342         int c, track;
343         char name[MAX_OSPATH];
344         char vabuf[1024];
345
346         c = Cmd_Argc(cmd);
347         if (c != 2 && c != 3 && c != 4)
348         {
349                 Con_Print("record <demoname> [<map> [cd track]]\n");
350                 return;
351         }
352
353         if (strstr(Cmd_Argv(cmd, 1), ".."))
354         {
355                 Con_Print("Relative pathnames are not allowed.\n");
356                 return;
357         }
358
359         if (c == 2 && cls.state == ca_connected)
360         {
361                 Con_Print("Can not record - already connected to server\nClient demo recording must be started before connecting\n");
362                 return;
363         }
364
365         if (cls.state == ca_connected)
366                 CL_Disconnect(false, NULL);
367
368         // write the forced cd track number, or -1
369         if (c == 4)
370         {
371                 track = atoi(Cmd_Argv(cmd, 3));
372                 Con_Printf("Forcing CD track to %i\n", cls.forcetrack);
373         }
374         else
375                 track = -1;
376
377         // get the demo name
378         strlcpy (name, Cmd_Argv(cmd, 1), sizeof (name));
379         FS_DefaultExtension (name, ".dem", sizeof (name));
380
381         // start the map up
382         if (c > 2)
383                 Cmd_ExecuteString ( cmd, va(vabuf, sizeof(vabuf), "map %s", Cmd_Argv(cmd, 2)), src_local, false);
384
385         // open the demo file
386         Con_Printf("recording to %s.\n", name);
387         cls.demofile = FS_OpenRealFile(name, "wb", false);
388         if (!cls.demofile)
389         {
390                 Con_Print(CON_ERROR "ERROR: couldn't open.\n");
391                 return;
392         }
393         strlcpy(cls.demoname, name, sizeof(cls.demoname));
394
395         cls.forcetrack = track;
396         FS_Printf(cls.demofile, "%i\n", cls.forcetrack);
397
398         cls.demorecording = true;
399         cls.demo_lastcsprogssize = -1;
400         cls.demo_lastcsprogscrc = -1;
401 }
402
403 void CL_PlayDemo(const char *demo)
404 {
405         char name[MAX_QPATH];
406         int c;
407         qbool neg = false;
408         qfile_t *f;
409
410         // open the demo file
411         strlcpy (name, demo, sizeof (name));
412         FS_DefaultExtension (name, ".dem", sizeof (name));
413         f = FS_OpenVirtualFile(name, false);
414         if (!f)
415         {
416                 Con_Printf(CON_ERROR "ERROR: couldn't open %s.\n", name);
417                 cls.demonum = -1;               // stop demo loop
418                 return;
419         }
420
421         cls.demostarting = true;
422
423         // disconnect from server
424         CL_Disconnect(false, NULL);
425
426         // update networking ports (this is mainly just needed at startup)
427         NetConn_UpdateSockets();
428
429         cls.protocol = PROTOCOL_QUAKE;
430
431         Con_Printf("Playing demo %s.\n", name);
432         cls.demofile = f;
433         strlcpy(cls.demoname, name, sizeof(cls.demoname));
434
435         cls.demoplayback = true;
436         cls.state = ca_connected;
437         cls.forcetrack = 0;
438
439         while ((c = FS_Getc (cls.demofile)) != '\n')
440                 if (c == '-')
441                         neg = true;
442                 else
443                         cls.forcetrack = cls.forcetrack * 10 + (c - '0');
444
445         if (neg)
446                 cls.forcetrack = -cls.forcetrack;
447
448         cls.demostarting = false;
449 }
450
451 /*
452 ====================
453 CL_PlayDemo_f
454
455 playdemo [demoname]
456 ====================
457 */
458 void CL_PlayDemo_f(cmd_state_t *cmd)
459 {
460         if (Cmd_Argc(cmd) != 2)
461         {
462                 Con_Print("playdemo <demoname> : plays a demo\n");
463                 return;
464         }
465
466         CL_PlayDemo(Cmd_Argv(cmd, 1));
467 }
468
469 typedef struct
470 {
471         int frames;
472         double time, totalfpsavg;
473         double fpsmin, fpsavg, fpsmax;
474 }
475 benchmarkhistory_t;
476 static size_t doublecmp_offset;
477 static int doublecmp_withoffset(const void *a_, const void *b_)
478 {
479         const double *a = (const double *) ((const char *) a_ + doublecmp_offset);
480         const double *b = (const double *) ((const char *) b_ + doublecmp_offset);
481         if(*a > *b)
482                 return +1;
483         if(*a < *b)
484                 return -1;
485         return 0;
486 }
487
488 /*
489 ====================
490 CL_FinishTimeDemo
491
492 ====================
493 */
494 static void CL_FinishTimeDemo (void)
495 {
496         int frames;
497         int i;
498         double time, totalfpsavg;
499         double fpsmin, fpsavg, fpsmax; // report min/avg/max fps
500         static int benchmark_runs = 0;
501         char vabuf[1024];
502
503         cls.timedemo = host.restless = false;
504
505         frames = cls.td_frames;
506         time = host.realtime - cls.td_starttime;
507         totalfpsavg = time > 0 ? frames / time : 0;
508         fpsmin = cls.td_onesecondminfps;
509         fpsavg = cls.td_onesecondavgcount ? cls.td_onesecondavgfps / cls.td_onesecondavgcount : 0;
510         fpsmax = cls.td_onesecondmaxfps;
511         // LadyHavoc: timedemo now prints out 7 digits of fraction, and min/avg/max
512         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);
513         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);
514         if (Sys_CheckParm("-benchmark"))
515         {
516                 ++benchmark_runs;
517                 i = Sys_CheckParm("-benchmarkruns");
518                 if(i && i + 1 < sys.argc)
519                 {
520                         static benchmarkhistory_t *history = NULL;
521                         if(!history)
522                                 history = (benchmarkhistory_t *)Z_Malloc(sizeof(*history) * atoi(sys.argv[i + 1]));
523
524                         history[benchmark_runs - 1].frames = frames;
525                         history[benchmark_runs - 1].time = time;
526                         history[benchmark_runs - 1].totalfpsavg = totalfpsavg;
527                         history[benchmark_runs - 1].fpsmin = fpsmin;
528                         history[benchmark_runs - 1].fpsavg = fpsavg;
529                         history[benchmark_runs - 1].fpsmax = fpsmax;
530
531                         if(atoi(sys.argv[i + 1]) > benchmark_runs)
532                         {
533                                 // restart the benchmark
534                                 Cbuf_AddText(cmd_local, va(vabuf, sizeof(vabuf), "timedemo %s\n", cls.demoname));
535                                 // cannot execute here
536                         }
537                         else
538                         {
539                                 // print statistics
540                                 int first = Sys_CheckParm("-benchmarkruns_skipfirst") ? 1 : 0;
541                                 if(benchmark_runs > first)
542                                 {
543 #define DO_MIN(f) \
544                                         for(i = first; i < benchmark_runs; ++i) if((i == first) || (history[i].f < f)) f = history[i].f
545
546 #define DO_MAX(f) \
547                                         for(i = first; i < benchmark_runs; ++i) if((i == first) || (history[i].f > f)) f = history[i].f
548
549 #define DO_MED(f) \
550                                         doublecmp_offset = (char *)&history->f - (char *)history; \
551                                         qsort(history + first, benchmark_runs - first, sizeof(*history), doublecmp_withoffset); \
552                                         if((first + benchmark_runs) & 1) \
553                                                 f = history[(first + benchmark_runs - 1) / 2].f; \
554                                         else \
555                                                 f = (history[(first + benchmark_runs - 2) / 2].f + history[(first + benchmark_runs) / 2].f) / 2
556
557                                         DO_MIN(frames);
558                                         DO_MAX(time);
559                                         DO_MIN(totalfpsavg);
560                                         DO_MIN(fpsmin);
561                                         DO_MIN(fpsavg);
562                                         DO_MIN(fpsmax);
563                                         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);
564
565                                         DO_MED(frames);
566                                         DO_MED(time);
567                                         DO_MED(totalfpsavg);
568                                         DO_MED(fpsmin);
569                                         DO_MED(fpsavg);
570                                         DO_MED(fpsmax);
571                                         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);
572
573                                         DO_MAX(frames);
574                                         DO_MIN(time);
575                                         DO_MAX(totalfpsavg);
576                                         DO_MAX(fpsmin);
577                                         DO_MAX(fpsavg);
578                                         DO_MAX(fpsmax);
579                                         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);
580                                 }
581                                 Z_Free(history);
582                                 history = NULL;
583                                 host.state = host_shutdown;
584                         }
585                 }
586                 else
587                         host.state = host_shutdown;
588         }
589 }
590
591 /*
592 ====================
593 CL_TimeDemo_f
594
595 timedemo [demoname]
596 ====================
597 */
598 void CL_TimeDemo_f(cmd_state_t *cmd)
599 {
600         if (Cmd_Argc(cmd) != 2)
601         {
602                 Con_Print("timedemo <demoname> : gets demo speeds\n");
603                 return;
604         }
605
606         srand(0); // predictable random sequence for benchmarking
607
608         CL_PlayDemo(Cmd_Argv(cmd, 1));
609
610 // cls.td_starttime will be grabbed at the second frame of the demo, so
611 // all the loading time doesn't get counted
612
613         // instantly hide console and deactivate it
614         key_dest = key_game;
615         key_consoleactive = 0;
616         scr_con_current = 0;
617
618         cls.timedemo = host.restless = true;
619         cls.td_frames = -2;             // skip the first frame
620         cls.demonum = -1;               // stop demo loop
621 }
622
623 /*
624 ===============================================================================
625
626 DEMO LOOP CONTROL
627
628 ===============================================================================
629 */
630
631
632 /*
633 ==================
634 CL_Startdemos_f
635 ==================
636 */
637 static void CL_Startdemos_f(cmd_state_t *cmd)
638 {
639         int             i, c;
640
641         if (cls.state == ca_dedicated || Sys_CheckParm("-listen") || Sys_CheckParm("-benchmark") || Sys_CheckParm("-demo") || Sys_CheckParm("-capturedemo"))
642                 return;
643
644         c = Cmd_Argc(cmd) - 1;
645         if (c > MAX_DEMOS)
646         {
647                 Con_Printf("Max %i demos in demoloop\n", MAX_DEMOS);
648                 c = MAX_DEMOS;
649         }
650         Con_DPrintf("%i demo(s) in loop\n", c);
651
652         for (i=1 ; i<c+1 ; i++)
653                 strlcpy (cls.demos[i-1], Cmd_Argv(cmd, i), sizeof (cls.demos[i-1]));
654
655         // LadyHavoc: clear the remaining slots
656         for (;i <= MAX_DEMOS;i++)
657                 cls.demos[i-1][0] = 0;
658
659         if (!sv.active && cls.demonum != -1 && !cls.demoplayback)
660         {
661                 cls.demonum = 0;
662                 CL_NextDemo ();
663         }
664         else
665                 cls.demonum = -1;
666 }
667
668
669 /*
670 ==================
671 CL_Demos_f
672
673 Return to looping demos
674 ==================
675 */
676 static void CL_Demos_f(cmd_state_t *cmd)
677 {
678         if (cls.state == ca_dedicated)
679                 return;
680         if (cls.demonum == -1)
681                 cls.demonum = 1;
682         CL_Disconnect(false, NULL);
683         CL_NextDemo();
684 }
685
686 /*
687 ==================
688 CL_Stopdemo_f
689
690 Return to looping demos
691 ==================
692 */
693 static void CL_Stopdemo_f(cmd_state_t *cmd)
694 {
695         if (!cls.demoplayback)
696                 return;
697         CL_Disconnect(false, NULL);
698 }
699
700 // LadyHavoc: pausedemo command
701 static void CL_PauseDemo_f(cmd_state_t *cmd)
702 {
703         cls.demopaused = !cls.demopaused;
704         if (cls.demopaused)
705                 Con_Print("Demo paused\n");
706         else
707                 Con_Print("Demo unpaused\n");
708 }
709
710 void CL_Demo_Init(void)
711 {
712         Cmd_AddCommand(CF_CLIENT, "record", CL_Record_f, "record a demo");
713         Cmd_AddCommand(CF_CLIENT, "stop", CL_Stop_f, "stop recording or playing a demo");
714         Cmd_AddCommand(CF_CLIENT, "playdemo", CL_PlayDemo_f, "watch a demo file");
715         Cmd_AddCommand(CF_CLIENT, "timedemo", CL_TimeDemo_f, "play back a demo as fast as possible and save statistics to benchmark.log");
716         Cmd_AddCommand(CF_CLIENT, "startdemos", CL_Startdemos_f, "start playing back the selected demos sequentially (used at end of startup script)");
717         Cmd_AddCommand(CF_CLIENT, "demos", CL_Demos_f, "restart looping demos defined by the last startdemos command");
718         Cmd_AddCommand(CF_CLIENT, "stopdemo", CL_Stopdemo_f, "stop playing or recording demo (like stop command) and return to looping demos");
719         // LadyHavoc: added pausedemo
720         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)");
721         Cvar_RegisterVariable (&cl_autodemo);
722         Cvar_RegisterVariable (&cl_autodemo_nameformat);
723         Cvar_RegisterVariable (&cl_autodemo_delete);
724 }