From 1ddc203168b4bdffad9293caab4ee2f244fb4ec0 Mon Sep 17 00:00:00 2001 From: Noah Metzger Date: Fri, 3 Nov 2023 00:08:10 -0500 Subject: [PATCH 1/2] SSR: Add initial server-side recording system --- Makefile | 10 + README.md | 12 + code/server/server.h | 13 + code/server/sv_client.c | 6 + code/server/sv_game.c | 1 + code/server/sv_init.c | 4 + code/server/sv_main.c | 3 + code/server/sv_record_common.c | 997 ++++++++++++++++++ code/server/sv_record_convert.c | 631 +++++++++++ code/server/sv_record_local.h | 223 ++++ code/server/sv_record_main.c | 198 ++++ code/server/sv_record_spectator.c | 982 +++++++++++++++++ code/server/sv_record_writer.c | 650 ++++++++++++ code/server/sv_snapshot.c | 1 + code/win32/msvc2005/quake3e-ded.vcproj | 24 + code/win32/msvc2005/quake3e.vcproj | 24 + code/win32/msvc2017/quake3e-ded.vcxproj | 6 + .../msvc2017/quake3e-ded.vcxproj.filters | 18 + code/win32/msvc2017/quake3e.vcxproj | 6 + code/win32/msvc2017/quake3e.vcxproj.filters | 18 + 20 files changed, 3827 insertions(+) create mode 100644 code/server/sv_record_common.c create mode 100644 code/server/sv_record_convert.c create mode 100644 code/server/sv_record_local.h create mode 100644 code/server/sv_record_main.c create mode 100644 code/server/sv_record_spectator.c create mode 100644 code/server/sv_record_writer.c diff --git a/Makefile b/Makefile index f11818a41..28557f8d3 100644 --- a/Makefile +++ b/Makefile @@ -1058,6 +1058,11 @@ Q3OBJ = \ $(B)/client/sv_init.o \ $(B)/client/sv_main.o \ $(B)/client/sv_net_chan.o \ + $(B)/client/sv_record_common.o \ + $(B)/client/sv_record_convert.o \ + $(B)/client/sv_record_main.o \ + $(B)/client/sv_record_spectator.o \ + $(B)/client/sv_record_writer.o \ $(B)/client/sv_snapshot.o \ $(B)/client/sv_world.o \ \ @@ -1256,6 +1261,11 @@ Q3DOBJ = \ $(B)/ded/sv_init.o \ $(B)/ded/sv_main.o \ $(B)/ded/sv_net_chan.o \ + $(B)/ded/sv_record_common.o \ + $(B)/ded/sv_record_convert.o \ + $(B)/ded/sv_record_main.o \ + $(B)/ded/sv_record_spectator.o \ + $(B)/ded/sv_record_writer.o \ $(B)/ded/sv_snapshot.o \ $(B)/ded/sv_world.o \ \ diff --git a/README.md b/README.md index 0a4ad08f8..213b06a3b 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,18 @@ Performance is usually greater or equal to other opengl1 renderers Original ioquake3 renderer, performance is very poor on non-nvidia systems, unmaintained +## Server Side Recording + +Enabled by `sv_recordAutoRecording 1` to record all matches on the server to records directory. + +To convert recorded file to demo format for playback, use `record_convert ` command, where filename is the path to the .rec file within the records directory, clientnum is the client perspective to view, and instance differentiates multiple clients or reconnections with the same client number. + +To view available clients and instances for a record file, use `record_scan ` command. + +## Admin Spectator + +Allows admins to spectate players on the server without joining (useful to monitor for cheating). To enable, set `sv_adminSpectatorPassword` on the server to a password of your choosing. To join the server in spectator mode, set password on the client to `spect_` plus the server password. + ## [Build Instructions](BUILD.md) ## Contacts diff --git a/code/server/server.h b/code/server/server.h index d05ad1811..f0417e10e 100644 --- a/code/server/server.h +++ b/code/server/server.h @@ -489,3 +489,16 @@ void SV_LoadFilters( const char *filename ); const char *SV_RunFilters( const char *userinfo, const netadr_t *addr ); void SV_AddFilter_f( void ); void SV_AddFilterCmd_f( void ); + +// +// sv_record_main.c +// +void Record_Initialize( void ); +void Record_ProcessUsercmd( int clientNum, usercmd_t *usercmd ); +void Record_ProcessConfigstring( int index, const char *value ); +void Record_ProcessServercmd( int clientNum, const char *value ); +void Record_ProcessMapLoaded( void ); +void Record_ProcessSnapshot( void ); +void Record_ProcessGameShutdown( void ); +qboolean Record_ProcessClientConnect( const netadr_t *address, const char *userinfo, int challenge, int qport, qboolean compat ); +qboolean Record_ProcessPacketEvent( const netadr_t *address, msg_t *msg, int qport ); diff --git a/code/server/sv_client.c b/code/server/sv_client.c index a1eeaebf2..bfe14d230 100644 --- a/code/server/sv_client.c +++ b/code/server/sv_client.c @@ -595,6 +595,10 @@ void SV_DirectConnect( const netadr_t *from ) { return; } + if ( Record_ProcessClientConnect( from, userinfo, challenge, qport, compat ) ) { + return; + } + // run userinfo filter SV_SetTLD( tld, from, Sys_IsLANAddress( from ) ); Info_SetValueForKey( userinfo, "tld", tld ); @@ -2021,6 +2025,8 @@ void SV_ClientThink (client_t *cl, usercmd_t *cmd) { return; // may have been kicked during the last usercmd } + Record_ProcessUsercmd( cl - svs.clients, cmd ); + VM_Call( gvm, 1, GAME_CLIENT_THINK, cl - svs.clients ); } diff --git a/code/server/sv_game.c b/code/server/sv_game.c index 290947126..0bc47dd50 100644 --- a/code/server/sv_game.c +++ b/code/server/sv_game.c @@ -993,6 +993,7 @@ Called every time a map changes =============== */ void SV_ShutdownGameProgs( void ) { + Record_ProcessGameShutdown(); if ( !gvm ) { return; } diff --git a/code/server/sv_init.c b/code/server/sv_init.c index f786809d3..087f34e16 100644 --- a/code/server/sv_init.c +++ b/code/server/sv_init.c @@ -143,6 +143,7 @@ void SV_SetConfigstring (int index, const char *val) { SV_SendConfigstring(client, index); } } + Record_ProcessConfigstring( index, val ); } @@ -669,6 +670,8 @@ void SV_SpawnServer( const char *mapname, qboolean killBots ) { Hunk_SetMark(); + Record_ProcessMapLoaded(); + Com_Printf ("-----------------------------------\n"); Sys_SetStatus( "Running map %s", mapname ); @@ -812,6 +815,7 @@ void SV_Init( void ) SV_TrackCvarChanges(); SV_InitChallenger(); + Record_Initialize(); } diff --git a/code/server/sv_main.c b/code/server/sv_main.c index efccf5f64..1432a33af 100644 --- a/code/server/sv_main.c +++ b/code/server/sv_main.c @@ -152,6 +152,8 @@ void SV_AddServerCommand( client_t *client, const char *cmd ) { if ( client->state < CS_PRIMED ) return; + Record_ProcessServercmd( client - svs.clients, cmd ); + client->reliableSequence++; // if we would be losing an old command that hasn't been acknowledged, // we must drop the connection @@ -1027,6 +1029,7 @@ void SV_PacketEvent( const netadr_t *from, msg_t *msg ) { return; } } + Record_ProcessPacketEvent( from, msg, qport ); } diff --git a/code/server/sv_record_common.c b/code/server/sv_record_common.c new file mode 100644 index 000000000..5fd32c06d --- /dev/null +++ b/code/server/sv_record_common.c @@ -0,0 +1,997 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2017-2023 Noah Metzger (chomenor@gmail.com) + +This file is part of Quake III Arena source code. + +Quake III Arena source code is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Quake III Arena source code is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Quake III Arena source code; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +#include "sv_record_local.h" + +/* ******************************************************************************** */ +// Data Stream +/* ******************************************************************************** */ + +/* +================== +Record_Stream_Error + +General purpose error that can be called by any function doing encoding/decoding on the stream +Uses stream abort jump if set, otherwise calls Com_Error +================== +*/ +void Record_Stream_Error( record_data_stream_t *stream, const char *message ) { + Record_Printf( RP_ALL, "%s\n", message ); + if ( stream->abortSet ) { + stream->abortSet = qfalse; + longjmp( stream->abort, 1 ); + } + Com_Error( ERR_FATAL, "%s", message ); +} + +/* +================== +Record_Stream_Allocate +================== +*/ +char *Record_Stream_Allocate( int size, record_data_stream_t *stream ) { + char *data = stream->data + stream->position; + if ( stream->position + size > stream->size || stream->position + size < stream->position ) { + Record_Stream_Error( stream, "Record_Stream_Allocate: stream overflow" ); + } + stream->position += size; + return data; +} + +/* +================== +Record_Stream_Write +================== +*/ +void Record_Stream_Write( void *data, int size, record_data_stream_t *stream ) { + char *target = Record_Stream_Allocate( size, stream ); + if ( target ) { + Com_Memcpy( target, data, size ); + } +} + +/* +================== +Record_Stream_WriteValue +================== +*/ +void Record_Stream_WriteValue( int value, int size, record_data_stream_t *stream ) { + Record_Stream_Write( &value, size, stream ); +} + +/* +================== +Record_Stream_ReadStatic +================== +*/ +char *Record_Stream_ReadStatic( int size, record_data_stream_t *stream ) { + char *output = stream->data + stream->position; + if ( stream->position + size > stream->size || stream->position + size < stream->position ) { + Record_Stream_Error( stream, "Record_Stream_ReadStatic: stream overflow" ); + } + stream->position += size; + return output; +} + +/* +================== +Record_Stream_ReadBuffer +================== +*/ +void Record_Stream_ReadBuffer( void *output, int size, record_data_stream_t *stream ) { + void *data = Record_Stream_ReadStatic( size, stream ); + if ( data ) { + Com_Memcpy( output, data, size ); + } +} + +/* +================== +Record_Stream_DumpToFile +================== +*/ +void Record_Stream_DumpToFile( record_data_stream_t *stream, fileHandle_t file ) { + FS_Write( stream->data, stream->position, file ); + stream->position = 0; +} + +/* ******************************************************************************** */ +// Memory allocation +/* ******************************************************************************** */ + +int alloc_count = 0; + +/* +================== +Record_Calloc +================== +*/ +void *Record_Calloc( unsigned int size ) { + ++alloc_count; + return calloc( size, 1 ); +} + +/* +================== +Record_Free +================== +*/ +void Record_Free( void *ptr ) { + --alloc_count; + free( ptr ); +} + +/* ******************************************************************************** */ +// Bit operations +/* ******************************************************************************** */ + +/* +================== +Record_Bit_Set +================== +*/ +void Record_Bit_Set( int *target, int position ) { + target[position / 32] |= 1 << ( position % 32 ); +} + +/* +================== +Record_Bit_Unset +================== +*/ +void Record_Bit_Unset( int *target, int position ) { + target[position / 32] &= ~( 1 << ( position % 32 ) ); +} + +/* +================== +Record_Bit_Get +================== +*/ +int Record_Bit_Get( int *source, int position ) { + return ( source[position / 32] >> ( position % 32 ) ) & 1; +} + +/* ******************************************************************************** */ +// Flag operations +/* ******************************************************************************** */ + +// These flags are potentially game/mod specific, so their access is aggregated here +// so game-specific changes can be made in one place if needed + +/* +================== +Record_UsercmdIsFiringWeapon +================== +*/ +qboolean Record_UsercmdIsFiringWeapon( const usercmd_t *cmd ) { + if ( cmd->buttons & BUTTON_ATTACK ) { + return qtrue; + } + return qfalse; +} + +/* +================== +Record_PlayerstateIsSpectator +================== +*/ +qboolean Record_PlayerstateIsSpectator( const playerState_t *ps ) { + if ( ps->pm_type == PM_SPECTATOR || ps->pm_flags & PMF_FOLLOW ) { + return qtrue; + } + return qfalse; +} + +/* +================== +Record_SetPlayerstateFollowFlag +================== +*/ +void Record_SetPlayerstateFollowFlag( playerState_t *ps ) { + ps->pm_flags |= PMF_FOLLOW; +} + +/* ******************************************************************************** */ +// Message printing +/* ******************************************************************************** */ + +/* +================== +Record_Printf +================== +*/ +void QDECL Record_Printf( record_print_mode_t mode, const char *fmt, ... ) { + va_list argptr; + char message[1024]; + + if ( mode == RP_DEBUG && !sv_recordDebug->integer ) { + return; + } + + va_start( argptr, fmt ); + Q_vsnprintf( message, sizeof( message ), fmt, argptr ); + va_end( argptr ); + + Com_Printf( "%s", message ); +} + +/* ******************************************************************************** */ +// Record State Functions +/* ******************************************************************************** */ + +/* +================== +Record_AllocateState +================== +*/ +record_state_t *Record_AllocateState( int maxClients ) { + int i; + record_state_t *rs = (record_state_t *)Record_Calloc( sizeof( *rs ) ); + rs->clients = (record_state_client_t *)Record_Calloc( sizeof( *rs->clients ) * maxClients ); + rs->maxClients = maxClients; + + // Initialize configstrings + for ( i = 0; i < MAX_CONFIGSTRINGS; ++i ) { + rs->configstrings[i] = CopyString( "" ); + } + + rs->currentServercmd = CopyString( "" ); + return rs; +} + +/* +================== +Record_FreeState +================== +*/ +void Record_FreeState( record_state_t *rs ) { + int i; + Record_Free( rs->clients ); + for ( i = 0; i < MAX_CONFIGSTRINGS; ++i ) { + if ( rs->configstrings[i] ) + Z_Free( rs->configstrings[i] ); + } + if ( rs->currentServercmd ) { + Z_Free( rs->currentServercmd ); + } + Record_Free( rs ); +} + +/* ******************************************************************************** */ +// Structure Encoding/Decoding Functions +/* ******************************************************************************** */ + +// ***** Strings ***** + +/* +================== +Record_EncodeString +================== +*/ +void Record_EncodeString( char *string, record_data_stream_t *stream ) { + int length = strlen( string ); + Record_Stream_WriteValue( length, 4, stream ); + Record_Stream_Write( string, length + 1, stream ); +} + +/* +================== +Record_DecodeString +================== +*/ +char *Record_DecodeString( record_data_stream_t *stream ) { + int length = *(int *)Record_Stream_ReadStatic( 4, stream ); + char *string; + if ( length < 0 ) { + Record_Stream_Error( stream, "Record_DecodeString: invalid length" ); + } + string = Record_Stream_ReadStatic( length + 1, stream ); + if ( string[length] ) { + Record_Stream_Error( stream, "Record_DecodeString: string not null terminated" ); + } + return string; +} + +// ***** Generic Structure ***** + +/* +================== +Record_EncodeStructure + +Basic structure encoding sends the index byte followed by data chunk +Field encoding sends the index byte with high bit set, followed by byte field indicating + the following 8 indexes to send, followed by specified data chunks +In byte_pass mode only data chunks that can be encoded as 1 byte are encoded, otherwise + chunks are 4 bytes +================== +*/ +static void Record_EncodeStructure( qboolean byte_pass, unsigned int *state, unsigned int *source, int size, + record_data_stream_t *stream ) { + int i, j; + unsigned char *field = 0; + int fieldPosition = 0; + + for ( i = 0; i < size; ++i ) { + if ( state[i] != source[i] && ( !byte_pass || ( state[i] & ~255 ) == ( source[i] & ~255 ) ) ) { + if ( field && i - fieldPosition < 8 ) { + { + *field |= ( 1 << ( i - fieldPosition ) ); + } + } else { + int fieldHits = 0; + for ( j = i + 1; j < i + 9 && j < size; ++j ) { + if ( state[j] != source[j] && ( !byte_pass || ( state[j] & ~255 ) == ( source[j] & ~255 ) ) ) { + ++fieldHits; + } + } + if ( fieldHits > 1 ) { + Record_Stream_WriteValue( i | 128, 1, stream ); + field = (unsigned char *)Record_Stream_Allocate( 1, stream ); + *field = 0; + fieldPosition = i + 1; + } else { + Record_Stream_WriteValue( i, 1, stream ); + } + } + Record_Stream_WriteValue( state[i] ^ source[i], byte_pass ? 1 : 4, stream ); + state[i] = source[i]; + } + } + + Record_Stream_WriteValue( 255, 1, stream ); +} + +/* +================== +Record_DecodeStructure +================== +*/ +static void Record_DecodeStructure( qboolean byte_pass, unsigned int *state, unsigned int size, record_data_stream_t *stream ) { + while ( 1 ) { + unsigned char cmd = *(unsigned char *)Record_Stream_ReadStatic( 1, stream ); + int index = cmd & 127; + int field = 0; + + if ( cmd == 255 ) { + break; + } + if ( cmd & 128 ) { + field = *(unsigned char *)Record_Stream_ReadStatic( 1, stream ); + } + field = ( field << 1 ) | 1; + + while ( field ) { + if ( field & 1 ) { + if ( index >= size ) { + Record_Stream_Error( stream, "Record_DecodeStructure: out of bounds" ); + } + if ( byte_pass ) { + state[index] ^= *(unsigned char *)Record_Stream_ReadStatic( 1, stream ); + } else { + state[index] ^= *(unsigned int *)Record_Stream_ReadStatic( 4, stream ); + } + } + field >>= 1; + ++index; + } + } +} + +// ***** Playerstates ***** + +/* +================== +Record_EncodePlayerstate +================== +*/ +void Record_EncodePlayerstate(playerState_t *state, playerState_t *source, record_data_stream_t *stream) { + Record_EncodeStructure(qtrue, (unsigned int *)state, (unsigned int *)source, sizeof(*state)/4, stream); + Record_EncodeStructure(qfalse, (unsigned int *)state, (unsigned int *)source, sizeof(*state)/4, stream); } + +/* +================== +Record_DecodePlayerstate +================== +*/ +void Record_DecodePlayerstate(playerState_t *state, record_data_stream_t *stream) { + Record_DecodeStructure(qtrue, (unsigned int *)state, sizeof(*state)/4, stream); + Record_DecodeStructure(qfalse, (unsigned int *)state, sizeof(*state)/4, stream); } + +// ***** Entitystates ***** + +/* +================== +Record_EncodeEntitystate +================== +*/ +void Record_EncodeEntitystate( entityState_t *state, entityState_t *source, record_data_stream_t *stream ) { + Record_EncodeStructure( qtrue, (unsigned int *)state, (unsigned int *)source, sizeof( *state ) / 4, stream ); + Record_EncodeStructure( qfalse, (unsigned int *)state, (unsigned int *)source, sizeof( *state ) / 4, stream ); +} + +/* +================== +Record_DecodeEntitystate +================== +*/ +void Record_DecodeEntitystate( entityState_t *state, record_data_stream_t *stream ) { + Record_DecodeStructure( qtrue, (unsigned int *)state, sizeof( *state ) / 4, stream ); + Record_DecodeStructure( qfalse, (unsigned int *)state, sizeof( *state ) / 4, stream ); +} + +// ***** Entitysets ***** + +/* +================== +Record_EncodeEntityset + +Sets state equal to source, and writes delta change to stream +================== +*/ +void Record_EncodeEntityset( record_entityset_t *state, record_entityset_t *source, record_data_stream_t *stream ) { + int i; + for ( i = 0; i < MAX_GENTITIES; ++i ) { + if ( !Record_Bit_Get( state->activeFlags, i ) && !Record_Bit_Get( source->activeFlags, i ) ) + continue; + else if ( Record_Bit_Get( state->activeFlags, i ) && !Record_Bit_Get( source->activeFlags, i ) ) { + // Com_Printf( "encode remove %i\n", i ); + Record_Stream_WriteValue( i | ( 1 << 12 ), 2, stream ); + Record_Bit_Unset( state->activeFlags, i ); + } else if ( !Record_Bit_Get( state->activeFlags, i ) || + memcmp( &state->entities[i], &source->entities[i], sizeof( state->entities[i] ) ) ) { + // Com_Printf( "encode modify %i\n", i ); + Record_Stream_WriteValue( i | ( 2 << 12 ), 2, stream ); + Record_EncodeEntitystate( &state->entities[i], &source->entities[i], stream ); + Record_Bit_Set( state->activeFlags, i ); + } + } + + // Finished + Record_Stream_WriteValue( -1, 2, stream ); +} + +/* +================== +Record_DecodeEntityset + +Modifies state to reflect delta changes in stream +================== +*/ +void Record_DecodeEntityset( record_entityset_t *state, record_data_stream_t *stream ) { + while ( 1 ) { + short data = *(short *)Record_Stream_ReadStatic( 2, stream ); + short newnum = data & ( ( 1 << 12 ) - 1 ); + short command = data >> 12; + + // Finished + if ( data == -1 ) { + break; + } + + if ( newnum < 0 || newnum >= MAX_GENTITIES ) { + Record_Stream_Error( stream, "Record_DecodeEntityset: bad entity number" ); + } + + if ( command == 1 ) { + // Com_Printf( "decode remove %i\n", newnum ); + Record_Bit_Unset( state->activeFlags, newnum ); + } else if ( command == 2 ) { + // Com_Printf( "decode modify %i\n", newnum ); + Record_DecodeEntitystate( &state->entities[newnum], stream ); + Record_Bit_Set( state->activeFlags, newnum ); + } else { + Record_Stream_Error( stream, "Record_DecodeEntityset: bad command" ); + } + } +} + +// ***** Visibility States ***** + +/* +================== +Record_EncodeVisibilityState +================== +*/ +void Record_EncodeVisibilityState( record_visibility_state_t *state, record_visibility_state_t *source, + record_data_stream_t *stream ) { + Record_EncodeStructure( qfalse, (unsigned int *)state, (unsigned int *)source, sizeof( *state ) / 4, stream ); +} + +/* +================== +Record_DecodeVisibilityState +================== +*/ +void Record_DecodeVisibilityState( record_visibility_state_t *state, record_data_stream_t *stream ) { + Record_DecodeStructure( qfalse, (unsigned int *)state, sizeof( *state ) / 4, stream ); +} + +// ***** Usercmd States ***** + +/* +================== +Record_EncodeUsercmd +================== +*/ +void Record_EncodeUsercmd( usercmd_t *state, usercmd_t *source, record_data_stream_t *stream ) { + Record_EncodeStructure( qfalse, (unsigned int *)state, (unsigned int *)source, sizeof( *state ) / 4, stream ); +} + +/* +================== +Record_DecodeUsercmd +================== +*/ +void Record_DecodeUsercmd( usercmd_t *state, record_data_stream_t *stream ) { + Record_DecodeStructure( qfalse, (unsigned int *)state, sizeof( *state ) / 4, stream ); +} + +/* ******************************************************************************** */ +// Entity Set Building +/* ******************************************************************************** */ + +/* +================== +Record_GetCurrentEntities +================== +*/ +void Record_GetCurrentEntities( record_entityset_t *target ) { + int i; + if ( sv.num_entities > MAX_GENTITIES ) { + Record_Printf( RP_ALL, "Record_GetCurrentEntities: sv.num_entities > MAX_GENTITIES\n" ); + return; + } + + memset( target->activeFlags, 0, sizeof( target->activeFlags ) ); + + for ( i = 0; i < sv.num_entities; ++i ) { + sharedEntity_t *ent = SV_GentityNum( i ); + if ( !ent->r.linked ) + continue; + if ( ent->s.number != i ) { + Record_Printf( RP_DEBUG, "Record_GetCurrentEntities: bad ent->s.number\n" ); + continue; + } + target->entities[i] = ent->s; + Record_Bit_Set( target->activeFlags, i ); + } +} + +/* +================== +Record_GetCurrentBaselines +================== +*/ +void Record_GetCurrentBaselines( record_entityset_t *target ) { + int i; + memset( target->activeFlags, 0, sizeof( target->activeFlags ) ); + + for ( i = 0; i < MAX_GENTITIES; ++i ) { + if ( !sv.svEntities[i].baseline.number ) + continue; + if ( sv.svEntities[i].baseline.number != i ) { + Record_Printf( RP_DEBUG, "Record_GetCurrentBaselines: bad baseline number\n" ); + continue; + } + target->entities[i] = sv.svEntities[i].baseline; + Record_Bit_Set( target->activeFlags, i ); + } +} + +/* ******************************************************************************** */ +// Visibility Building +/* ******************************************************************************** */ + +/* +================== +Record_SetVisibleEntities + +Based on sv_snapshot.c->SV_AddEntitiesVisibleFromPoint +================== +*/ +static void Record_SetVisibleEntities( int clientNum, vec3_t origin, qboolean portal, record_visibility_state_t *target ) { + int e, i; + sharedEntity_t *ent; + svEntity_t *svEnt; + int l; + int clientarea, clientcluster; + int leafnum; + byte *clientpvs; + byte *bitvector; + + if ( !sv.state ) { + Record_Printf(RP_ALL, "Record_SetVisibleEntities: sv.state error\n"); + return; + } + + leafnum = CM_PointLeafnum (origin); + clientarea = CM_LeafArea (leafnum); + clientcluster = CM_LeafCluster (leafnum); + + // calculate the visible areas + target->areaVisibilitySize = CM_WriteAreaBits( (byte *)target->areaVisibility, clientarea ); + + clientpvs = CM_ClusterPVS (clientcluster); + + for ( e = 0 ; e < sv.num_entities ; e++ ) { + ent = SV_GentityNum(e); + + // never send entities that aren't linked in + if ( !ent->r.linked ) { + continue; + } + + /* + if (ent->s.number != e) { + Com_DPrintf ("FIXING ENT->S.NUMBER!!!\n"); + ent->s.number = e; + } + */ + + // entities can be flagged to explicitly not be sent to the client + if ( ent->r.svFlags & SVF_NOCLIENT ) { + continue; + } + + // entities can be flagged to be sent to only one client + if ( ent->r.svFlags & SVF_SINGLECLIENT ) { + if ( ent->r.singleClient != clientNum ) { + continue; + } + } + // entities can be flagged to be sent to everyone but one client + if ( ent->r.svFlags & SVF_NOTSINGLECLIENT ) { + if ( ent->r.singleClient == clientNum ) { + continue; + } + } + // entities can be flagged to be sent to a given mask of clients + if ( ent->r.svFlags & SVF_CLIENTMASK ) { + if (clientNum >= 32) { + Record_Printf(RP_DEBUG, "Record_SetVisibleEntities: clientNum >= 32\n"); + continue; } + if (~ent->r.singleClient & (1 << clientNum)) + continue; + } + + svEnt = SV_SvEntityForGentity( ent ); + + // don't double add an entity through portals + if ( Record_Bit_Get(target->entVisibility, e) ) { + continue; + } + + // broadcast entities are always sent + if ( ent->r.svFlags & SVF_BROADCAST ) { + Record_Bit_Set(target->entVisibility, e); + continue; + } + + // ignore if not touching a PV leaf + // check area + if ( !CM_AreasConnected( clientarea, svEnt->areanum ) ) { + // doors can legally straddle two areas, so + // we may need to check another one + if ( !CM_AreasConnected( clientarea, svEnt->areanum2 ) ) { + continue; // blocked by a door + } + } + + bitvector = clientpvs; + + // check individual leafs + if ( !svEnt->numClusters ) { + continue; + } + l = 0; + for ( i=0 ; i < svEnt->numClusters ; i++ ) { + l = svEnt->clusternums[i]; + if ( bitvector[l >> 3] & (1 << (l&7) ) ) { + break; + } + } + + // if we haven't found it to be visible, + // check overflow clusters that coudln't be stored + if ( i == svEnt->numClusters ) { + if ( svEnt->lastCluster ) { + for ( ; l <= svEnt->lastCluster ; l++ ) { + if ( bitvector[l >> 3] & (1 << (l&7) ) ) { + break; + } + } + if ( l == svEnt->lastCluster ) { + continue; // not visible + } + } else { + continue; + } + } + + // add it + Record_Bit_Set(target->entVisibility, e); + + // if it's a portal entity, add everything visible from its camera position + if ( ent->r.svFlags & SVF_PORTAL ) { + if ( ent->s.generic1 ) { + vec3_t dir; + VectorSubtract(ent->s.origin, origin, dir); + if ( VectorLengthSquared(dir) > (float) ent->s.generic1 * ent->s.generic1 ) { + continue; + } + } + Record_SetVisibleEntities( clientNum, ent->s.origin2, qtrue, target ); + } + } + + ent = SV_GentityNum( clientNum ); + // extension: merge second PVS at ent->r.s.origin2 + if ( ent->r.svFlags & SVF_SELF_PORTAL2 && !portal ) { + Record_SetVisibleEntities( clientNum, ent->r.s.origin2, qtrue, target ); + } +} + +/* +================== +Record_CalculateCurrentVisibility + +Based on sv_snapshot.c->SV_BuildClientSnapshot +================== +*/ +static void Record_CalculateCurrentVisibility( int clientNum, record_visibility_state_t *target ) { + playerState_t *ps = SV_GameClientNum( clientNum ); + vec3_t org; + + memset( target, 0, sizeof( *target ) ); + + // find the client's viewpoint + VectorCopy( ps->origin, org ); + org[2] += ps->viewheight; + + // Account for behavior of SV_BuildClientSnapshot under "never send client's own entity..." + Record_Bit_Set( target->entVisibility, ps->clientNum ); + + Record_SetVisibleEntities( ps->clientNum, org, qfalse, target ); + + Record_Bit_Unset( target->entVisibility, ps->clientNum ); +} + +/* +================== +Record_GetCurrentVisibility + +Try to get visibility from previously calculated snapshot, but if not available +(e.g. due to rate limited client) run the calculation directly +================== +*/ +void Record_GetCurrentVisibility( int clientNum, record_visibility_state_t *target ) { + int i; + client_t *client = &svs.clients[clientNum]; + clientSnapshot_t *frame = &client->frames[ ( client->netchan.outgoingSequence - 1 ) & PACKET_MASK ]; + if ( !svs.currFrame || svs.currFrame->frameNum != frame->frameNum ) { + Record_CalculateCurrentVisibility( clientNum, target ); + return; + } + + memset( target, 0, sizeof( *target ) ); + + target->areaVisibilitySize = frame->areabytes; + for ( i = 0; i < MAX_MAP_AREA_BYTES / sizeof( int ); i++ ) { + ( (int *)target->areaVisibility )[i] = ( (int *)frame->areabits )[i] ^ -1; + } + + for ( i = 0; i < frame->num_entities; ++i ) { + int num = frame->ents[i]->number; + if ( num < 0 || num >= MAX_GENTITIES ) { + Record_Printf( RP_ALL, "Record_GetCurrentVisibility: invalid entity number\n" ); + Record_CalculateCurrentVisibility( clientNum, target ); + return; + } + Record_Bit_Set( target->entVisibility, num ); + } + + if ( sv_recordVerifyData->integer ) { + record_visibility_state_t testVisibility; + Record_CalculateCurrentVisibility( clientNum, &testVisibility ); + + if ( memcmp( testVisibility.entVisibility, target->entVisibility, sizeof( testVisibility.entVisibility ) ) ) { + Record_Printf( RP_ALL, "Record_GetCurrentVisibility: entVisibility discrepancy for client %i\n", clientNum ); + } + + if ( memcmp( testVisibility.areaVisibility, target->areaVisibility, sizeof( testVisibility.areaVisibility ) ) ) { + Record_Printf( RP_ALL, "Record_GetCurrentVisibility: areaVisibility discrepancy for client %i\n", clientNum ); + } + + if ( testVisibility.areaVisibilitySize != target->areaVisibilitySize ) { + Record_Printf( RP_ALL, "Record_GetCurrentVisibility: areaVisibilitySize discrepancy for client %i\n", clientNum ); + } + } +} + +/* +================== +Record_OptimizeInactiveVisibility + +Sets bits for inactive entities that are set in the previous visibility, to reduce data usage +================== +*/ +void Record_OptimizeInactiveVisibility( record_entityset_t *entityset, record_visibility_state_t *oldVisibility, + record_visibility_state_t *source, record_visibility_state_t *target ) { + int i; + *target = *source; // Deal with non-entity stuff + for ( i = 0; i < 32; ++i ) { + // We should be able to assume no inactive entities are set as visible in the source + if ( ( source->entVisibility[i] & entityset->activeFlags[i] ) != source->entVisibility[i] ) { + Record_Printf( RP_ALL, "Record_OptimizeInactiveVisibility: inactive entity was visible in source\n" ); + } + + // Toggle visibility of inactive entities that are visible in the old visibility + target->entVisibility[i] = source->entVisibility[i] | + ( oldVisibility->entVisibility[i] & ~entityset->activeFlags[i] ); + } +} + +/* ******************************************************************************** */ +// Message Building +/* ******************************************************************************** */ + +/* +================== +Record_CalculateBaselineCutoff + +Returns first baseline index to drop due to msg overflow +================== +*/ +static int Record_CalculateBaselineCutoff( record_entityset_t *baselines, msg_t msg ) { + int i; + byte buffer[MAX_MSGLEN]; + entityState_t nullstate; + + msg.data = buffer; + Com_Memset( &nullstate, 0, sizeof( nullstate ) ); + for ( i = 0; i < MAX_GENTITIES; ++i ) { + if ( !Record_Bit_Get( baselines->activeFlags, i ) ) { + continue; + } + MSG_WriteByte( &msg, svc_baseline ); + MSG_WriteDeltaEntity( &msg, &nullstate, &baselines->entities[i], qtrue ); + if ( msg.cursize + 32 >= msg.maxsize ) { + return i; + } + } + + return MAX_GENTITIES; +} + +/* +================== +Record_WriteGamestateMessage +================== +*/ +void Record_WriteGamestateMessage( record_entityset_t *baselines, char **configstrings, int clientNum, + int serverCommandSequence, msg_t *msg, int *baselineCutoffOut ) { + int i; + entityState_t nullstate; + + MSG_WriteByte( msg, svc_gamestate ); + MSG_WriteLong( msg, serverCommandSequence ); + + // Write configstrings + for ( i = 0; i < MAX_CONFIGSTRINGS; ++i ) { + if ( !*configstrings[i] ) { + continue; + } + MSG_WriteByte( msg, svc_configstring ); + MSG_WriteShort( msg, i ); + MSG_WriteBigString( msg, configstrings[i] ); + } + + *baselineCutoffOut = Record_CalculateBaselineCutoff( baselines, *msg ); + + // Write the baselines + Com_Memset( &nullstate, 0, sizeof( nullstate ) ); + for ( i = 0; i < MAX_GENTITIES; ++i ) { + if ( !Record_Bit_Get( baselines->activeFlags, i ) ) { + continue; + } + if ( i >= *baselineCutoffOut ) { + continue; + } + MSG_WriteByte( msg, svc_baseline ); + MSG_WriteDeltaEntity( msg, &nullstate, &baselines->entities[i], qtrue ); + } + + MSG_WriteByte( msg, svc_EOF ); + + // write the client num + MSG_WriteLong( msg, clientNum ); + + // write the checksum feed + MSG_WriteLong( msg, 0 ); +} + +/* +================== +Record_WriteSnapshotMessage + +Based on sv_snapshot.c->SV_SendClientSnapshot +For non-delta snapshot, set deltaEntities, deltaVisibility, deltaPs, and deltaFrame to null +================== +*/ +void Record_WriteSnapshotMessage( record_entityset_t *entities, record_visibility_state_t *visibility, playerState_t *ps, + record_entityset_t *deltaEntities, record_visibility_state_t *deltaVisibility, playerState_t *deltaPs, + record_entityset_t *baselines, int baselineCutoff, int lastClientCommand, int deltaFrame, int snapFlags, + int svTime, msg_t *msg ) { + int i; + + MSG_WriteByte( msg, svc_snapshot ); + + MSG_WriteLong( msg, svTime ); + + // what we are delta'ing from + MSG_WriteByte( msg, deltaFrame ); + + // Write snapflags + MSG_WriteByte( msg, snapFlags ); + + // Write area visibility + { + int invertedAreaVisibility[8]; + for ( i = 0; i < 8; ++i ) { + invertedAreaVisibility[i] = ~visibility->areaVisibility[i]; + } + MSG_WriteByte( msg, visibility->areaVisibilitySize ); + MSG_WriteData( msg, invertedAreaVisibility, visibility->areaVisibilitySize ); + } + + // Write playerstate + MSG_WriteDeltaPlayerstate( msg, deltaPs, ps ); + + // Write entities + for ( i = 0; i < MAX_GENTITIES; ++i ) { + if ( Record_Bit_Get( entities->activeFlags, i ) && Record_Bit_Get( visibility->entVisibility, i ) ) { + // Active and visible entity + if ( deltaFrame && Record_Bit_Get( deltaEntities->activeFlags, i ) && Record_Bit_Get( deltaVisibility->entVisibility, i ) ) { + // Keep entity (delta from previous entity) + MSG_WriteDeltaEntity( msg, &deltaEntities->entities[i], &entities->entities[i], qfalse ); + } else { + // New entity (delta from baseline if valid) + if ( Record_Bit_Get( baselines->activeFlags, i ) && i < baselineCutoff ) { + MSG_WriteDeltaEntity( msg, &baselines->entities[i], &entities->entities[i], qtrue ); + } else { + entityState_t nullstate; + Com_Memset( &nullstate, 0, sizeof( nullstate ) ); + MSG_WriteDeltaEntity( msg, &nullstate, &entities->entities[i], qtrue ); + } + } + } else if ( deltaFrame && Record_Bit_Get( deltaEntities->activeFlags, i ) && Record_Bit_Get( deltaVisibility->entVisibility, i ) ) { + // Remove entity + MSG_WriteBits( msg, i, GENTITYNUM_BITS ); + MSG_WriteBits( msg, 1, 1 ); + } + } + + // End of entities + MSG_WriteBits( msg, ( MAX_GENTITIES - 1 ), GENTITYNUM_BITS ); +} diff --git a/code/server/sv_record_convert.c b/code/server/sv_record_convert.c new file mode 100644 index 000000000..077bd3550 --- /dev/null +++ b/code/server/sv_record_convert.c @@ -0,0 +1,631 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2017-2023 Noah Metzger (chomenor@gmail.com) + +This file is part of Quake III Arena source code. + +Quake III Arena source code is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Quake III Arena source code is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Quake III Arena source code; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +#include "sv_record_local.h" + +/* ******************************************************************************** */ +// Record Demo Writer +/* ******************************************************************************** */ + +typedef struct { + fileHandle_t demofile; + record_entityset_t baselines; + + qboolean haveDelta; + record_entityset_t deltaEntities; + record_visibility_state_t deltaVisibility; + playerState_t deltaPlayerstate; + + char pendingCommands[MAX_RELIABLE_COMMANDS][MAX_STRING_CHARS]; + int pendingCommandCount; + + int baselineCutoff; + int messageSequence; + int serverCommandSequence; + int snapflags; +} record_demo_writer_t; + +/* +================== +Record_InitializeDemoWriter + +Returns qtrue on success, qfalse otherwise +In the event of qtrue, stream needs to be freed by Record_CloseDemoWriter +================== +*/ +static qboolean Record_InitializeDemoWriter( record_demo_writer_t *rdw, const char *path ) { + Com_Memset( rdw, 0, sizeof( *rdw ) ); + + rdw->demofile = FS_FOpenFileWrite( path ); + if ( !rdw->demofile ) { + Record_Printf( RP_ALL, "Record_InitializeDemoWriter: failed to open file\n" ); + return qfalse; + } + + rdw->messageSequence = 1; + return qtrue; +} + +/* +================== +Record_CloseDemoWriter +================== +*/ +static void Record_CloseDemoWriter( record_demo_writer_t *rdw ) { + FS_FCloseFile( rdw->demofile ); +} + +/* +================== +Record_FinishDemoMessage + +From sv_net_chan->SV_Netchan_Transmit +================== +*/ +static void Record_FinishDemoMessage( msg_t *msg, record_demo_writer_t *rdw ) { + MSG_WriteByte( msg, svc_EOF ); + + FS_Write( &rdw->messageSequence, 4, rdw->demofile ); + ++rdw->messageSequence; + + FS_Write( &msg->cursize, 4, rdw->demofile ); + FS_Write( msg->data, msg->cursize, rdw->demofile ); +} + +/* +================== +Record_WriteDemoGamestate + +Based on cl_main.c->CL_Record_f +================== +*/ +static void Record_WriteDemoGamestate( record_entityset_t *baselines, char **configstrings, + int clientNum, record_demo_writer_t *rdw ) { + byte buffer[MAX_MSGLEN]; + msg_t msg; + + // Delta from baselines for next snapshot + rdw->haveDelta = qfalse; + rdw->baselines = *baselines; + + MSG_Init( &msg, buffer, sizeof( buffer ) ); + + MSG_WriteLong( &msg, 0 ); + + Record_WriteGamestateMessage( baselines, configstrings, clientNum, rdw->serverCommandSequence, + &msg, &rdw->baselineCutoff ); + + Record_FinishDemoMessage( &msg, rdw ); +} + +/* +================== +Record_WriteDemoSvcmd +================== +*/ +static void Record_WriteDemoSvcmd( char *command, record_demo_writer_t *rdw ) { + if ( rdw->pendingCommandCount >= ARRAY_LEN( rdw->pendingCommands ) ) { + Record_Printf( RP_ALL, "Record_WriteDemoSvcmd: pending command overflow\n" ); + return; + } + + Q_strncpyz( rdw->pendingCommands[rdw->pendingCommandCount], command, sizeof( *rdw->pendingCommands ) ); + ++rdw->pendingCommandCount; +} + +/* +================== +Record_WriteDemoSnapshot + +Based on sv.snapshot.c->SV_SendClientSnapshot +================== +*/ +static void Record_WriteDemoSnapshot( record_entityset_t *entities, record_visibility_state_t *visibility, + playerState_t *ps, int svTime, record_demo_writer_t *rdw ) { + int i; + byte buffer[MAX_MSGLEN]; + msg_t msg; + + MSG_Init( &msg, buffer, sizeof( buffer ) ); + + MSG_WriteLong( &msg, 0 ); + + // send any reliable server commands + for ( i = 0; i < rdw->pendingCommandCount; ++i ) { + MSG_WriteByte( &msg, svc_serverCommand ); + MSG_WriteLong( &msg, ++rdw->serverCommandSequence ); + MSG_WriteString( &msg, rdw->pendingCommands[i] ); + } + rdw->pendingCommandCount = 0; + + // Write the snapshot + if ( rdw->haveDelta ) { + Record_WriteSnapshotMessage( entities, visibility, ps, &rdw->deltaEntities, &rdw->deltaVisibility, + &rdw->deltaPlayerstate, &rdw->baselines, rdw->baselineCutoff, 0, 1, rdw->snapflags, svTime, &msg ); + } else { + Record_WriteSnapshotMessage( entities, visibility, ps, 0, 0, 0, &rdw->baselines, rdw->baselineCutoff, 0, 0, + rdw->snapflags, svTime, &msg ); + } + + // Store delta for next frame + rdw->deltaEntities = *entities; + rdw->deltaVisibility = *visibility; + rdw->deltaPlayerstate = *ps; + rdw->haveDelta = qtrue; + + Record_FinishDemoMessage( &msg, rdw ); +} + +/* +================== +Record_WriteDemoMapRestart +================== +*/ +static void Record_WriteDemoMapRestart( record_demo_writer_t *rdw ) { + rdw->snapflags ^= SNAPFLAG_SERVERCOUNT; +} + +/* ******************************************************************************** */ +// Record Stream Reader +/* ******************************************************************************** */ + +typedef struct { + record_data_stream_t stream; + record_state_t *rs; + + record_command_t command; + int time; + int clientNum; +} record_stream_reader_t; + +/* +================== +Record_StreamReader_LoadFile + +Returns qtrue on success, qfalse otherwise +If qtrue returned, call free on stream->data +================== +*/ +static qboolean Record_StreamReader_LoadFile( fileHandle_t fp, record_data_stream_t *stream ) { + FS_Seek( fp, 0, FS_SEEK_END ); + stream->size = FS_FTell( fp ); + if ( !stream->size ) { + return qfalse; + } + stream->data = (char *)Record_Calloc( stream->size ); + + FS_Seek( fp, 0, FS_SEEK_SET ); + FS_Read( stream->data, stream->size, fp ); + stream->position = 0; + return qtrue; +} + +/* +================== +Record_StreamReader_Init + +Returns qtrue on success, qfalse otherwise +If qtrue returned, stream needs to be freed by Record_StreamReader_Close +================== +*/ +static qboolean Record_StreamReader_Init( record_stream_reader_t *rsr, const char *path ) { + fileHandle_t fp = 0; + int size; + char *protocol; + int maxClients; + + Com_Memset( rsr, 0, sizeof( *rsr ) ); + + FS_SV_FOpenFileRead( path, &fp ); + if ( !fp ) { + Record_Printf( RP_ALL, "Record_StreamReader_Init: failed to open source file\n" ); + return qfalse; + } + + if ( !Record_StreamReader_LoadFile( fp, &rsr->stream ) ) { + Record_Printf( RP_ALL, "Record_StreamReader_Init: failed to read source file\n" ); + FS_FCloseFile( fp ); + return qfalse; + } + FS_FCloseFile( fp ); + + if ( rsr->stream.size < 8 ) { + Record_Printf( RP_ALL, "Record_StreamReader_Init: invalid source file length\n" ); + Record_Free( rsr->stream.data ); + return qfalse; + } + + // verify protocol version + size = *(int *)Record_Stream_ReadStatic( 4, &rsr->stream ); + if ( size != sizeof( RECORD_PROTOCOL ) - 1 ) { + Record_Printf( RP_ALL, "Record_StreamReader_Init: record stream has wrong protocol length\n" ); + Record_Free( rsr->stream.data ); + return qfalse; + } + protocol = Record_Stream_ReadStatic( size, &rsr->stream ); + if ( memcmp( protocol, RECORD_PROTOCOL, size ) ) { + Record_Printf( RP_ALL, "Record_StreamReader_Init: record stream has wrong protocol string\n" ); + Record_Free( rsr->stream.data ); + return qfalse; + } + // read past optional auxiliary field + size = *(int *)Record_Stream_ReadStatic( 4, &rsr->stream ); + Record_Stream_ReadStatic( size, &rsr->stream ); + + maxClients = *(int *)Record_Stream_ReadStatic( 4, &rsr->stream ); + if ( maxClients < 1 || maxClients > RECORD_MAX_CLIENTS ) { + Record_Printf( RP_ALL, "Record_StreamReader_Init: bad maxClients\n" ); + Record_Free( rsr->stream.data ); + return qfalse; + } + + rsr->rs = Record_AllocateState( maxClients ); + Record_Printf( RP_DEBUG, "stream reader initialized with %i maxClients\n", maxClients ); + return qtrue; +} + +/* +================== +Record_StreamReader_Close +================== +*/ +static void Record_StreamReader_Close( record_stream_reader_t *rsr ) { + Record_Free( rsr->stream.data ); + Record_FreeState( rsr->rs ); +} + +/* +================== +Record_StreamReader_SetClientnum +================== +*/ +static void Record_StreamReader_SetClientnum( record_stream_reader_t *rsr, int clientNum ) { + if ( clientNum < 0 || clientNum >= rsr->rs->maxClients ) { + Record_Stream_Error( &rsr->stream, "Record_StreamReader_SetClientnum: invalid clientnum" ); + } + rsr->clientNum = clientNum; +} + +/* +================== +Record_StreamReader_Advance + +Returns qtrue on success, qfalse on error or end of stream +================== +*/ +static qboolean Record_StreamReader_Advance( record_stream_reader_t *rsr ) { + if ( rsr->stream.position >= rsr->stream.size ) { + return qfalse; + } + rsr->command = (record_command_t)*(unsigned char *)Record_Stream_ReadStatic( 1, &rsr->stream ); + + switch ( rsr->command ) { + case RC_MISC_COMMAND: { + int size = *(unsigned char *)Record_Stream_ReadStatic( 1, &rsr->stream ); + if ( size == 255 ) { + size = *(unsigned short *)Record_Stream_ReadStatic( 2, &rsr->stream ); + } + Record_Stream_ReadStatic( size, &rsr->stream ); + break; + } + + case RC_STATE_ENTITY_SET: + Record_DecodeEntityset( &rsr->rs->entities, &rsr->stream ); + break; + case RC_STATE_PLAYERSTATE: + Record_StreamReader_SetClientnum( rsr, *(unsigned char *)Record_Stream_ReadStatic( 1, &rsr->stream ) ); + Record_DecodePlayerstate( &rsr->rs->clients[rsr->clientNum].playerstate, &rsr->stream ); + break; + case RC_STATE_VISIBILITY: + Record_StreamReader_SetClientnum( rsr, *(unsigned char *)Record_Stream_ReadStatic( 1, &rsr->stream ) ); + Record_DecodeVisibilityState( &rsr->rs->clients[rsr->clientNum].visibility, &rsr->stream ); + break; + case RC_STATE_USERCMD: + Record_StreamReader_SetClientnum( rsr, *(unsigned char *)Record_Stream_ReadStatic( 1, &rsr->stream ) ); + Record_DecodeUsercmd( &rsr->rs->clients[rsr->clientNum].usercmd, &rsr->stream ); + break; + case RC_STATE_CONFIGSTRING: { + int index = *(unsigned short *)Record_Stream_ReadStatic( 2, &rsr->stream ); + char *string = Record_DecodeString( &rsr->stream ); + Z_Free( rsr->rs->configstrings[index] ); + rsr->rs->configstrings[index] = CopyString( string ); + break; + } + case RC_STATE_CURRENT_SERVERCMD: { + char *string = Record_DecodeString( &rsr->stream ); + Z_Free( rsr->rs->currentServercmd ); + rsr->rs->currentServercmd = CopyString( string ); + break; + } + + case RC_EVENT_SNAPSHOT: + rsr->time = *(int *)Record_Stream_ReadStatic( 4, &rsr->stream ); + break; + case RC_EVENT_SERVERCMD: + case RC_EVENT_CLIENT_ENTER_WORLD: + case RC_EVENT_CLIENT_DISCONNECT: + Record_StreamReader_SetClientnum( rsr, *(unsigned char *)Record_Stream_ReadStatic( 1, &rsr->stream ) ); + break; + case RC_EVENT_BASELINES: + case RC_EVENT_MAP_RESTART: + break; + + default: + Record_Printf( RP_ALL, "Record_StreamReader_Advance: unknown command %i\n", rsr->command ); + return qfalse; + } + + return qtrue; +} + +/* ******************************************************************************** */ +// Record Conversion +/* ******************************************************************************** */ + +typedef enum { + CSTATE_NOT_STARTED, // Gamestate not written yet + CSTATE_CONVERTING, // Gamestate written, write snapshots + CSTATE_FINISHED // Finished, don't write anything more +} record_conversion_state_t; + +typedef struct { + int clientNum; + int instanceWait; + int firingTime; // For weapon timing + record_conversion_state_t state; + record_entityset_t baselines; + record_stream_reader_t rsr; + record_demo_writer_t rdw; + int frameCount; +} record_conversion_handler_t; + +/* +================== +Record_Convert_Process +================== +*/ +static void Record_Convert_Process( record_conversion_handler_t *rch ) { + rch->rsr.stream.abortSet = qtrue; + if ( setjmp( rch->rsr.stream.abort ) ) { + return; + } + + while ( Record_StreamReader_Advance( &rch->rsr ) ) { + switch ( rch->rsr.command ) { + case RC_EVENT_BASELINES: + rch->baselines = rch->rsr.rs->entities; + break; + + case RC_EVENT_SNAPSHOT: + if ( rch->state == CSTATE_CONVERTING ) { + playerState_t ps = rch->rsr.rs->clients[rch->clientNum].playerstate; + if ( sv_recordConvertSimulateFollow->integer ) { + Record_SetPlayerstateFollowFlag( &ps ); + } + Record_WriteDemoSnapshot( &rch->rsr.rs->entities, &rch->rsr.rs->clients[rch->clientNum].visibility, + &ps, rch->rsr.time, &rch->rdw ); + ++rch->frameCount; + } + break; + + case RC_EVENT_SERVERCMD: + if ( rch->state == CSTATE_CONVERTING && rch->rsr.clientNum == rch->clientNum ) { + Record_WriteDemoSvcmd( rch->rsr.rs->currentServercmd, &rch->rdw ); + } + break; + + case RC_STATE_USERCMD: + if ( rch->state == CSTATE_CONVERTING && rch->rsr.clientNum == rch->clientNum && + sv_recordConvertWeptiming->integer ) { + usercmd_t *usercmd = &rch->rsr.rs->clients[rch->clientNum].usercmd; + if ( Record_UsercmdIsFiringWeapon( usercmd ) ) { + if ( !rch->firingTime ) { + Record_WriteDemoSvcmd( "print \"Firing\n\"", &rch->rdw ); + rch->firingTime = usercmd->serverTime; + } + } else { + if ( rch->firingTime ) { + char buffer[128]; + Com_sprintf( buffer, sizeof( buffer ), "print \"Ceased %i\n\"", + usercmd->serverTime - rch->firingTime ); + Record_WriteDemoSvcmd( buffer, &rch->rdw ); + rch->firingTime = 0; + } + } + } + break; + + case RC_EVENT_MAP_RESTART: + if ( rch->state == CSTATE_CONVERTING ) { + Record_WriteDemoMapRestart( &rch->rdw ); + } + break; + + case RC_EVENT_CLIENT_ENTER_WORLD: + if ( rch->state == CSTATE_NOT_STARTED && rch->rsr.clientNum == rch->clientNum ) { + if ( rch->instanceWait ) { + --rch->instanceWait; + } else { + // Start encoding + Record_WriteDemoGamestate( &rch->baselines, rch->rsr.rs->configstrings, rch->clientNum, &rch->rdw ); + rch->state = CSTATE_CONVERTING; + } + } + break; + + case RC_EVENT_CLIENT_DISCONNECT: + if ( rch->state == CSTATE_CONVERTING && rch->rsr.clientNum == rch->clientNum ) { + // Stop encoding + rch->state = CSTATE_FINISHED; + } + break; + + default: + break; + } + } + + rch->rsr.stream.abortSet = qfalse; +} + +/* +================== +Record_Convert_Run +================== +*/ +static void Record_Convert_Run( const char *path, int clientNum, int instance ) { + const char *output_path = "demos/output.dm_" XSTRING( OLD_PROTOCOL_VERSION ); + record_conversion_handler_t *rch; + + rch = (record_conversion_handler_t *)Record_Calloc( sizeof( *rch ) ); + rch->clientNum = clientNum; + rch->instanceWait = instance; + + if ( !Record_StreamReader_Init( &rch->rsr, path ) ) { + Record_Free( rch ); + return; + } + + if ( !Record_InitializeDemoWriter( &rch->rdw, output_path ) ) { + Record_StreamReader_Close( &rch->rsr ); + Record_Free( rch ); + return; + } + + Record_Convert_Process( rch ); + + if ( rch->state == CSTATE_NOT_STARTED ) { + Record_Printf( RP_ALL, "failed to locate session; check client and instance parameters\n" + "use record_scan command to show available client and instance options\n" ); + } else { + if ( rch->state == CSTATE_CONVERTING ) { + Record_Printf( RP_ALL, "failed to reach disconnect marker; demo may be incomplete\n" ); + } + Record_Printf( RP_ALL, "%i frames written to %s\n", rch->frameCount, output_path ); + } + + Record_CloseDemoWriter( &rch->rdw ); + Record_StreamReader_Close( &rch->rsr ); + Record_Free( rch ); +} + +/* +================== +Record_Convert_Cmd +================== +*/ +void Record_Convert_Cmd( void ) { + char path[128]; + + if ( Cmd_Argc() < 2 ) { + Record_Printf( RP_ALL, "Usage: record_convert \n" + "Example: record_convert source.rec 0 0\n" ); + return; + } + + Com_sprintf( path, sizeof( path ), "records/%s", Cmd_Argv( 1 ) ); + COM_DefaultExtension( path, sizeof( path ), ".rec" ); + if ( strstr( path, ".." ) ) { + Record_Printf( RP_ALL, "Invalid path\n" ); + return; + } + + Record_Convert_Run( path, atoi( Cmd_Argv( 2 ) ), atoi( Cmd_Argv( 3 ) ) ); +} + +/* ******************************************************************************** */ +// Record Scanning +/* ******************************************************************************** */ + +/* +================== +Record_Scan_ProcessStream +================== +*/ +static void Record_Scan_ProcessStream( record_stream_reader_t *rsr ) { + int instance_counts[RECORD_MAX_CLIENTS]; + + rsr->stream.abortSet = qtrue; + if ( setjmp( rsr->stream.abort ) ) { + return; + } + + Com_Memset( instance_counts, 0, sizeof( instance_counts ) ); + + while ( Record_StreamReader_Advance( rsr ) ) { + switch ( rsr->command ) { + case RC_EVENT_CLIENT_ENTER_WORLD: + Record_Printf( RP_ALL, "client(%i) instance(%i)\n", rsr->clientNum, + instance_counts[rsr->clientNum] ); + ++instance_counts[rsr->clientNum]; + break; + default: + break; + } + } + + rsr->stream.abortSet = qfalse; +} + +/* +================== +Record_Scan_Run +================== +*/ +static void Record_Scan_Run( const char *path ) { + record_stream_reader_t *rsr = (record_stream_reader_t *)Record_Calloc( sizeof( *rsr ) ); + + if ( !Record_StreamReader_Init( rsr, path ) ) { + Record_Free( rsr ); + return; + } + + Record_Scan_ProcessStream( rsr ); + + Record_StreamReader_Close( rsr ); + Record_Free( rsr ); +} + +/* +================== +Record_Scan_Cmd +================== +*/ +void Record_Scan_Cmd( void ) { + char path[128]; + + if ( Cmd_Argc() < 2 ) { + Record_Printf( RP_ALL, "Usage: record_scan \n" + "Example: record_scan source.rec\n" ); + return; + } + + Com_sprintf( path, sizeof( path ), "records/%s", Cmd_Argv( 1 ) ); + COM_DefaultExtension( path, sizeof( path ), ".rec" ); + if ( strstr( path, ".." ) ) { + Record_Printf( RP_ALL, "Invalid path\n" ); + return; + } + + Record_Scan_Run( path ); +} diff --git a/code/server/sv_record_local.h b/code/server/sv_record_local.h new file mode 100644 index 000000000..fefd4b772 --- /dev/null +++ b/code/server/sv_record_local.h @@ -0,0 +1,223 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2017-2023 Noah Metzger (chomenor@gmail.com) + +This file is part of Quake III Arena source code. + +Quake III Arena source code is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Quake III Arena source code is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Quake III Arena source code; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +#include "server.h" +#include + +/* ******************************************************************************** */ +// Definitions +/* ******************************************************************************** */ + +#define RECORD_PROTOCOL "quake3-v1" + +#define RECORD_MAX_CLIENTS 256 + +typedef struct { + char *data; + unsigned int position; + unsigned int size; + + // Overflow abort + qboolean abortSet; + jmp_buf abort; +} record_data_stream_t; + +typedef enum { + RP_ALL, + RP_DEBUG +} record_print_mode_t; + +typedef struct { + int activeFlags[(MAX_GENTITIES+31)/32]; + entityState_t entities[MAX_GENTITIES]; +} record_entityset_t; + +typedef struct { + int entVisibility[(MAX_GENTITIES+31)/32]; + int areaVisibility[8]; + int areaVisibilitySize; +} record_visibility_state_t; + +typedef struct { + playerState_t playerstate; + record_visibility_state_t visibility; + usercmd_t usercmd; +} record_state_client_t; + +typedef struct { + // Holds current data state of the record stream for both recording and playback + record_entityset_t entities; + record_state_client_t *clients; + int maxClients; + char *configstrings[MAX_CONFIGSTRINGS]; + char *currentServercmd; +} record_state_t; + +typedef enum { + // Optional command which includes the message size, to allow adding data + // in the future without breaking compatibility + RC_MISC_COMMAND = 32, + + // State + RC_STATE_ENTITY_SET, + RC_STATE_PLAYERSTATE, + RC_STATE_VISIBILITY, + RC_STATE_USERCMD, + RC_STATE_CONFIGSTRING, + RC_STATE_CURRENT_SERVERCMD, + + // Events + RC_EVENT_BASELINES, + RC_EVENT_SNAPSHOT, + RC_EVENT_SERVERCMD, + RC_EVENT_CLIENT_ENTER_WORLD, + RC_EVENT_CLIENT_DISCONNECT, + RC_EVENT_MAP_RESTART, +} record_command_t; + +/* ******************************************************************************** */ +// Main +/* ******************************************************************************** */ + +extern cvar_t *sv_adminSpectatorPassword; +extern cvar_t *sv_adminSpectatorSlots; + +extern cvar_t *sv_recordAutoRecording; +extern cvar_t *sv_recordFilenameIncludeMap; +extern cvar_t *sv_recordFullBotData; +extern cvar_t *sv_recordFullUsercmdData; + +extern cvar_t *sv_recordConvertWeptiming; +extern cvar_t *sv_recordConvertSimulateFollow; + +extern cvar_t *sv_recordVerifyData; +extern cvar_t *sv_recordDebug; + +/* ******************************************************************************** */ +// Writer +/* ******************************************************************************** */ + +void Record_Writer_ProcessUsercmd( usercmd_t *usercmd, int clientNum ); +void Record_Writer_ProcessConfigstring( int index, const char *value ); +void Record_Writer_ProcessServercmd( int clientNum, const char *value ); +void Record_Writer_ProcessSnapshot( void ); +void Record_StopWriter( void ); +void Record_StartCmd( void ); +void Record_StopCmd( void ); + +/* ******************************************************************************** */ +// Convert +/* ******************************************************************************** */ + +void Record_Convert_Cmd( void ); +void Record_Scan_Cmd( void ); + +/* ******************************************************************************** */ +// Spectator +/* ******************************************************************************** */ + +void Record_Spectator_PrintStatus( void ); +void Record_Spectator_ProcessSnapshot( void ); +qboolean Record_Spectator_ProcessConnection( const netadr_t *address, const char *userinfo, int challenge, + int qport, qboolean compat ); +qboolean Record_Spectator_ProcessPacketEvent( const netadr_t *address, msg_t *msg, int qport ); +void Record_Spectator_ProcessMapLoaded( void ); +void Record_Spectator_ProcessConfigstring( int index, const char *value ); +void Record_Spectator_ProcessServercmd( int clientNum, const char *value ); +void Record_Spectator_ProcessUsercmd( int clientNum, usercmd_t *usercmd ); + +/* ******************************************************************************** */ +// Common +/* ******************************************************************************** */ + +// ***** Data Stream ***** + +void Record_Stream_Error( record_data_stream_t *stream, const char *message ); +char *Record_Stream_Allocate( int size, record_data_stream_t *stream ); +void Record_Stream_Write( void *data, int size, record_data_stream_t *stream ); +void Record_Stream_WriteValue( int value, int size, record_data_stream_t *stream ); +char *Record_Stream_ReadStatic( int size, record_data_stream_t *stream ); +void Record_Stream_ReadBuffer( void *output, int size, record_data_stream_t *stream ); +void Record_Stream_DumpToFile( record_data_stream_t *stream, fileHandle_t file ); + +// ***** Memory Allocation ***** + +void *Record_Calloc( unsigned int size ); +void Record_Free( void *ptr ); + +// ***** Bit Operations ***** + +void Record_Bit_Set( int *target, int position ); +void Record_Bit_Unset( int *target, int position ); +int Record_Bit_Get( int *source, int position ); + +// ***** Flag Operations ***** + +qboolean Record_UsercmdIsFiringWeapon( const usercmd_t *cmd ); +qboolean Record_PlayerstateIsSpectator( const playerState_t *ps ); +void Record_SetPlayerstateFollowFlag( playerState_t *ps ); + +// ***** Message Printing ***** + +void QDECL Record_Printf( record_print_mode_t mode, const char *fmt, ... ) __attribute__( ( format( printf, 2, 3 ) ) ); + +// ***** Record State ***** + +record_state_t *Record_AllocateState( int maxClients ); +void Record_FreeState( record_state_t *rs ); + +// ***** Structure Encoding/Decoding Functions ***** + +void Record_EncodeString( char *string, record_data_stream_t *stream ); +char *Record_DecodeString( record_data_stream_t *stream ); +void Record_EncodePlayerstate( playerState_t *state, playerState_t *source, record_data_stream_t *stream ); +void Record_DecodePlayerstate( playerState_t *state, record_data_stream_t *stream ); +void Record_EncodeEntitystate( entityState_t *state, entityState_t *source, record_data_stream_t *stream ); +void Record_DecodeEntitystate( entityState_t *state, record_data_stream_t *stream ); +void Record_EncodeEntityset( record_entityset_t *state, record_entityset_t *source, record_data_stream_t *stream ); +void Record_DecodeEntityset( record_entityset_t *state, record_data_stream_t *stream ); +void Record_EncodeVisibilityState( record_visibility_state_t *state, record_visibility_state_t *source, + record_data_stream_t *stream ); +void Record_DecodeVisibilityState( record_visibility_state_t *state, record_data_stream_t *stream ); +void Record_EncodeUsercmd( usercmd_t *state, usercmd_t *source, record_data_stream_t *stream ); +void Record_DecodeUsercmd( usercmd_t *state, record_data_stream_t *stream ); + +// ***** Entity Set Building ***** + +void Record_GetCurrentEntities( record_entityset_t *target ); +void Record_GetCurrentBaselines( record_entityset_t *target ); + +// ***** Visibility Building ***** + +void Record_GetCurrentVisibility( int clientNum, record_visibility_state_t *target ); +void Record_OptimizeInactiveVisibility( record_entityset_t *entityset, record_visibility_state_t *delta, + record_visibility_state_t *source, record_visibility_state_t *target ); + +// ***** Message Building ***** + +void Record_WriteGamestateMessage( record_entityset_t *baselines, char **configstrings, int clientNum, + int serverCommandSequence, msg_t *msg, int *baselineCutoffOut ); +void Record_WriteSnapshotMessage( record_entityset_t *entities, record_visibility_state_t *visibility, playerState_t *ps, + record_entityset_t *deltaEntities, record_visibility_state_t *deltaVisibility, playerState_t *deltaPs, + record_entityset_t *baselines, int baselineCutoff, int lastClientCommand, int deltaFrame, int snapFlags, + int svTime, msg_t *msg ); diff --git a/code/server/sv_record_main.c b/code/server/sv_record_main.c new file mode 100644 index 000000000..ad7be6c14 --- /dev/null +++ b/code/server/sv_record_main.c @@ -0,0 +1,198 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2017-2023 Noah Metzger (chomenor@gmail.com) + +This file is part of Quake III Arena source code. + +Quake III Arena source code is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Quake III Arena source code is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Quake III Arena source code; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +#include "sv_record_local.h" + +static qboolean recordInitialized = qfalse; + +cvar_t *sv_adminSpectatorPassword; +cvar_t *sv_adminSpectatorSlots; + +cvar_t *sv_recordAutoRecording; +cvar_t *sv_recordFilenameIncludeMap; +cvar_t *sv_recordFullBotData; +cvar_t *sv_recordFullUsercmdData; + +cvar_t *sv_recordConvertWeptiming; +cvar_t *sv_recordConvertSimulateFollow; + +cvar_t *sv_recordDebug; +cvar_t *sv_recordVerifyData; + +/* ******************************************************************************** */ +// Server Calls +/* ******************************************************************************** */ + +/* +================== +Record_ProcessUsercmd +================== +*/ +void Record_ProcessUsercmd( int clientNum, usercmd_t *usercmd ) { + if ( !recordInitialized ) { + return; + } + Record_Spectator_ProcessUsercmd( clientNum, usercmd ); + Record_Writer_ProcessUsercmd( usercmd, clientNum ); +} + +/* +================== +Record_ProcessConfigstring +================== +*/ +void Record_ProcessConfigstring( int index, const char *value ) { + if ( !recordInitialized ) { + return; + } + Record_Spectator_ProcessConfigstring( index, value ); + Record_Writer_ProcessConfigstring( index, value ); +} + +/* +================== +Record_ProcessServercmd +================== +*/ +void Record_ProcessServercmd( int clientNum, const char *value ) { + if ( !recordInitialized ) { + return; + } + Record_Spectator_ProcessServercmd( clientNum, value ); + Record_Writer_ProcessServercmd( clientNum, value ); +} + +/* +================== +Record_ProcessMapLoaded +================== +*/ +void Record_ProcessMapLoaded( void ) { + if ( !recordInitialized ) { + return; + } + Record_Spectator_ProcessMapLoaded(); +} + +/* +================== +Record_ProcessSnapshot +================== +*/ +void Record_ProcessSnapshot( void ) { + if ( !recordInitialized ) { + return; + } + Record_Spectator_ProcessSnapshot(); + Record_Writer_ProcessSnapshot(); +} + +/* +================== +Record_ProcessGameShutdown +================== +*/ +void Record_ProcessGameShutdown( void ) { + if ( !recordInitialized ) { + return; + } + Record_StopWriter(); +} + +/* +================== +Record_ProcessClientConnect + +Returns qtrue to suppress normal handling of connection, qfalse otherwise +================== +*/ +qboolean Record_ProcessClientConnect( const netadr_t *address, const char *userinfo, int challenge, + int qport, qboolean compat ) { + if ( !recordInitialized ) { + return qfalse; + } + return Record_Spectator_ProcessConnection( address, userinfo, challenge, qport, compat ); +} + +/* +================== +Record_ProcessPacketEvent + +Returns qtrue to suppress normal handling of packet, qfalse otherwise +================== +*/ +qboolean Record_ProcessPacketEvent( const netadr_t *address, msg_t *msg, int qport ) { + if ( !recordInitialized ) { + return qfalse; + } + return Record_Spectator_ProcessPacketEvent( address, msg, qport ); +} + +/* ******************************************************************************** */ +// Initialization +/* ******************************************************************************** */ + +/* +================== +Record_Initialize +================== +*/ +void Record_Initialize( void ) { + sv_adminSpectatorPassword = Cvar_Get( "sv_adminSpectatorPassword", "", 0 ); + Cvar_SetDescription( sv_adminSpectatorPassword, "Password to join game in admin spectator mode," + " or empty string to disable admin spectator support." + " On the client, set 'password' to 'spect_' plus this value to join in spectator mode."); + sv_adminSpectatorSlots = Cvar_Get( "sv_adminSpectatorSlots", "32", 0 ); + Cvar_CheckRange( sv_adminSpectatorSlots, "1", "1024", CV_INTEGER ); + Cvar_SetDescription( sv_adminSpectatorSlots, "Maximum simultaneous users in admin spectator mode." ); + + sv_recordAutoRecording = Cvar_Get( "sv_recordAutoRecording", "0", 0 ); + Cvar_SetDescription( sv_recordAutoRecording, "Enables automatic server-side recording of all games." ); + sv_recordFilenameIncludeMap = Cvar_Get( "sv_recordFilenameIncludeMap", "1", 0 ); + Cvar_SetDescription( sv_recordFilenameIncludeMap, "Add map name to server side recording filenames." ); + sv_recordFullBotData = Cvar_Get( "sv_recordFullBotData", "0", 0 ); + Cvar_SetDescription( sv_recordFullBotData, "Add record data to generate demos from bot perspective." ); + sv_recordFullUsercmdData = Cvar_Get( "sv_recordFullUsercmdData", "0", 0 ); + Cvar_SetDescription( sv_recordFullUsercmdData, "Write all usercmds to record file. Normally has no effect" + " except increasing record file size, but may be useful to advanced users." ); + + sv_recordConvertWeptiming = Cvar_Get( "sv_recordConvertWeptiming", "0", 0 ); + Cvar_SetDescription( sv_recordConvertWeptiming, "Add 'firing' and 'ceased' messages to converted demo file" + " to track fire button presses." ); + sv_recordConvertSimulateFollow = Cvar_Get( "sv_recordConvertSimulateFollow", "1", 0 ); + Cvar_SetDescription( sv_recordConvertSimulateFollow, "Add follow spectator flag to converted demo file" + " to display 'following' message and player name on screen during replay." ); + + sv_recordVerifyData = Cvar_Get( "sv_recordVerifyData", "0", 0 ); + Cvar_SetDescription( sv_recordVerifyData, "Enables extra debug checks during server-side recording." ); + sv_recordDebug = Cvar_Get( "sv_recordDebug", "0", 0 ); + Cvar_SetDescription( sv_recordDebug, "Enables additional debug prints." ); + + Cmd_AddCommand( "record_start", Record_StartCmd ); + Cmd_AddCommand( "record_stop", Record_StopCmd ); + Cmd_AddCommand( "record_convert", Record_Convert_Cmd ); + Cmd_AddCommand( "record_scan", Record_Scan_Cmd ); + Cmd_AddCommand( "spect_status", Record_Spectator_PrintStatus ); + + recordInitialized = qtrue; +} diff --git a/code/server/sv_record_spectator.c b/code/server/sv_record_spectator.c new file mode 100644 index 000000000..32747d3e9 --- /dev/null +++ b/code/server/sv_record_spectator.c @@ -0,0 +1,982 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2017-2023 Noah Metzger (chomenor@gmail.com) + +This file is part of Quake III Arena source code. + +Quake III Arena source code is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Quake III Arena source code is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Quake III Arena source code; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +#include "sv_record_local.h" + +/* ******************************************************************************** */ +// Definitions +/* ******************************************************************************** */ + +typedef struct { + playerState_t ps; + int frameEntitiesPosition; + record_visibility_state_t visibility; +} spectator_frame_t; + +typedef struct { + client_t cl; + int targetClient; // Client currently being spectated + spectator_frame_t frames[PACKET_BACKUP]; + int lastSnapshotSvTime; + int baselineCutoff; + int targetFiringTime; + + // Client settings + qboolean weptiming; + qboolean cycleall; +} spectator_t; + +#define FRAME_ENTITY_COUNT (PACKET_BACKUP * 2) + +typedef struct { + record_entityset_t currentBaselines; + spectator_t *spectators; + int maxSpectators; + int frameEntitiesPosition; + record_entityset_t frameEntities[FRAME_ENTITY_COUNT]; +} spectator_system_t; + +spectator_system_t *sps; + +/* ******************************************************************************** */ +// Command / configstring update handling +/* ******************************************************************************** */ + +/* +================== +Record_Spectator_AddServerCommand + +Based on sv_main.c->SV_AddServerCommand +================== +*/ +static void Record_Spectator_AddServerCommand( client_t *cl, const char *cmd ) { + int index; + ++cl->reliableSequence; + if ( cl->reliableSequence - cl->reliableAcknowledge >= MAX_RELIABLE_COMMANDS + 1 ) { + Record_Printf( RP_DEBUG, "Record_Spectator_AddServerCommand: command overflow\n" ); + return; + } + index = cl->reliableSequence & ( MAX_RELIABLE_COMMANDS - 1 ); + Q_strncpyz( cl->reliableCommands[index], cmd, sizeof( cl->reliableCommands[index] ) ); +} + +static void QDECL Record_Spectator_AddServerCmdFmt(client_t *cl, const char *fmt, ...) + __attribute__ ((format (printf, 2, 3))); + +/* +================== +Record_Spectator_AddServerCmdFmt +================== +*/ +static void QDECL Record_Spectator_AddServerCmdFmt( client_t *cl, const char *fmt, ... ) { + va_list argptr; + char message[MAX_STRING_CHARS]; + + va_start( argptr, fmt ); + Q_vsnprintf( message, sizeof( message ), fmt, argptr ); + va_end( argptr ); + + Record_Spectator_AddServerCommand( cl, message ); +} + +/* +================== +Record_Spectator_SendConfigstring + +Based on sv_init.c->SV_SendConfigstring +================== +*/ +static void Record_Spectator_SendConfigstring( client_t *cl, int index, const char *value ) { + int maxChunkSize = MAX_STRING_CHARS - 24; + int len = strlen( value ); + + if ( len >= maxChunkSize ) { + int sent = 0; + int remaining = len; + char *cmd; + char buf[MAX_STRING_CHARS]; + + while ( remaining > 0 ) { + if ( sent == 0 ) { + cmd = "bcs0"; + } else if ( remaining < maxChunkSize ) { + cmd = "bcs2"; + } else { + cmd = "bcs1"; + } + Q_strncpyz( buf, &value[sent], maxChunkSize ); + + Record_Spectator_AddServerCmdFmt( cl, "%s %i \"%s\"\n", cmd, index, buf ); + + sent += ( maxChunkSize - 1 ); + remaining -= ( maxChunkSize - 1 ); + } + } else { + // standard cs, just send it + Record_Spectator_AddServerCmdFmt( cl, "cs %i \"%s\"\n", index, value ); + } +} + +/* ******************************************************************************** */ +// Target Selection +/* ******************************************************************************** */ + +/* +================== +Record_TargetClientValid +================== +*/ +static qboolean Record_TargetClientValid( int clientnum ) { + if ( sv.state != SS_GAME || clientnum < 0 || clientnum > sv_maxclients->integer || + svs.clients[clientnum].state != CS_ACTIVE ) { + return qfalse; + } + return qtrue; +} + +/* +================== +Record_SelectTargetClient + +Returns clientnum if valid client selected, -1 otherwise +================== +*/ +static int Record_SelectTargetClient( int startIndex, qboolean cycleall ) { + int i; + if ( startIndex < 0 || startIndex >= sv_maxclients->integer ) { + startIndex = 0; + } + + for ( i = startIndex; i < startIndex + sv_maxclients->integer; ++i ) { + int clientnum = i % sv_maxclients->integer; + if ( !Record_TargetClientValid( clientnum ) ) { + continue; + } + if ( !cycleall ) { + if ( svs.clients[clientnum].netchan.remoteAddress.type == NA_BOT ) { + continue; + } + if ( Record_PlayerstateIsSpectator( SV_GameClientNum( clientnum ) ) ) { + continue; + } + } + return clientnum; + } + + if ( !cycleall ) { + return Record_SelectTargetClient( startIndex, qtrue ); + } + return -1; +} + +/* +================== +Record_AdvanceTargetClient + +Advances to next target client +Sets targetClient to -1 if no valid target available +================== +*/ +static void Record_AdvanceTargetClient( spectator_t *spectator ) { + int original_target = spectator->targetClient; + spectator->targetClient = Record_SelectTargetClient( spectator->targetClient + 1, spectator->cycleall ); + if ( spectator->targetClient >= 0 && spectator->targetClient != original_target ) { + const char *suffix = ""; + if ( Record_PlayerstateIsSpectator( SV_GameClientNum( spectator->targetClient ) ) ) { + suffix = " [SPECT]"; + } + if ( svs.clients[spectator->targetClient].netchan.remoteAddress.type == NA_BOT ) { + suffix = " [BOT]"; + } + + Record_Spectator_AddServerCmdFmt( &spectator->cl, "print \"Client(%i) Name(%s^7)%s\n\"", + spectator->targetClient, svs.clients[spectator->targetClient].name, suffix ); + } +} + +/* +================== +Record_ValidateTargetClient + +Advances target client if current one is invalid +Sets targetClient to -1 if no valid target available +================== +*/ +static void Record_ValidateTargetClient( spectator_t *spectator ) { + if ( !Record_TargetClientValid( spectator->targetClient ) ) { + Record_AdvanceTargetClient( spectator ); + } +} + +/* ******************************************************************************** */ +// Outgoing message (gamestate/snapshot) handling +/* ******************************************************************************** */ + +/* +================== +Record_InitSpectatorMessage + +Initializes a base message common to both gamestate and snapshot +================== +*/ +static void Record_InitSpectatorMessage( client_t *cl, msg_t *msg, byte *buffer, int bufferSize ) { + MSG_Init( msg, buffer, bufferSize ); + + // let the client know which reliable clientCommands we have received + MSG_WriteLong( msg, cl->lastClientCommand ); + + // Update server commands to client + // The standard non-spectator function *should* be safe to use here + SV_UpdateServerCommandsToClient( cl, msg ); +} + +/* +================== +Record_SendSpectatorGamestate + +Based on sv_client.c->SV_SendClientGameState +================== +*/ +static void Record_SendSpectatorGamestate( spectator_t *spectator ) { + client_t *cl = &spectator->cl; + msg_t msg; + byte msgBuf[MAX_MSGLEN]; + + if ( SVC_RateLimit( &cl->gamestate_rate, 4, 1000 ) ) { + return; + } + + cl->state = CS_PRIMED; + + // Note the message number to avoid further attempts to send the gamestate + // until the client acknowledges a higher message number + cl->gamestateMessageNum = cl->netchan.outgoingSequence; + + // Initialize message + Record_InitSpectatorMessage( cl, &msg, msgBuf, sizeof( msgBuf ) ); + + // Write gamestate message + Record_WriteGamestateMessage( &sps->currentBaselines, sv.configstrings, 0, cl->reliableSequence, &msg, + &spectator->baselineCutoff ); + + // Send to client + SV_SendMessageToClient( &msg, cl ); +} + +/* +================== +Record_SendSpectatorSnapshot + +Based on sv_snapshot.c->SV_SendClientSnapshot +================== +*/ +static void Record_SendSpectatorSnapshot( spectator_t *spectator ) { + client_t *cl = &spectator->cl; + msg_t msg; + byte msg_buf[MAX_MSGLEN]; + spectator_frame_t *current_frame = &spectator->frames[cl->netchan.outgoingSequence % PACKET_BACKUP]; + spectator_frame_t *delta_frame = 0; + int delta_frame_offset = 0; + int snapFlags = svs.snapFlagServerBit; + + // Advance target client if current one is invalid + Record_ValidateTargetClient( spectator ); + if ( spectator->targetClient < 0 ) { + return; + } + + // Store snapshot time in case it is needed to set oldServerTime on a map change + spectator->lastSnapshotSvTime = sv.time + cl->oldServerTime; + + // Determine snapFlags + if ( cl->state != CS_ACTIVE ) { + snapFlags |= SNAPFLAG_NOT_ACTIVE; + } + + // Set up current frame + current_frame->frameEntitiesPosition = sps->frameEntitiesPosition; + current_frame->ps = *SV_GameClientNum( spectator->targetClient ); + Record_GetCurrentVisibility( spectator->targetClient, ¤t_frame->visibility ); + + // Tweak playerstate to indicate spectator mode + Record_SetPlayerstateFollowFlag( ¤t_frame->ps ); + + // Determine delta frame + if ( cl->state == CS_ACTIVE && cl->deltaMessage > 0 ) { + delta_frame_offset = cl->netchan.outgoingSequence - cl->deltaMessage; + if ( delta_frame_offset > 0 && delta_frame_offset < PACKET_BACKUP - 3 ) { + delta_frame = &spectator->frames[cl->deltaMessage % PACKET_BACKUP]; + // Make sure delta frame references valid frame entities + // If this client skipped enough frames, the frame entities could have been overwritten + if ( sps->frameEntitiesPosition - delta_frame->frameEntitiesPosition >= FRAME_ENTITY_COUNT ) { + delta_frame = 0; + } + } + } + + // Initialize message + Record_InitSpectatorMessage( cl, &msg, msg_buf, sizeof( msg_buf ) ); + + // Write snapshot message + Record_WriteSnapshotMessage( &sps->frameEntities[current_frame->frameEntitiesPosition % FRAME_ENTITY_COUNT], + ¤t_frame->visibility, ¤t_frame->ps, + delta_frame ? &sps->frameEntities[delta_frame->frameEntitiesPosition % FRAME_ENTITY_COUNT] : 0, + delta_frame ? &delta_frame->visibility : 0, delta_frame ? &delta_frame->ps : 0, + &sps->currentBaselines, spectator->baselineCutoff, cl->lastClientCommand, + delta_frame ? delta_frame_offset : 0, snapFlags, spectator->lastSnapshotSvTime, &msg ); + + // Send to client + SV_SendMessageToClient( &msg, cl ); +} + +/* ******************************************************************************** */ +// Spectator client functions +/* ******************************************************************************** */ + +/* +================== +Record_Spectator_DropClient +================== +*/ +static void Record_Spectator_DropClient( spectator_t *spectator, const char *message ) { + client_t *cl = &spectator->cl; + if ( cl->state == CS_FREE ) { + return; + } + + if ( message ) { + Record_Spectator_AddServerCmdFmt( cl, "disconnect \"%s\"", message ); + + Record_SendSpectatorSnapshot( spectator ); + while ( cl->netchan.unsentFragments || cl->netchan_start_queue ) { + SV_Netchan_TransmitNextFragment( cl ); + } + } + + SV_Netchan_FreeQueue( cl ); + cl->state = CS_FREE; +} + +/* +================== +Record_Spectator_ProcessUserinfo + +Based on sv_client.c->SV_UserinfoChanged +Currently just sets rate +================== +*/ +static void Record_Spectator_ProcessUserinfo( spectator_t *spectator, const char *userinfo ) { + spectator->cl.rate = atoi( Info_ValueForKey( userinfo, "rate" ) ); + if ( spectator->cl.rate <= 0 ) { + spectator->cl.rate = 90000; + } else if ( spectator->cl.rate < 5000 ) { + spectator->cl.rate = 5000; + } else if ( spectator->cl.rate > 90000 ) { + spectator->cl.rate = 90000; + } +} + +/* +================== +Record_Spectator_EnterWorld + +Based on sv_client.c->SV_ClientEnterWorld +Spectators don't really enter the world, but they do need some configuration +to go to CS_ACTIVE after loading the map +================== +*/ +static void Record_Spectator_EnterWorld( spectator_t *spectator ) { + client_t *cl = &spectator->cl; + int i; + + cl->state = CS_ACTIVE; + + // Based on sv_init.c->SV_UpdateConfigstrings + for ( i = 0; i < MAX_CONFIGSTRINGS; ++i ) { + if ( cl->csUpdated[i] ) { + Record_Spectator_SendConfigstring( cl, i, sv.configstrings[i] ); + cl->csUpdated[i] = qfalse; + } + } + + cl->deltaMessage = -1; + cl->lastSnapshotTime = 0; +} + +/* +================== +Record_Spectator_Think +================== +*/ +static void Record_Spectator_Think( spectator_t *spectator, usercmd_t *cmd ) { + client_t *cl = &spectator->cl; + if ( Record_UsercmdIsFiringWeapon( cmd ) && !Record_UsercmdIsFiringWeapon( &cl->lastUsercmd ) ) { + Record_AdvanceTargetClient( spectator ); + } +} + +/* +================== +Record_Spectator_Move + +Based on sv_client.c->SV_UserMove +================== +*/ +static void Record_Spectator_Move( spectator_t *spectator, msg_t *msg, qboolean delta ) { + client_t *cl = &spectator->cl; + int i; + int key; + int cmdCount; + usercmd_t nullcmd; + usercmd_t cmds[MAX_PACKET_USERCMDS]; + usercmd_t *cmd, *oldcmd; + + if ( delta ) { + cl->deltaMessage = cl->messageAcknowledge; + } else { + cl->deltaMessage = -1; + } + + cmdCount = MSG_ReadByte( msg ); + if ( cmdCount < 1 || cmdCount > MAX_PACKET_USERCMDS ) { + Record_Printf( RP_DEBUG, "Record_Spectator_Move: invalid spectator cmdCount\n" ); + return; + } + + // use the checksum feed in the key + key = 0; + // also use the message acknowledge + key ^= cl->messageAcknowledge; + // also use the last acknowledged server command in the key + key ^= MSG_HashKey(cl->reliableCommands[ cl->reliableAcknowledge & (MAX_RELIABLE_COMMANDS-1) ], 32); + + Com_Memset( &nullcmd, 0, sizeof( nullcmd ) ); + oldcmd = &nullcmd; + for ( i = 0; i < cmdCount; i++ ) { + cmd = &cmds[i]; + MSG_ReadDeltaUsercmdKey( msg, key, oldcmd, cmd ); + oldcmd = cmd; + } + + if ( cl->state == CS_PRIMED ) { + Record_Spectator_EnterWorld( spectator ); + } + + // Handle sv.time reset on map restart etc. + if ( cl->lastUsercmd.serverTime > sv.time ) { + cl->lastUsercmd.serverTime = 0; + } + + for ( i = 0; i < cmdCount; ++i ) { + if ( cmds[i].serverTime > cmds[cmdCount - 1].serverTime ) { + continue; + } + if ( cmds[i].serverTime <= cl->lastUsercmd.serverTime ) { + continue; + } + Record_Spectator_Think( spectator, &cmds[i] ); + cl->lastUsercmd = cmds[i]; + } +} + +/* +================== +Record_Spectator_ProcessBooleanSetting +================== +*/ +static void Record_Spectator_ProcessBooleanSetting( spectator_t *spectator, const char *setting_name, qboolean *target ) { + if ( !Q_stricmp( Cmd_Argv( 1 ), "0" ) ) { + Record_Spectator_AddServerCmdFmt( &spectator->cl, "print \"%s disabled\n\"", setting_name ); + *target = qfalse; + } else if ( !Q_stricmp( Cmd_Argv( 1 ), "1" ) ) { + Record_Spectator_AddServerCmdFmt( &spectator->cl, "print \"%s enabled\n\"", setting_name ); + *target = qtrue; + } else + Record_Spectator_AddServerCmdFmt( &spectator->cl, "print \"Usage: '%s 0' or '%s 1'\n\"", + setting_name, setting_name ); +} + +/* +================== +Record_Spectator_ProcessCommand + +Based on sv_client.c->SV_ClientCommand +================== +*/ +static void Record_Spectator_ProcessCommand( spectator_t *spectator, msg_t *msg ) { + client_t *cl = &spectator->cl; + int seq = MSG_ReadLong( msg ); + const char *cmd = MSG_ReadString( msg ); + + if ( cl->lastClientCommand >= seq ) { + // Command already executed + return; + } + + if ( seq > cl->lastClientCommand + 1 ) { + // Command lost error + Record_Printf( RP_ALL, "Spectator %i lost client commands\n", (int)( spectator - sps->spectators ) ); + Record_Spectator_DropClient( spectator, "Lost reliable commands" ); + return; + } + + Record_Printf( RP_DEBUG, "Have spectator command: %s\n", cmd ); + cl->lastClientCommand = seq; + Q_strncpyz( cl->lastClientCommandString, cmd, sizeof( cl->lastClientCommandString ) ); + + Cmd_TokenizeString( cmd ); + if ( !Q_stricmp( Cmd_Argv( 0 ), "disconnect" ) ) { + Record_Printf( RP_ALL, "Spectator %i disconnected\n", (int)( spectator - sps->spectators ) ); + Record_Spectator_DropClient( spectator, "disconnected" ); + return; + } else if ( !Q_stricmp( Cmd_Argv( 0 ), "weptiming" ) ) { + Record_Spectator_ProcessBooleanSetting( spectator, "weptiming", &spectator->weptiming ); + } else if ( !Q_stricmp( Cmd_Argv( 0 ), "cycleall" ) ) { + Record_Spectator_ProcessBooleanSetting( spectator, "cycleall", &spectator->cycleall ); + } else if ( !Q_stricmp( Cmd_Argv( 0 ), "help" ) ) { + Record_Spectator_AddServerCommand( cl, "print \"Commands:\nweptiming - Enables or disables" + " weapon firing prints\ncycleall - Enables or disables selecting bot and spectator" + " target clients\n\"" ); + } else if ( !Q_stricmp( Cmd_Argv( 0 ), "userinfo" ) ) { + Record_Spectator_ProcessUserinfo( spectator, Cmd_Argv( 1 ) ); + } +} + +/* +================== +Record_Spectator_ProcessMessage + +Based on sv_client.c->SV_ExecuteClientMessage +================== +*/ +static void Record_Spectator_ProcessMessage( spectator_t *spectator, msg_t *msg ) { + client_t *cl = &spectator->cl; + int serverId; + int cmd; + + MSG_Bitstream( msg ); + + serverId = MSG_ReadLong( msg ); + cl->messageAcknowledge = MSG_ReadLong( msg ); + if ( cl->netchan.outgoingSequence - cl->messageAcknowledge <= 0 ) { + Record_Printf( RP_DEBUG, "Invalid messageAcknowledge" ); + return; + } + + cl->reliableAcknowledge = MSG_ReadLong( msg ); + if ( cl->reliableSequence - cl->reliableAcknowledge < 0 || + cl->reliableSequence - cl->reliableAcknowledge > MAX_RELIABLE_COMMANDS ) { + Record_Printf( RP_DEBUG, "Invalid reliableAcknowledge" ); + cl->reliableAcknowledge = cl->reliableSequence; + } + + if ( serverId < sv.restartedServerId || serverId > sv.serverId ) { + // Pre map change serverID, or invalid high serverID + if ( cl->messageAcknowledge > cl->gamestateMessageNum ) { + // No previous gamestate waiting to be acknowledged - send new one + Record_SendSpectatorGamestate( spectator ); + } + return; + } + + // No need to send old servertime once an up-to-date gamestate is acknowledged + cl->oldServerTime = 0; + + // Read optional client command strings + while ( 1 ) { + cmd = MSG_ReadByte( msg ); + + if ( cmd == clc_EOF ) { + return; + } + if ( cmd != clc_clientCommand ) { + break; + } + Record_Spectator_ProcessCommand( spectator, msg ); + + // In case command resulted in error/disconnection + if ( cl->state < CS_CONNECTED ) { + return; + } + } + + // Process move commands + if ( cmd == clc_move ) { + Record_Spectator_Move( spectator, msg, qtrue ); + } else if ( cmd == clc_moveNoDelta ) { + Record_Spectator_Move( spectator, msg, qfalse ); + } else { + Record_Printf( RP_DEBUG, "Record_Spectator_ProcessMessage: invalid spectator command byte\n" ); + } +} + +/* ******************************************************************************** */ +// Spectator system initialization/allocation +/* ******************************************************************************** */ + +/* +================== +Record_Spectator_Init +================== +*/ +static void Record_Spectator_Init( int maxSpectators ) { + sps = (spectator_system_t *)Record_Calloc( sizeof( *sps ) ); + sps->spectators = (spectator_t *)Record_Calloc( sizeof( *sps->spectators ) * maxSpectators ); + sps->maxSpectators = maxSpectators; + Record_GetCurrentBaselines( &sps->currentBaselines ); +} + +/* +================== +Record_Spectator_Shutdown +================== +*/ +static void Record_Spectator_Shutdown( void ) { + Record_Free( sps->spectators ); + Record_Free( sps ); + sps = 0; +} + +/* +================== +Record_Spectator_AllocateClient + +Returns either reused or new spectator on success, or null if all slots in use +Allocated structure will not have zeroed memory +================== +*/ +static spectator_t *Record_Spectator_AllocateClient( const netadr_t *address, int qport ) { + int i; + spectator_t *avail = 0; + if ( !sps ) { + Record_Spectator_Init( sv_adminSpectatorSlots->integer ); + } + for ( i = 0; i < sps->maxSpectators; ++i ) { + if ( sps->spectators[i].cl.state == CS_FREE ) { + if ( !avail ) { + avail = &sps->spectators[i]; + } + } + else if ( NET_CompareBaseAdr( address, &sps->spectators[i].cl.netchan.remoteAddress ) + && ( sps->spectators[i].cl.netchan.qport == qport || + address->port == sps->spectators[i].cl.netchan.remoteAddress.port ) ) { + Record_Spectator_DropClient( &sps->spectators[i], 0 ); + return &sps->spectators[i]; + } + } + return avail; +} + +/* ******************************************************************************** */ +// Exported functions +/* ******************************************************************************** */ + +/* +================== +Record_Spectator_PrintStatus +================== +*/ +void Record_Spectator_PrintStatus( void ) { + int i; + if ( !sps ) { + Record_Printf( RP_ALL, "No spectators; spectator system not running\n" ); + return; + } + + for ( i = 0; i < sps->maxSpectators; ++i ) { + client_t *cl = &sps->spectators[i].cl; + const char *state = "unknown"; + if ( cl->state == CS_FREE ) { + continue; + } + + if ( cl->state == CS_CONNECTED ) { + state = "connected"; + } else if ( cl->state == CS_PRIMED ) { + state = "primed"; + } else if ( cl->state == CS_ACTIVE ) { + state = "active"; + } + + Record_Printf( RP_ALL, "num(%i) address(%s) state(%s) lastmsg(%i) rate(%i)\n", i, + NET_AdrToString( &cl->netchan.remoteAddress ), state, svs.time - cl->lastPacketTime, cl->rate ); + } +} + +/* +================== +Record_Spectator_ProcessSnapshot +================== +*/ +void Record_Spectator_ProcessSnapshot( void ) { + int i; + qboolean active = qfalse; + if ( !sps ) { + return; + } + + // Add current entities to entity buffer + Record_GetCurrentEntities( &sps->frameEntities[++sps->frameEntitiesPosition % FRAME_ENTITY_COUNT] ); + + // Based on sv_snapshot.c->SV_SendClientMessages + for ( i = 0; i < sps->maxSpectators; ++i ) { + client_t *cl = &sps->spectators[i].cl; + if ( cl->state == CS_FREE ) { + continue; + } + active = qtrue; + + if ( cl->lastPacketTime > svs.time ) { + cl->lastPacketTime = svs.time; + } + if ( svs.time - cl->lastPacketTime > 60000 ) { + Record_Printf( RP_ALL, "Spectator %i timed out\n", i ); + Record_Spectator_DropClient( &sps->spectators[i], "timed out" ); + continue; + } + + if ( cl->netchan.unsentFragments || cl->netchan_start_queue ) { + SV_Netchan_TransmitNextFragment( cl ); + cl->rateDelayed = qtrue; + continue; + } + + // SV_RateMsec appears safe to call + if ( SV_RateMsec( cl ) > 0 ) { + cl->rateDelayed = qtrue; + continue; + } + + Record_SendSpectatorSnapshot( &sps->spectators[i] ); + cl->lastSnapshotTime = svs.time; + cl->rateDelayed = qfalse; + } + + if ( !active ) { + // No active spectators; free spectator system to save memory + Record_Spectator_Shutdown(); + } +} + +/* +================== +Record_Spectator_ProcessConnection + +Returns qtrue to suppress normal handling of connection, qfalse otherwise +================== +*/ +qboolean Record_Spectator_ProcessConnection( const netadr_t *address, const char *userinfo, int challenge, + int qport, qboolean compat ) { + spectator_t *spectator; + const char *password = Info_ValueForKey( userinfo, "password" ); + if ( Q_stricmpn( password, "spect_", 6 ) ) { + return qfalse; + } + + if ( !*sv_adminSpectatorPassword->string ) { + NET_OutOfBandPrint( NS_SERVER, address, "print\nSpectator mode not enabled on this server.\n" ); + return qtrue; + } + + if ( strcmp( password + 6, sv_adminSpectatorPassword->string ) ) { + NET_OutOfBandPrint( NS_SERVER, address, "print\nIncorrect spectator password.\n" ); + return qtrue; + } + + spectator = Record_Spectator_AllocateClient( address, qport ); + if ( !spectator ) { + Record_Printf( RP_ALL, "Failed to allocate spectator slot.\n" ); + NET_OutOfBandPrint( NS_SERVER, address, "print\nSpectator slots full.\n" ); + return qtrue; + } + + // Perform initializations from sv_client.c->SV_DirectConnect + Com_Memset( spectator, 0, sizeof( *spectator ) ); + spectator->targetClient = -1; + spectator->cl.challenge = challenge; + spectator->cl.compat = compat; + Netchan_Setup( NS_SERVER, &spectator->cl.netchan, address, qport, spectator->cl.challenge, compat ); + spectator->cl.netchan_end_queue = &spectator->cl.netchan_start_queue; + NET_OutOfBandPrint( NS_SERVER, address, "connectResponse %d", spectator->cl.challenge ); + spectator->cl.lastPacketTime = svs.time; + spectator->cl.gamestateMessageNum = -1; + spectator->cl.state = CS_CONNECTED; + Record_Spectator_ProcessUserinfo( spectator, userinfo ); + + Record_Spectator_AddServerCommand( &spectator->cl, "print \"Spectator mode enabled - type /help for options\n\"" ); + Record_Printf( RP_ALL, "Spectator %i connected from %s\n", (int)( spectator - sps->spectators ), NET_AdrToString( address ) ); + + return qtrue; +} + +/* +================== +Record_Spectator_ProcessPacketEvent + +Returns qtrue to suppress normal handling of packet, qfalse otherwise +Based on sv_main.c->SV_PacketEvent +================== +*/ +qboolean Record_Spectator_ProcessPacketEvent( const netadr_t *address, msg_t *msg, int qport ) { + int i; + if ( !sps ) { + return qfalse; + } + + for ( i = 0; i < sps->maxSpectators; ++i ) { + client_t *cl = &sps->spectators[i].cl; + if ( cl->state == CS_FREE ) { + continue; + } + if ( !NET_CompareBaseAdr( address, &cl->netchan.remoteAddress ) || cl->netchan.qport != qport ) { + continue; + } + + cl->netchan.remoteAddress.port = address->port; + if ( SV_Netchan_Process( cl, msg ) ) { + if ( cl->state != CS_ZOMBIE ) { + cl->lastPacketTime = svs.time; // don't timeout + Record_Spectator_ProcessMessage( &sps->spectators[i], msg ); + } + } + return qtrue; + } + + return qfalse; +} + +/* +================== +Record_Spectator_ProcessMapLoaded +================== +*/ +void Record_Spectator_ProcessMapLoaded( void ) { + int i; + if ( !sps ) { + return; + } + + // Update current baselines + Record_GetCurrentBaselines( &sps->currentBaselines ); + + for ( i = 0; i < sps->maxSpectators; ++i ) { + client_t *cl = &sps->spectators[i].cl; + if ( cl->state >= CS_CONNECTED ) { + cl->state = CS_CONNECTED; + cl->oldServerTime = sps->spectators[i].lastSnapshotSvTime; + } + } +} + +/* +================== +Record_Spectator_ProcessConfigstring +================== +*/ +void Record_Spectator_ProcessConfigstring( int index, const char *value ) { + int i; + if ( !sps ) { + return; + } + + // Based on sv_init.c->SV_SetConfigstring + if ( sv.state == SS_GAME || sv.restarting ) { + for ( i = 0; i < sps->maxSpectators; ++i ) { + client_t *cl = &sps->spectators[i].cl; + if ( cl->state == CS_ACTIVE ) { + Record_Spectator_SendConfigstring( cl, index, value ); + } else { + cl->csUpdated[index] = qtrue; + } + } + } +} + +/* +================== +Record_Spectator_ProcessServercmd +================== +*/ +void Record_Spectator_ProcessServercmd( int clientNum, const char *value ) { + int i; + if ( !sps ) { + return; + } + + if ( !Q_stricmpn( value, "cs ", 3 ) || !Q_stricmpn( value, "bcs0 ", 5 ) || !Q_stricmpn( value, "bcs1 ", 5 ) || + !Q_stricmpn( value, "bcs2 ", 5 ) || !Q_stricmpn( value, "disconnect ", 11 ) ) { + // Skip configstring updates because they are handled separately + // Also don't cause the spectator to disconnect when the followed client gets a disconnect command + return; + } + + for ( i = 0; i < sps->maxSpectators; ++i ) { + client_t *cl = &sps->spectators[i].cl; + if ( cl->state != CS_ACTIVE ) { + continue; + } + if ( sps->spectators[i].targetClient == clientNum ) { + Record_Spectator_AddServerCommand( cl, value ); + } + } +} + +/* +================== +Record_Spectator_ProcessUsercmd +================== +*/ +void Record_Spectator_ProcessUsercmd( int clientNum, usercmd_t *usercmd ) { + int i; + if ( !sps ) { + return; + } + + for ( i = 0; i < sps->maxSpectators; ++i ) { + // Send firing/ceased messages to spectators following this client with weptiming enabled + client_t *cl = &sps->spectators[i].cl; + if ( cl->state != CS_ACTIVE ) { + continue; + } + if ( sps->spectators[i].targetClient != clientNum ) { + continue; + } + + if ( Record_UsercmdIsFiringWeapon( usercmd ) ) { + if ( !sps->spectators[i].targetFiringTime ) { + if ( sps->spectators[i].weptiming ) { + Record_Spectator_AddServerCommand( cl, "print \"Firing\n\"" ); + } + sps->spectators[i].targetFiringTime = usercmd->serverTime; + } + } else { + if ( sps->spectators[i].targetFiringTime ) { + if ( sps->spectators[i].weptiming ) { + Record_Spectator_AddServerCmdFmt( cl, "print \"Ceased %i\n\"", + usercmd->serverTime - sps->spectators[i].targetFiringTime ); + } + sps->spectators[i].targetFiringTime = 0; + } + } + } +} diff --git a/code/server/sv_record_writer.c b/code/server/sv_record_writer.c new file mode 100644 index 000000000..e55d23502 --- /dev/null +++ b/code/server/sv_record_writer.c @@ -0,0 +1,650 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2017-2023 Noah Metzger (chomenor@gmail.com) + +This file is part of Quake III Arena source code. + +Quake III Arena source code is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Quake III Arena source code is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Quake III Arena source code; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +#include "sv_record_local.h" +#include + +/* ******************************************************************************** */ +// Definitions +/* ******************************************************************************** */ + +typedef struct { + qboolean autoStarted; + + record_state_t *rs; + char activePlayers[RECORD_MAX_CLIENTS]; + int lastSnapflags; + + char *targetDirectory; + char *targetFilename; + + fileHandle_t recordfile; + record_data_stream_t stream; + char streamBuffer[130000]; +} record_writer_state_t; + +record_writer_state_t *rws; + +/* ******************************************************************************** */ +// State-Updating Operations +/* ******************************************************************************** */ + +/* +================== +Record_CompareEntityStates + +Returns qtrue on discrepancy, qfalse otherwise +================== +*/ +static qboolean Record_CompareEntityStates( record_entityset_t *state1, record_entityset_t *state2, qboolean verbose ) { + qboolean discrepancy = qfalse; + int i; + for ( i = 0; i < MAX_GENTITIES; ++i ) { + if ( Record_Bit_Get( state1->activeFlags, i ) != Record_Bit_Get( state2->activeFlags, i ) ) { + if ( verbose ) { + Record_Printf( RP_ALL, "Entity %i active discrepancy\n", i ); + } + discrepancy = qtrue; + continue; + } + if ( !Record_Bit_Get( state1->activeFlags, i ) ) { + continue; + } + if ( memcmp( &state1->entities[i], &state2->entities[i], sizeof( *state1->entities ) ) ) { + if ( verbose ) { + Record_Printf( RP_ALL, "Entity %i content discrepancy\n", i ); + } + discrepancy = qtrue; + continue; + } + } + return discrepancy; +} + +/* +================== +Record_UpdateEntityset +================== +*/ +static void Record_UpdateEntityset( record_entityset_t *entities ) { + record_data_stream_t verifyStream; + record_entityset_t *verifyEntities = 0; + + Record_Stream_WriteValue( RC_STATE_ENTITY_SET, 1, &rws->stream ); + + if ( sv_recordVerifyData->integer ) { + verifyStream = rws->stream; + verifyEntities = (record_entityset_t *)Z_Malloc( sizeof( *verifyEntities ) ); + *verifyEntities = rws->rs->entities; + } + + Record_EncodeEntityset( &rws->rs->entities, entities, &rws->stream ); + + if ( sv_recordVerifyData->integer ) { + Record_DecodeEntityset( verifyEntities, &verifyStream ); + if ( verifyStream.position != rws->stream.position ) { + Record_Printf( RP_ALL, "Record_UpdateEntityset: verify stream in different position\n" ); + } else if ( Record_CompareEntityStates( entities, verifyEntities, qtrue ) ) { + Record_Printf( RP_ALL, "Record_UpdateEntityset: verify discrepancy\n" ); + } + Z_Free( verifyEntities ); + } +} + +/* +================== +Record_UpdatePlayerstate +================== +*/ +static void Record_UpdatePlayerstate( playerState_t *ps, int clientNum ) { + record_data_stream_t verifyStream; + playerState_t verifyPs; + + if ( !memcmp( ps, &rws->rs->clients[clientNum].playerstate, sizeof( *ps ) ) ) { + return; + } + + Record_Stream_WriteValue( RC_STATE_PLAYERSTATE, 1, &rws->stream ); + + // We can't rely on ps->clientNum because it can be wrong due to spectating and such + Record_Stream_WriteValue( clientNum, 1, &rws->stream ); + + if ( sv_recordVerifyData->integer ) { + verifyStream = rws->stream; + verifyPs = rws->rs->clients[clientNum].playerstate; + } + + Record_EncodePlayerstate( &rws->rs->clients[clientNum].playerstate, ps, &rws->stream ); + + if ( sv_recordVerifyData->integer ) { + Record_DecodePlayerstate( &verifyPs, &verifyStream ); + if ( verifyStream.position != rws->stream.position ) { + Record_Printf( RP_ALL, "Record_UpdatePlayerstate: verify stream in different position\n" ); + } else if ( memcmp( ps, &verifyPs, sizeof( *ps ) ) ) { + Record_Printf( RP_ALL, "Record_UpdatePlayerstate: verify discrepancy\n" ); + } + } +} + +/* +================== +Record_UpdateVisibilityState +================== +*/ +static void Record_UpdateVisibilityState( record_visibility_state_t *vs, int clientNum ) { + record_data_stream_t verifyStream; + record_visibility_state_t verifyVs; + + if ( !memcmp( vs, &rws->rs->clients[clientNum].visibility, sizeof( *vs ) ) ) { + return; + } + + Record_Stream_WriteValue( RC_STATE_VISIBILITY, 1, &rws->stream ); + Record_Stream_WriteValue( clientNum, 1, &rws->stream ); + + if ( sv_recordVerifyData->integer ) { + verifyStream = rws->stream; + verifyVs = rws->rs->clients[clientNum].visibility; + } + + Record_EncodeVisibilityState( &rws->rs->clients[clientNum].visibility, vs, &rws->stream ); + + if ( sv_recordVerifyData->integer ) { + Record_DecodeVisibilityState( &verifyVs, &verifyStream ); + if ( verifyStream.position != rws->stream.position ) { + Record_Printf( RP_ALL, "Record_UpdateVisibilityState: verify stream in different position\n" ); + } else if ( memcmp( vs, &verifyVs, sizeof( *vs ) ) ) { + Record_Printf( RP_ALL, "Record_UpdateVisibilityState: verify discrepancy\n" ); + } + } +} + +/* +================== +Record_UpdateVisibilityStateClient +================== +*/ +static void Record_UpdateVisibilityStateClient( int clientNum ) { + record_visibility_state_t vs; + record_visibility_state_t vsOptimized; + Record_GetCurrentVisibility( clientNum, &vs ); + Record_OptimizeInactiveVisibility( &rws->rs->entities, &rws->rs->clients[clientNum].visibility, &vs, &vsOptimized ); + Record_UpdateVisibilityState( &vsOptimized, clientNum ); +} + +/* +================== +Record_UpdateUsercmd +================== +*/ +static void Record_UpdateUsercmd( usercmd_t *usercmd, int clientNum ) { + record_data_stream_t verifyStream; + usercmd_t verifyUsercmd; + + Record_Stream_WriteValue( RC_STATE_USERCMD, 1, &rws->stream ); + Record_Stream_WriteValue( clientNum, 1, &rws->stream ); + + if ( sv_recordVerifyData->integer ) { + verifyStream = rws->stream; + verifyUsercmd = rws->rs->clients[clientNum].usercmd; + } + + Record_EncodeUsercmd( &rws->rs->clients[clientNum].usercmd, usercmd, &rws->stream ); + + if ( sv_recordVerifyData->integer ) { + Record_DecodeUsercmd( &verifyUsercmd, &verifyStream ); + if ( verifyStream.position != rws->stream.position ) { + Record_Printf( RP_ALL, "Record_UpdateUsercmd: verify stream in different position\n" ); + } else if ( memcmp( usercmd, &verifyUsercmd, sizeof( *usercmd ) ) ) { + Record_Printf( RP_ALL, "Record_UpdateUsercmd: verify discrepancy\n" ); + } + } +} + +/* +================== +Record_UpdateConfigstring +================== +*/ +static void Record_UpdateConfigstring( int index, char *value ) { + if ( index < 0 || index >= MAX_CONFIGSTRINGS ) { + Record_Printf( RP_ALL, "Record_UpdateConfigstring: invalid configstring index\n" ); + return; + } + + if ( !strcmp( rws->rs->configstrings[index], value ) ) { + return; + } + + Record_Stream_WriteValue( RC_STATE_CONFIGSTRING, 1, &rws->stream ); + Record_Stream_WriteValue( index, 2, &rws->stream ); + Record_EncodeString( value, &rws->stream ); + + Z_Free( rws->rs->configstrings[index] ); + rws->rs->configstrings[index] = CopyString( value ); +} + +/* +================== +Record_UpdateCurrentServercmd +================== +*/ +static void Record_UpdateCurrentServercmd( char *value ) { + if ( !strcmp( rws->rs->currentServercmd, value ) ) { + return; + } + + Record_Stream_WriteValue( RC_STATE_CURRENT_SERVERCMD, 1, &rws->stream ); + Record_EncodeString( value, &rws->stream ); + + Z_Free( rws->rs->currentServercmd ); + rws->rs->currentServercmd = CopyString( value ); +} + +/* ******************************************************************************** */ +// Recording Start/Stop Functions +/* ******************************************************************************** */ + +/* +================== +Record_DeallocateRecordWriter +================== +*/ +static void Record_DeallocateRecordWriter( void ) { + if ( !rws ) { + return; + } + if ( rws->rs ) { + Record_FreeState( rws->rs ); + } + if ( rws->targetDirectory ) { + Z_Free( rws->targetDirectory ); + } + if ( rws->targetFilename ) { + Z_Free( rws->targetFilename ); + } + Record_Free( rws ); + rws = 0; +} + +/* +================== +Record_CloseRecordWriter +================== +*/ +static void Record_CloseRecordWriter( void ) { + if ( !rws ) { + // Not supposed to happen + Record_Printf( RP_ALL, "Record_CloseRecordWriter called with record writer not initialized\n" ); + return; + } + + // Flush stream to file and close temp file + Record_Stream_DumpToFile( &rws->stream, rws->recordfile ); + FS_FCloseFile( rws->recordfile ); + + // Attempt to move the temp file to final destination + FS_SV_Rename( "records/current.rec", va( "records/%s/%s.rec", rws->targetDirectory, rws->targetFilename ) ); + + Record_DeallocateRecordWriter(); +} + +/* +================== +Record_InitializeRecordWriter +================== +*/ +static void Record_InitializeRecordWriter( int maxClients, qboolean autoStarted ) { + if ( rws ) { + // Not supposed to happen + Record_Printf( RP_ALL, "Record_InitializeRecordWriter called with record writer already initialized\n" ); + return; + } + + // Allocate the structure + rws = (record_writer_state_t *)Record_Calloc( sizeof( *rws ) ); + + // Make sure records folder exists + Sys_Mkdir( va( "%s/records", Cvar_VariableString( "fs_homepath" ) ) ); + + // Rename any existing output file that might have been left over from a crash + FS_SV_Rename( "records/current.rec", va( "records/orphan_%u.rec", rand() ) ); + + // Determine move location (targetDirectory and targetFilename) for when recording is complete + { + time_t rawtime; + struct tm *timeinfo; + + time( &rawtime ); + timeinfo = localtime( &rawtime ); + if ( !timeinfo ) { + Record_Printf( RP_ALL, "Record_InitializeRecordWriter: failed to get timeinfo\n" ); + Record_DeallocateRecordWriter(); + return; + } + + rws->targetDirectory = CopyString( va( "%i-%02i-%02i", timeinfo->tm_year + 1900, + timeinfo->tm_mon + 1, timeinfo->tm_mday ) ); + if ( sv_recordFilenameIncludeMap->integer ) { + rws->targetFilename = CopyString( va( "%02i-%02i-%02i-%s", timeinfo->tm_hour, + timeinfo->tm_min, timeinfo->tm_sec, Cvar_VariableString( "mapname" ) ) ); + } else { + rws->targetFilename = CopyString( va( "%02i-%02i-%02i", timeinfo->tm_hour, + timeinfo->tm_min, timeinfo->tm_sec ) ); + } + } + + // Open the temp output file + rws->recordfile = FS_SV_FOpenFileWrite( "records/current.rec" ); + if ( !rws->recordfile ) { + Record_Printf( RP_ALL, "Record_InitializeRecordWriter: failed to open output file\n" ); + Record_DeallocateRecordWriter(); + return; + } + + // Set up the stream + rws->stream.data = rws->streamBuffer; + rws->stream.size = sizeof( rws->streamBuffer ); + + // Set up the record state + rws->rs = Record_AllocateState( maxClients ); + rws->autoStarted = autoStarted; + rws->lastSnapflags = svs.snapFlagServerBit; +} + +/* +================== +Record_WriteClientEnterWorld +================== +*/ +static void Record_WriteClientEnterWorld( int clientNum ) { + if ( !rws ) { + return; + } + rws->activePlayers[clientNum] = 1; + Record_Stream_WriteValue( RC_EVENT_CLIENT_ENTER_WORLD, 1, &rws->stream ); + Record_Stream_WriteValue( clientNum, 1, &rws->stream ); +} + +/* +================== +Record_WriteClientDisconnect +================== +*/ +static void Record_WriteClientDisconnect( int clientNum ) { + if ( !rws || !rws->activePlayers[clientNum] ) { + return; + } + rws->activePlayers[clientNum] = 0; + Record_Stream_WriteValue( RC_EVENT_CLIENT_DISCONNECT, 1, &rws->stream ); + Record_Stream_WriteValue( clientNum, 1, &rws->stream ); +} + +/* +================== +Record_CheckConnections + +Handles connecting / disconnecting clients from record state +================== +*/ +static void Record_CheckConnections( void ) { + int i; + for ( i = 0; i < rws->rs->maxClients; ++i ) { + if ( sv.state == SS_GAME && i < sv_maxclients->integer && svs.clients[i].state == CS_ACTIVE && + ( svs.clients[i].netchan.remoteAddress.type != NA_BOT || sv_recordFullBotData->integer ) ) { + if ( !rws->activePlayers[i] ) { + Record_WriteClientEnterWorld( i ); + } + } else { + if ( rws->activePlayers[i] ) { + Record_WriteClientDisconnect( i ); + } + } + } +} + +/* +================== +Record_StartWriter +================== +*/ +static void Record_StartWriter( int maxClients, qboolean autoStarted ) { + int i; + if ( rws ) { + return; + } + + if ( maxClients < 1 || maxClients > RECORD_MAX_CLIENTS ) { + Record_Printf( RP_ALL, "Record_StartWriter: invalid maxClients" ); + maxClients = RECORD_MAX_CLIENTS; + } + + Record_InitializeRecordWriter( maxClients, autoStarted ); + if ( !rws ) { + return; + } + + // Write the protocol + Record_Stream_WriteValue( sizeof( RECORD_PROTOCOL ) - 1, 4, &rws->stream ); // version length + Record_Stream_Write( RECORD_PROTOCOL, sizeof( RECORD_PROTOCOL ) - 1, &rws->stream ); // version value + Record_Stream_WriteValue( 0, 4, &rws->stream ); // aux info length (zero) + + // Write max clients + Record_Stream_WriteValue( maxClients, 4, &rws->stream ); + + // Write the configstrings + for ( i = 0; i < MAX_CONFIGSTRINGS; ++i ) { + if ( !sv.configstrings[i] ) { + Record_Printf( RP_ALL, "Record_StartWriter: null configstring\n" ); + continue; + } + if ( !*sv.configstrings[i] ) { + continue; + } + Record_UpdateConfigstring( i, sv.configstrings[i] ); + } + + // Write the baselines + { + record_entityset_t baselines; + Record_GetCurrentBaselines( &baselines ); + Record_UpdateEntityset( &baselines ); + } + Record_Stream_WriteValue( RC_EVENT_BASELINES, 1, &rws->stream ); + + Record_Stream_DumpToFile( &rws->stream, rws->recordfile ); + + Record_Printf( RP_ALL, "Recording to %s/%s.rec\n", rws->targetDirectory, rws->targetFilename ); +} + +/* +================== +Record_StopWriter +================== +*/ +void Record_StopWriter( void ) { + if ( !rws ) { + return; + } + Record_CloseRecordWriter(); + Record_Printf( RP_ALL, "Recording stopped.\n" ); +} + +/* +================== +Record_HaveRecordablePlayers +================== +*/ +static qboolean Record_HaveRecordablePlayers( qboolean include_bots ) { + int i; + if ( sv.state != SS_GAME ) { + return qfalse; + } + for ( i = 0; i < sv_maxclients->integer; ++i ) { + if ( svs.clients[i].state == CS_ACTIVE && ( ( include_bots && sv_recordFullBotData->integer ) || + svs.clients[i].netchan.remoteAddress.type != NA_BOT ) ) { + return qtrue; + } + } + return qfalse; +} + +/* +================== +Record_StartCmd +================== +*/ +void Record_StartCmd( void ) { + if ( rws ) { + Record_Printf( RP_ALL, "Already recording.\n" ); + return; + } + if ( !Record_HaveRecordablePlayers( sv_recordFullBotData->integer ? qtrue : qfalse ) ) { + Record_Printf( RP_ALL, "No players to record.\n" ); + return; + } + Record_StartWriter( sv_maxclients->integer, qfalse ); +} + +/* +================== +Record_StopCmd +================== +*/ +void Record_StopCmd( void ) { + if ( !rws ) { + Record_Printf( RP_ALL, "Not currently recording.\n" ); + return; + } + if ( sv_recordAutoRecording->integer ) { + Record_Printf( RP_ALL, "NOTE: To permanently stop recording, set sv_recordAutoRecording to 0.\n" ); + } + Record_StopWriter(); +} + +/* ******************************************************************************** */ +// Event Handling Functions +/* ******************************************************************************** */ + +/* +================== +Record_Writer_ProcessUsercmd +================== +*/ +void Record_Writer_ProcessUsercmd( usercmd_t *usercmd, int clientNum ) { + if ( !rws || !rws->activePlayers[clientNum] ) { + return; + } + + if ( !sv_recordFullUsercmdData->integer ) { + // Don't write a new usercmd if most of the fields are the same + usercmd_t *oldUsercmd = &rws->rs->clients[clientNum].usercmd; + if ( usercmd->buttons == oldUsercmd->buttons && usercmd->weapon == oldUsercmd->weapon && + usercmd->forwardmove == oldUsercmd->forwardmove && + usercmd->rightmove == oldUsercmd->rightmove && usercmd->upmove == oldUsercmd->upmove ) { + return; + } + } + + Record_UpdateUsercmd( usercmd, clientNum ); +} + +/* +================== +Record_Writer_ProcessConfigstring +================== +*/ +void Record_Writer_ProcessConfigstring( int index, const char *value ) { + if ( !rws ) { + return; + } + Record_UpdateConfigstring( index, (char *)value ); +} + +/* +================== +Record_Writer_ProcessServercmd +================== +*/ +void Record_Writer_ProcessServercmd( int clientNum, const char *value ) { + if ( !rws || !rws->activePlayers[clientNum] ) { + return; + } + Record_UpdateCurrentServercmd( (char *)value ); + Record_Stream_WriteValue( RC_EVENT_SERVERCMD, 1, &rws->stream ); + Record_Stream_WriteValue( clientNum, 1, &rws->stream ); +} + +/* +================== +Record_Writer_ProcessSnapshot + +Check record connections; auto start and stop recording if needed +================== +*/ +void Record_Writer_ProcessSnapshot( void ) { + if ( !rws && sv_recordAutoRecording->integer && Record_HaveRecordablePlayers( qfalse ) ) { + Record_StartWriter( sv_maxclients->integer, qtrue ); + } + if ( rws ) { + Record_CheckConnections(); + } + if ( rws && !Record_HaveRecordablePlayers( sv_recordFullBotData->integer && !rws->autoStarted ? qtrue : qfalse ) ) { + Record_StopWriter(); + } + if ( !rws ) { + return; + } + + // Check for map restart + if ( ( rws->lastSnapflags & SNAPFLAG_SERVERCOUNT ) != ( svs.snapFlagServerBit & SNAPFLAG_SERVERCOUNT ) ) { + Record_Printf( RP_DEBUG, "Record_Writer_ProcessSnapshot: recording map restart\n" ); + Record_Stream_WriteValue( RC_EVENT_MAP_RESTART, 1, &rws->stream ); + } + rws->lastSnapflags = svs.snapFlagServerBit; + + { + record_entityset_t entities; + Record_GetCurrentEntities( &entities ); + Record_UpdateEntityset( &entities ); + } + + { + int i; + for ( i = 0; i < sv_maxclients->integer && i < rws->rs->maxClients; ++i ) { + if ( svs.clients[i].state < CS_ACTIVE ) { + continue; + } + if ( !rws->activePlayers[i] ) { + continue; + } + Record_UpdatePlayerstate( SV_GameClientNum( i ), i ); + Record_UpdateVisibilityStateClient( i ); + } + } + + Record_Stream_WriteValue( RC_EVENT_SNAPSHOT, 1, &rws->stream ); + Record_Stream_WriteValue( sv.time, 4, &rws->stream ); + + Record_Stream_DumpToFile( &rws->stream, rws->recordfile ); +} diff --git a/code/server/sv_snapshot.c b/code/server/sv_snapshot.c index 80a37098a..fb80af24c 100644 --- a/code/server/sv_snapshot.c +++ b/code/server/sv_snapshot.c @@ -790,4 +790,5 @@ void SV_SendClientMessages( void ) c->lastSnapshotTime = svs.time; c->rateDelayed = qfalse; } + Record_ProcessSnapshot(); } diff --git a/code/win32/msvc2005/quake3e-ded.vcproj b/code/win32/msvc2005/quake3e-ded.vcproj index 1486a44fb..e2357415f 100644 --- a/code/win32/msvc2005/quake3e-ded.vcproj +++ b/code/win32/msvc2005/quake3e-ded.vcproj @@ -507,6 +507,26 @@ RelativePath="..\..\server\sv_net_chan.c" > + + + + + + + + + + @@ -612,6 +632,10 @@ RelativePath="..\..\server\server.h" > + + diff --git a/code/win32/msvc2005/quake3e.vcproj b/code/win32/msvc2005/quake3e.vcproj index 86676a8f1..31181f3e6 100644 --- a/code/win32/msvc2005/quake3e.vcproj +++ b/code/win32/msvc2005/quake3e.vcproj @@ -627,6 +627,26 @@ RelativePath="..\..\server\sv_net_chan.c" > + + + + + + + + + + @@ -784,6 +804,10 @@ RelativePath="..\..\server\server.h" > + + diff --git a/code/win32/msvc2017/quake3e-ded.vcxproj b/code/win32/msvc2017/quake3e-ded.vcxproj index 35b9774de..c40a388df 100644 --- a/code/win32/msvc2017/quake3e-ded.vcxproj +++ b/code/win32/msvc2017/quake3e-ded.vcxproj @@ -299,6 +299,11 @@ + + + + + @@ -324,6 +329,7 @@ + diff --git a/code/win32/msvc2017/quake3e-ded.vcxproj.filters b/code/win32/msvc2017/quake3e-ded.vcxproj.filters index b94911ee9..322111b27 100644 --- a/code/win32/msvc2017/quake3e-ded.vcxproj.filters +++ b/code/win32/msvc2017/quake3e-ded.vcxproj.filters @@ -85,6 +85,21 @@ Source Files + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + Source Files @@ -165,6 +180,9 @@ Header Files + + Header Files + Header Files diff --git a/code/win32/msvc2017/quake3e.vcxproj b/code/win32/msvc2017/quake3e.vcxproj index 4e2043a23..94d44d387 100644 --- a/code/win32/msvc2017/quake3e.vcxproj +++ b/code/win32/msvc2017/quake3e.vcxproj @@ -331,6 +331,11 @@ + + + + + @@ -387,6 +392,7 @@ + diff --git a/code/win32/msvc2017/quake3e.vcxproj.filters b/code/win32/msvc2017/quake3e.vcxproj.filters index 4bf446867..17b5714fb 100644 --- a/code/win32/msvc2017/quake3e.vcxproj.filters +++ b/code/win32/msvc2017/quake3e.vcxproj.filters @@ -154,6 +154,21 @@ Source Files + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + Source Files @@ -267,6 +282,9 @@ Header Files + + Header Files + Header Files From d416d8e77081730bc200e705b7c3da744dbbc5d2 Mon Sep 17 00:00:00 2001 From: Noah Metzger Date: Sun, 5 May 2024 20:59:40 -0500 Subject: [PATCH 2/2] SSR: Fix message buffer sizes --- code/server/sv_record_common.c | 2 +- code/server/sv_record_convert.c | 8 ++++---- code/server/sv_record_spectator.c | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/code/server/sv_record_common.c b/code/server/sv_record_common.c index 5fd32c06d..f3c145a2f 100644 --- a/code/server/sv_record_common.c +++ b/code/server/sv_record_common.c @@ -865,7 +865,7 @@ Returns first baseline index to drop due to msg overflow */ static int Record_CalculateBaselineCutoff( record_entityset_t *baselines, msg_t msg ) { int i; - byte buffer[MAX_MSGLEN]; + byte buffer[MAX_MSGLEN_BUF]; entityState_t nullstate; msg.data = buffer; diff --git a/code/server/sv_record_convert.c b/code/server/sv_record_convert.c index 077bd3550..0e8ae9a91 100644 --- a/code/server/sv_record_convert.c +++ b/code/server/sv_record_convert.c @@ -101,14 +101,14 @@ Based on cl_main.c->CL_Record_f */ static void Record_WriteDemoGamestate( record_entityset_t *baselines, char **configstrings, int clientNum, record_demo_writer_t *rdw ) { - byte buffer[MAX_MSGLEN]; + byte buffer[MAX_MSGLEN_BUF]; msg_t msg; // Delta from baselines for next snapshot rdw->haveDelta = qfalse; rdw->baselines = *baselines; - MSG_Init( &msg, buffer, sizeof( buffer ) ); + MSG_Init( &msg, buffer, MAX_MSGLEN ); MSG_WriteLong( &msg, 0 ); @@ -143,10 +143,10 @@ Based on sv.snapshot.c->SV_SendClientSnapshot static void Record_WriteDemoSnapshot( record_entityset_t *entities, record_visibility_state_t *visibility, playerState_t *ps, int svTime, record_demo_writer_t *rdw ) { int i; - byte buffer[MAX_MSGLEN]; + byte buffer[MAX_MSGLEN_BUF]; msg_t msg; - MSG_Init( &msg, buffer, sizeof( buffer ) ); + MSG_Init( &msg, buffer, MAX_MSGLEN ); MSG_WriteLong( &msg, 0 ); diff --git a/code/server/sv_record_spectator.c b/code/server/sv_record_spectator.c index 32747d3e9..c7371e264 100644 --- a/code/server/sv_record_spectator.c +++ b/code/server/sv_record_spectator.c @@ -260,7 +260,7 @@ Based on sv_client.c->SV_SendClientGameState static void Record_SendSpectatorGamestate( spectator_t *spectator ) { client_t *cl = &spectator->cl; msg_t msg; - byte msgBuf[MAX_MSGLEN]; + byte msgBuf[MAX_MSGLEN_BUF]; if ( SVC_RateLimit( &cl->gamestate_rate, 4, 1000 ) ) { return; @@ -273,7 +273,7 @@ static void Record_SendSpectatorGamestate( spectator_t *spectator ) { cl->gamestateMessageNum = cl->netchan.outgoingSequence; // Initialize message - Record_InitSpectatorMessage( cl, &msg, msgBuf, sizeof( msgBuf ) ); + Record_InitSpectatorMessage( cl, &msg, msgBuf, MAX_MSGLEN ); // Write gamestate message Record_WriteGamestateMessage( &sps->currentBaselines, sv.configstrings, 0, cl->reliableSequence, &msg, @@ -293,7 +293,7 @@ Based on sv_snapshot.c->SV_SendClientSnapshot static void Record_SendSpectatorSnapshot( spectator_t *spectator ) { client_t *cl = &spectator->cl; msg_t msg; - byte msg_buf[MAX_MSGLEN]; + byte msg_buf[MAX_MSGLEN_BUF]; spectator_frame_t *current_frame = &spectator->frames[cl->netchan.outgoingSequence % PACKET_BACKUP]; spectator_frame_t *delta_frame = 0; int delta_frame_offset = 0; @@ -335,7 +335,7 @@ static void Record_SendSpectatorSnapshot( spectator_t *spectator ) { } // Initialize message - Record_InitSpectatorMessage( cl, &msg, msg_buf, sizeof( msg_buf ) ); + Record_InitSpectatorMessage( cl, &msg, msg_buf, MAX_MSGLEN ); // Write snapshot message Record_WriteSnapshotMessage( &sps->frameEntities[current_frame->frameEntitiesPosition % FRAME_ENTITY_COUNT],