1 /*
2 Copyright (C) 2003-2006 Andrey Nazarov
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 //
22 // cl_demo.c - demo recording and playback
23 //
24 
25 #include "client.h"
26 
27 cvar_t	*cl_demo_local_fov;
28 
29 static cvar_t *cl_demo_timescale;
30 
31 // =========================================================================
32 
33 /*
34 ====================
35 CL_WriteDemoMessage
36 
37 Dumps the current net message, prefixed by the length
38 ====================
39 */
CL_WriteDemoMessage(sizebuf_t * msgbuf,int bytesToSkip)40 void CL_WriteDemoMessage( sizebuf_t *msgbuf, int bytesToSkip ) {
41 	int		len, swlen;
42 
43 	// the first several bytes are just packet sequencing stuff
44 	len = msgbuf->cursize - bytesToSkip;
45 	if( len < 1 ) {
46 		return; // don't write bad messages
47 	}
48 
49 	swlen = LittleLong( len );
50 	FS_Write( &swlen, 4, cls.demofile );
51 	FS_Write( msgbuf->data + bytesToSkip, len, cls.demofile );
52 }
53 
54 
55 /*
56 ====================
57 CL_CloseDemoFile
58 ====================
59 */
CL_CloseDemoFile(void)60 void CL_CloseDemoFile( void ) {
61 	if( !cls.demofile ) {
62 		return;
63 	}
64 
65 	FS_FCloseFile( cls.demofile );
66 
67 	cls.demofile = 0;
68 }
69 
70 
71 /*
72 ====================
73 CL_Stop_f
74 
75 stop recording a demo
76 ====================
77 */
CL_Stop_f(void)78 void CL_Stop_f( void ) {
79 	int len;
80 
81 	if( !cls.demorecording ) {
82 		Com_Printf( "Not recording a demo.\n" );
83 		return;
84 	}
85 
86 	if( cls.serverProtocol == PROTOCOL_VERSION_R1Q2 ||
87 		cls.serverProtocol == PROTOCOL_VERSION_Q2PRO )
88 	{
89 		/* tell the server we finished recording */
90 		MSG_WriteByte( clc_setting );
91 		MSG_WriteShort( CLS_RECORDING );
92 		MSG_WriteShort( 0 );
93 		MSG_FlushTo( &cls.netchan->message );
94 	}
95 
96 // finish up
97 	len = -1;
98 	FS_Write( &len, 4, cls.demofile );
99 
100 	CL_CloseDemoFile();
101 
102 	cls.demorecording = qfalse;
103 	Com_Printf( "Stopped demo.\n" );
104 }
105 
106 /*
107 ====================
108 CL_Record_f
109 
110 record <demoname>
111 
112 Begins recording a demo from the current position
113 ====================
114 */
CL_Record_f(void)115 void CL_Record_f( void ) {
116 	char	name[MAX_QPATH];
117 	int		i, length;
118 	entity_state_t	*ent;
119 	char *string;
120 	fileHandle_t demofile;
121 	qboolean compressed = qfalse;
122 
123 	i = 1;
124 	if( !strcmp( Cmd_Argv( i ), "-c" ) || !strcmp( Cmd_Argv( i ), "--compressed" ) ) {
125 		compressed = qtrue;
126 		i++;
127 	}
128 
129 	if( i >= Cmd_Argc() ) {
130 		Com_Printf( "Usage: %s [-c|--compressed] [/]<filename>\n", Cmd_Argv( 0 ) );
131 		return;
132 	}
133 
134 	if( cls.demorecording ) {
135 		Com_Printf( "Already recording.\n" );
136 		return;
137 	}
138 
139 	if( cls.state != ca_active || cls.demoplayback ) {
140 		Com_Printf( "You must be in a level to record.\n" );
141 		return;
142 	}
143 
144 	//
145 	// open the demo file
146 	//
147 	string = Cmd_Argv( i );
148 	if( *string == '/' ) {
149 		Q_strncpyz( name, string + 1, sizeof( name ) );
150 	} else {
151 		Com_sprintf( name, sizeof( name ), "demos/%s", string );
152 		if( cls.serverProtocol == PROTOCOL_VERSION_R1Q2 ) {
153 			COM_DefaultExtension( name, ".dm_35", sizeof( name ) );
154 		} else if( cls.serverProtocol == PROTOCOL_VERSION_Q2PRO ) {
155 			COM_DefaultExtension( name, ".dm_36", sizeof( name ) );
156 		} else {
157 			COM_DefaultExtension( name, ".dm2", sizeof( name ) );
158 		}
159 	}
160 	if( compressed ) {
161 		Q_strcat( name, sizeof( name ), ".gz" );
162 	}
163 
164 	FS_FOpenFile( name, &demofile, FS_MODE_WRITE );
165 	if( !demofile ) {
166 		Com_EPrintf( "Couldn't open %s for writing.\n", name );
167 		return;
168 	}
169 
170 	Com_Printf( "Recording client demo to %s.\n", name );
171 
172 	cls.demofile = demofile;
173 	cls.demorecording = qtrue;
174 
175 	// don't start saving messages until a non-delta compressed message is received
176 	cls.demowaiting = qtrue;
177 
178 	if( cls.serverProtocol == PROTOCOL_VERSION_R1Q2 ||
179 		cls.serverProtocol == PROTOCOL_VERSION_Q2PRO )
180 	{
181 		/* tell the server we are recording */
182 		MSG_WriteByte( clc_setting );
183 		MSG_WriteShort( CLS_RECORDING );
184 		MSG_WriteShort( 1 );
185 		MSG_FlushTo( &cls.netchan->message );
186 	}
187 
188 	//
189 	// write out messages to hold the startup information
190 	//
191 
192 	// send the serverdata
193 	MSG_WriteByte( svc_serverdata );
194 	MSG_WriteLong( cls.serverProtocol );
195 	MSG_WriteLong( 0x10000 + cl.servercount );
196 	MSG_WriteByte( ATR_DEMO );	// demos are always attract loops
197 	MSG_WriteString( cl.gamedir );
198     if( cl.clientNum == CLIENTNUM_NONE ) {
199         MSG_WriteShort( MAX_EDICTS - 1 );
200     } else {
201     	MSG_WriteShort( cl.clientNum );
202     }
203 
204 	MSG_WriteString( cl.configstrings[CS_NAME] );
205 
206 	if( cls.serverProtocol == PROTOCOL_VERSION_R1Q2 ) {
207 		MSG_WriteByte( 0 );		/* enchanced */
208 		MSG_WriteShort( PROTOCOL_VERSION_R1Q2_MINOR );
209 		MSG_WriteByte( 0 );		/* advanced deltas */
210 		MSG_WriteByte( cl.pmp.strafeHack );
211 	} else if( cls.serverProtocol == PROTOCOL_VERSION_Q2PRO ) {
212 		MSG_WriteShort( PROTOCOL_VERSION_Q2PRO_MINOR );
213 		MSG_WriteByte( cl.gametype );
214 		MSG_WriteByte( cl.pmp.strafeHack );
215 		MSG_WriteByte( cl.pmp.qwmod );
216 	}
217 
218 	// configstrings
219 	for( i = 0; i < MAX_CONFIGSTRINGS; i++ ) {
220 		string = cl.configstrings[i];
221 		if( !string[0] ) {
222 			continue;
223 		}
224 
225 		length = strlen( string );
226 		if( length > MAX_QPATH ) {
227 			length = MAX_QPATH;
228 		}
229 
230 		if( cls.serverProtocol != PROTOCOL_VERSION_Q2PRO ) {
231 			if( msg_write.cursize + length + 4 > MAX_PACKETLEN ) {
232 				CL_WriteDemoMessage( &msg_write, 0 );
233 				SZ_Clear( &msg_write );
234 			}
235 		}
236 
237 		MSG_WriteByte( svc_configstring );
238 		MSG_WriteShort( i );
239 		MSG_WriteData( string, length );
240 		MSG_WriteByte( 0 );
241 
242 	}
243 
244 	// baselines
245 	for( i = 1; i < MAX_EDICTS; i++ ) {
246 		ent = &cl.baselines[i];
247 		if( !ent->number ) {
248 			continue;
249 		}
250 
251 		if( cls.serverProtocol != PROTOCOL_VERSION_Q2PRO ) {
252 			if( msg_write.cursize + 64 > MAX_PACKETLEN ) {
253 				CL_WriteDemoMessage( &msg_write, 0 );
254 				SZ_Clear( &msg_write );
255 			}
256 		}
257 
258 		MSG_WriteByte( svc_spawnbaseline );
259 		MSG_WriteDeltaEntity( NULL, ent, MSG_ES_FORCE );
260 	}
261 
262 	MSG_WriteByte( svc_stufftext );
263 	MSG_WriteString( "precache\n" );
264 
265 	// write it to the demo file
266 	CL_WriteDemoMessage( &msg_write, 0 );
267 	SZ_Clear( &msg_write );
268 
269 	// the rest of the demo file will be individual frames
270 }
271 
272 /*
273 ====================
274 CL_ReadNextDemoMessage
275 ====================
276 */
CL_ReadNextDemoMessage(fileHandle_t f)277 static qboolean CL_ReadNextDemoMessage( fileHandle_t f ) {
278 	int		msglen;
279 
280 	// read msglen
281 	if( FS_Read( &msglen, 4, f ) != 4 ) {
282 		return qfalse;
283 	}
284 
285 	if( msglen == -1 ) {
286 		return qfalse;
287 	}
288 
289 	msglen = LittleLong( msglen );
290 	if( msglen < 1 || msglen >= msg_read.maxsize ) {
291 		return qfalse;
292 	}
293 
294 	msg_read.cursize = msglen;
295 	msg_read.readcount = 0;
296 
297 	// read packet data
298 	if( FS_Read( msg_read.data, msglen, f ) != msglen ) {
299 		return qfalse;
300 	}
301 
302 	return qtrue;
303 
304 }
305 
306 /*
307 ====================
308 CL_ParseNextDemoMessage
309 ====================
310 */
CL_ParseNextDemoMessage(void)311 static void CL_ParseNextDemoMessage( void ) {
312 	int pos;
313 	char *s;
314 
315 	if( !CL_ReadNextDemoMessage( cls.demofile ) ) {
316 		s = Cvar_VariableString( "nextserver" );
317 		if( !s[0] ) {
318 			Com_Error( ERR_SILENT, "Demo finished" );
319 		}
320 		Cbuf_AddText( s );
321 		Cbuf_AddText( "\n" );
322 		Cvar_Set( "nextserver", "" );
323 		cls.state = ca_connected;
324 		return;
325 	}
326 
327 	CL_ParseServerMessage();
328 
329 	if( cls.demofileSize ) {
330 		pos = FS_Tell( cls.demofile ) - cls.demofileFrameOffset;
331 		if( pos < 0 ) {
332 			pos = 0;
333 		}
334 		cls.demofilePercent = pos * 100 / cls.demofileSize;
335 	}
336 }
337 
338 static const char *demoExtTable[] = {
339 	".dm2",
340 	".dm2.gz",
341 	".dm_35",
342 	".dm_35.gz",
343 	".dm_36",
344 	".dm_36.gz"
345 };
346 
347 static const int numDemoExts = sizeof( demoExtTable ) / sizeof( demoExtTable[0] );
348 
349 /*
350 ====================
351 CL_PlayDemo_f
352 ====================
353 */
CL_PlayDemo_f(void)354 static void CL_PlayDemo_f( void ) {
355 	char name[MAX_QPATH];
356 	fileHandle_t demofile;
357 	char *arg, *ext;
358 	int i, length;
359 
360 	if( Cmd_Argc() < 2 ) {
361 		Com_Printf( "Usage: %s <filename>\n", Cmd_Argv( 0 ) );
362 		return;
363 	}
364 
365 	demofile = 0;
366 	length = 0;
367 
368 	arg = Cmd_Argv( 1 );
369 
370 	if( arg[0] == '/' ) {
371 		// Assume full path is given
372 		Q_strncpyz( name, arg + 1, sizeof( name ) );
373 		FS_FOpenFile( name, &demofile, FS_MODE_READ );
374 	} else {
375 		// Search for matching extensions
376 		ext = COM_FileExtension( arg );
377 		if( *ext ) {
378 			Com_sprintf( name, sizeof( name ), "demos/%s", arg );
379 			Com_Printf( "Trying %s...\n", name );
380 			FS_FOpenFile( name, &demofile, FS_MODE_READ );
381 		} else {
382 			// Try all known extensions
383 			for( i = 0; i < numDemoExts; i++ ) {
384 				Com_sprintf( name, sizeof( name ), "demos/%s%s", arg, demoExtTable[i] );
385 				Com_Printf( "Trying %s...\n", name );
386 
387 				FS_FOpenFile( name, &demofile, FS_MODE_READ );
388 				if( demofile ) {
389 					break;
390 				}
391 			}
392 		}
393 	}
394 
395 	if( !demofile ) {
396 		Com_Printf( "Couldn't open demofile\n" );
397 		return;
398 	}
399 
400 	if( sv_running->integer ) {
401 		// if running a local server, kill it and reissue
402 		SV_Shutdown( "Server was killed\n", KILL_DROP );
403 	}
404 
405 	CL_Disconnect( ERR_DISCONNECT, NULL );
406 
407 	Con_Close();
408 
409 	cls.demofile = demofile;
410 
411 	cls.demoplayback = qtrue;
412 	cls.state = ca_connected;
413 	Q_strncpyz( cls.servername, COM_SkipPath( name ), sizeof( cls.servername ) );
414 	cls.serverAddress.type = NA_LOOPBACK;
415 
416 	SCR_UpdateScreen();
417 
418 	do {
419 		CL_ParseNextDemoMessage();
420 	    Cbuf_Execute();
421 	} while( cls.state == ca_connected );
422 
423 	length = FS_GetFileLengthNoCache( cls.demofile );
424 	cls.demofileFrameOffset = FS_Tell( cls.demofile );
425 	cls.demofileSize = length - cls.demofileFrameOffset;
426 
427 	if( com_timedemo->integer ) {
428 		cls.timeDemoFrames = 0;
429 		cls.timeDemoStart = Sys_Milliseconds();
430 	}
431 }
432 
CL_PlayDemo_g(const char * partial,int state)433 static const char *CL_PlayDemo_g( const char *partial, int state ) {
434 	return Com_FileNameGeneratorByFilter( "demos", "*.dm2;*.dm_3?;*.dm2.gz;*.dm_3?.gz", partial, qfalse, state );
435 }
436 
437 /*
438 ====================
439 CL_GetDemoInfo
440 ====================
441 */
CL_GetDemoInfo(const char * path,demoInfo_t * info)442 qboolean CL_GetDemoInfo( const char *path, demoInfo_t *info ) {
443 	fileHandle_t hFile;
444 	int c, protocol;
445 	char *s, *p;
446 
447 	memset( info, 0, sizeof( *info ) );
448 
449 	FS_FOpenFile( path, &hFile, FS_MODE_READ );
450 	if( !hFile ) {
451 		return qfalse;
452 	}
453 
454 	if( !CL_ReadNextDemoMessage( hFile ) ) {
455 		goto fail;
456 	}
457 
458 	if( MSG_ReadByte() != svc_serverdata ) {
459 		goto fail;
460 	}
461 
462 	protocol = MSG_ReadLong();
463 
464 	msg_read.readcount += 5;
465 
466 	Q_strncpyz( info->gamedir, MSG_ReadString(), sizeof( info->gamedir ) );
467 
468 	info->clientNum = MSG_ReadShort();
469 
470 	Q_strncpyz( info->fullLevelName, MSG_ReadString(), sizeof( info->fullLevelName ) );
471 
472 	switch( protocol ) {
473 	case PROTOCOL_VERSION_MVD:
474 		info->mvd = qtrue;
475 		msg_read.readcount += 2;
476 		break;
477 	case PROTOCOL_VERSION_R1Q2:
478 		msg_read.readcount += 5;
479 		break;
480 	case PROTOCOL_VERSION_Q2PRO:
481 		msg_read.readcount += 5;
482 		break;
483 	default:
484 		break;
485 	}
486 
487 	while( 1 ) {
488 		c = MSG_ReadByte();
489 		if( c == -1 ) {
490 			if( !CL_ReadNextDemoMessage( hFile ) ) {
491 				break;
492 			}
493 			continue; // parse new message
494 		}
495 		if( c != svc_configstring ) {
496 			break;
497 		}
498 		c = MSG_ReadShort();
499 		s = MSG_ReadString();
500 		if( c >= CS_PLAYERSKINS && c < CS_PLAYERSKINS + MAX_DEMOINFO_CLIENTS ) {
501 			c -= CS_PLAYERSKINS;
502 			Q_strncpyz( info->clients[c], s, sizeof( info->clients[0] ) );
503 			if( ( p = strchr( info->clients[c], '\\' ) ) != NULL ) {
504 				*p = 0;
505 			}
506 		} else if( c == CS_MODELS + 1 ) {
507 			if( strlen( s ) > 9 ) {
508 				Q_strncpyz( info->mapname, s + 5, sizeof( info->mapname ) ); // skip "maps/"
509 				info->mapname[ strlen( info->mapname ) - 4 ] = 0; // cut off ".bsp"
510 			}
511 		}
512 	}
513 
514 	FS_FCloseFile( hFile );
515 	return qtrue;
516 
517 fail:
518 	FS_FCloseFile( hFile );
519 	return qfalse;
520 
521 }
522 
523 // =========================================================================
524 
525 
526 /*
527 ====================
528 CL_DemoFrame
529 ====================
530 */
CL_DemoFrame(int msec)531 void CL_DemoFrame( int msec ) {
532 	static float frac;
533 	int dt;
534 
535 	if( cls.state < ca_connected ) {
536 		return;
537 	}
538 
539 	if( sv_paused->integer ) {
540 		return;
541 	}
542 
543 	if( !cls.demoplayback ) {
544 		cl.time += msec;
545 		return;
546 	}
547 
548 	if( cls.state != ca_active ) {
549 		CL_ParseNextDemoMessage();
550 		return;
551 	}
552 
553 	if( com_timedemo->integer ) {
554 		CL_ParseNextDemoMessage();
555 		cl.time = cl.serverTime;
556 		cls.timeDemoFrames++;
557 		return;
558 	}
559 
560 	if( cl_demo_timescale->value < 0 ) {
561 		Cvar_Set( "cl_demo_timescale", "0" );
562 	} else if( cl_demo_timescale->value > 1000 ) {
563 		Cvar_Set( "cl_demo_timescale", "1000" );
564 	}
565 
566 	if( cl_demo_timescale->value ) {
567 		frac += msec * cl_demo_timescale->value;
568 		dt = frac;
569 		frac -= dt;
570 
571 		cl.time += dt;
572 	}
573 
574 	while( cl.serverTime < cl.time ) {
575 		CL_ParseNextDemoMessage();
576 		if( cls.state != ca_active ) {
577 			break;
578 		}
579 	}
580 
581 }
582 
CL_DemoLocalFov_OnChange(cvar_t * self,void * arg)583 static void CL_DemoLocalFov_OnChange( cvar_t *self, void *arg ) {
584 	CL_UpdateLocalFovSetting();
585 }
586 
587 /*
588 ====================
589 CL_InitDemos
590 ====================
591 */
CL_InitDemos(void)592 void CL_InitDemos( void ) {
593 	cl_demo_timescale = Cvar_Get( "cl_demo_timescale", "1", 0 );
594 	cl_demo_local_fov = Cvar_Get( "cl_demo_local_fov", "1", 0 );
595 	cl_demo_local_fov->changedFunc = CL_DemoLocalFov_OnChange;
596 
597 	Cmd_AddCommandEx( "demo", CL_PlayDemo_f, CL_PlayDemo_g );
598 	Cmd_AddCommandEx( "record", CL_Record_f, CL_PlayDemo_g );
599 	Cmd_AddCommand( "stop", CL_Stop_f );
600 }
601 
602 
603