# HG changeset patch # User Adam Kaminski # Date 1634405459 14400 # Sat Oct 16 13:30:59 2021 -0400 # Node ID 3fa62fab8f2064c3ac31235752d0898b8b68f487 # Parent e7df613d255779ad38d25495c65f4ce27ac267db Added packet loss mitigation, which the client can control using the CVar "cl_backupcommands". diff -r e7df613d2557 -r 3fa62fab8f20 src/cl_commands.cpp --- a/src/cl_commands.cpp Sat Oct 16 01:06:31 2021 -0400 +++ b/src/cl_commands.cpp Sat Oct 16 13:30:59 2021 -0400 @@ -76,6 +76,9 @@ static bool g_bIgnoreWeaponSelect = false; SDWORD g_sdwCheckCmd = 0; +// [AK] Backups of the last few movement commands we sent to the server. +static RingBuffer g_BackupCommands; + //***************************************************************************** // FUNCTIONS @@ -90,6 +93,27 @@ //***************************************************************************** // +void CLIENT_ResetBackupCommands( ULONG ulNumCommands ) +{ + CLIENT_MOVE_COMMAND_s emptyCMD; + memset( &emptyCMD, 0, sizeof( CLIENT_MOVE_COMMAND_s )); + + // [AK] Set the client gametic to a number that we can use to indicate + // if this command is supposed to be "empty". + emptyCMD.ulGametic = ULONG_MAX; + + ULONG ulOldPosition = g_BackupCommands.getPosition( ); + g_BackupCommands.setPosition( ulOldPosition + 1 ); + + // [AK] Replace any saved backup commands with the empty command. + for ( unsigned int i = 1; i <= ulNumCommands; i++ ) + g_BackupCommands.put( emptyCMD ); + + g_BackupCommands.setPosition( ulOldPosition ); +} + +//***************************************************************************** +// void CLIENT_IgnoreWeaponSelect( bool bIgnore ) { g_bIgnoreWeaponSelect = bIgnore; @@ -313,84 +337,126 @@ //***************************************************************************** // -void CLIENTCOMMANDS_ClientMove( void ) +static void clientcommand_CreateMoveCommand( void ) { - ticcmd_t *pCmd; - ULONG ulBits; + CLIENT_MOVE_COMMAND_s moveCMD; - CLIENT_GetLocalBuffer( )->ByteStream.WriteByte( CLC_CLIENTMOVE ); - CLIENT_GetLocalBuffer( )->ByteStream.WriteLong( gametic ); - // [CK] Send the server the latest known server-gametic - CLIENT_GetLocalBuffer( )->ByteStream.WriteLong( CLIENT_GetLatestServerGametic( ) + CLIENT_GetServerGameticOffset( ) ); - - // Decide what additional information needs to be sent. - ulBits = 0; - pCmd = &players[consoleplayer].cmd; + moveCMD.ulGametic = gametic; + moveCMD.ulServerGametic = CLIENT_GetLatestServerGametic( ) + CLIENT_GetServerGameticOffset( ); + moveCMD.cmd = players[consoleplayer].cmd; // [BB] If we think that we can't move, don't even try to tell the server that we // want to move. if ( players[consoleplayer].mo->reactiontime ) { - pCmd->ucmd.forwardmove = 0; - pCmd->ucmd.sidemove = 0; - pCmd->ucmd.buttons &= ~BT_JUMP; + moveCMD.cmd.ucmd.forwardmove = 0; + moveCMD.cmd.ucmd.sidemove = 0; + moveCMD.cmd.ucmd.buttons &= ~BT_JUMP; + } + + moveCMD.angle = players[consoleplayer].mo->angle; + moveCMD.pitch = players[consoleplayer].mo->pitch; + // [AK] Calculate the checksum of this ticcmd from this tic. + moveCMD.sdwChecksum = g_sdwCheckCmd; + + if ( players[consoleplayer].ReadyWeapon == NULL ) + moveCMD.usWeaponNetworkIndex = 0; + else + moveCMD.usWeaponNetworkIndex = players[consoleplayer].ReadyWeapon->GetClass( )->getActorNetworkIndex( ); + + // [AK] Create a backup copy of this command which we can resend later. + g_BackupCommands.put( moveCMD ); +} + +//***************************************************************************** +// +void CLIENTCOMMANDS_ClientMove( void ) +{ + ULONG ulNumCMDs = 0; + + // [AK] Create the movement command for the current tic. + clientcommand_CreateMoveCommand( ); + + // [AK] Determine how many movement commands we have saved. + for ( unsigned int i = 0; i < MAX_BACKUP_COMMANDS; i++ ) + { + if ( g_BackupCommands.getOldestEntry( i ).ulGametic != ULONG_MAX ) + ulNumCMDs++; } - if ( pCmd->ucmd.yaw ) - ulBits |= CLIENT_UPDATE_YAW; - if ( pCmd->ucmd.pitch ) - ulBits |= CLIENT_UPDATE_PITCH; - if ( pCmd->ucmd.roll ) - ulBits |= CLIENT_UPDATE_ROLL; - if ( pCmd->ucmd.buttons ) + ULONG ulNumCMDsExpected = cl_backupcommands + 1; + ULONG ulNumCMDsToSend = MIN( ulNumCMDs, static_cast( ulNumCMDsExpected )); + + CLIENT_GetLocalBuffer( )->ByteStream.WriteByte( CLC_CLIENTMOVE ); + // [AK] Include the number of commands we're sending and up to how many commands we want to send. + CLIENT_GetLocalBuffer( )->ByteStream.WriteShortByte( ulNumCMDsToSend, 4 ); + CLIENT_GetLocalBuffer( )->ByteStream.WriteShortByte( ulNumCMDsExpected, 4 ); + + // [AK] In order to mitigate packet loss, we can resend old movement commands to the server, + // so write older commands into the buffer before the newer ones. + for ( int i = ulNumCMDsToSend; i >= 1; i-- ) { - ulBits |= CLIENT_UPDATE_BUTTONS; - if ( zacompatflags & ZACOMPATF_CLIENTS_SEND_FULL_BUTTON_INFO ) - ulBits |= CLIENT_UPDATE_BUTTONS_LONG; - } - if ( pCmd->ucmd.forwardmove ) - ulBits |= CLIENT_UPDATE_FORWARDMOVE; - if ( pCmd->ucmd.sidemove ) - ulBits |= CLIENT_UPDATE_SIDEMOVE; - if ( pCmd->ucmd.upmove ) - ulBits |= CLIENT_UPDATE_UPMOVE; + CLIENT_MOVE_COMMAND_s moveCMD = g_BackupCommands.getOldestEntry( MAX_BACKUP_COMMANDS - i ); - // Tell the server what information we'll be sending. - CLIENT_GetLocalBuffer( )->ByteStream.WriteByte( ulBits ); + CLIENT_GetLocalBuffer( )->ByteStream.WriteLong( moveCMD.ulGametic ); + // [CK] Send the server the latest known server-gametic + CLIENT_GetLocalBuffer( )->ByteStream.WriteLong( moveCMD.ulServerGametic ); + + // Decide what additional information needs to be sent. + ULONG ulBits = 0; + ticcmd_t *pCmd = &moveCMD.cmd; - // Send the necessary movement/steering information. - if ( ulBits & CLIENT_UPDATE_YAW ) - CLIENT_GetLocalBuffer( )->ByteStream.WriteShort( pCmd->ucmd.yaw ); - if ( ulBits & CLIENT_UPDATE_PITCH ) - CLIENT_GetLocalBuffer( )->ByteStream.WriteShort( pCmd->ucmd.pitch ); - if ( ulBits & CLIENT_UPDATE_ROLL ) - CLIENT_GetLocalBuffer( )->ByteStream.WriteShort( pCmd->ucmd.roll ); - if ( ulBits & CLIENT_UPDATE_BUTTONS ) - { - if ( ulBits & CLIENT_UPDATE_BUTTONS_LONG ) - CLIENT_GetLocalBuffer( )->ByteStream.WriteLong( pCmd->ucmd.buttons ); - else - CLIENT_GetLocalBuffer( )->ByteStream.WriteByte( pCmd->ucmd.buttons ); - } - if ( ulBits & CLIENT_UPDATE_FORWARDMOVE ) - CLIENT_GetLocalBuffer( )->ByteStream.WriteShort( pCmd->ucmd.forwardmove ); - if ( ulBits & CLIENT_UPDATE_SIDEMOVE ) - CLIENT_GetLocalBuffer( )->ByteStream.WriteShort( pCmd->ucmd.sidemove ); - if ( ulBits & CLIENT_UPDATE_UPMOVE ) - CLIENT_GetLocalBuffer( )->ByteStream.WriteShort( pCmd->ucmd.upmove ); + if ( pCmd->ucmd.yaw ) + ulBits |= CLIENT_UPDATE_YAW; + if ( pCmd->ucmd.pitch ) + ulBits |= CLIENT_UPDATE_PITCH; + if ( pCmd->ucmd.roll ) + ulBits |= CLIENT_UPDATE_ROLL; + if ( pCmd->ucmd.buttons ) + { + ulBits |= CLIENT_UPDATE_BUTTONS; + if ( zacompatflags & ZACOMPATF_CLIENTS_SEND_FULL_BUTTON_INFO ) + ulBits |= CLIENT_UPDATE_BUTTONS_LONG; + } + if ( pCmd->ucmd.forwardmove ) + ulBits |= CLIENT_UPDATE_FORWARDMOVE; + if ( pCmd->ucmd.sidemove ) + ulBits |= CLIENT_UPDATE_SIDEMOVE; + if ( pCmd->ucmd.upmove ) + ulBits |= CLIENT_UPDATE_UPMOVE; + + // Tell the server what information we'll be sending. + CLIENT_GetLocalBuffer( )->ByteStream.WriteByte( ulBits ); - CLIENT_GetLocalBuffer( )->ByteStream.WriteLong( players[consoleplayer].mo->angle ); - CLIENT_GetLocalBuffer( )->ByteStream.WriteLong( players[consoleplayer].mo->pitch ); - // [BB] Send the checksum of our ticcmd we calculated when we originally generated the ticcmd from the user input. - CLIENT_GetLocalBuffer( )->ByteStream.WriteLong( g_sdwCheckCmd ); + // Send the necessary movement/steering information. + if ( ulBits & CLIENT_UPDATE_YAW ) + CLIENT_GetLocalBuffer( )->ByteStream.WriteShort( pCmd->ucmd.yaw ); + if ( ulBits & CLIENT_UPDATE_PITCH ) + CLIENT_GetLocalBuffer( )->ByteStream.WriteShort( pCmd->ucmd.pitch ); + if ( ulBits & CLIENT_UPDATE_ROLL ) + CLIENT_GetLocalBuffer( )->ByteStream.WriteShort( pCmd->ucmd.roll ); + if ( ulBits & CLIENT_UPDATE_BUTTONS ) + { + if ( ulBits & CLIENT_UPDATE_BUTTONS_LONG ) + CLIENT_GetLocalBuffer( )->ByteStream.WriteLong( pCmd->ucmd.buttons ); + else + CLIENT_GetLocalBuffer( )->ByteStream.WriteByte( pCmd->ucmd.buttons ); + } + if ( ulBits & CLIENT_UPDATE_FORWARDMOVE ) + CLIENT_GetLocalBuffer( )->ByteStream.WriteShort( pCmd->ucmd.forwardmove ); + if ( ulBits & CLIENT_UPDATE_SIDEMOVE ) + CLIENT_GetLocalBuffer( )->ByteStream.WriteShort( pCmd->ucmd.sidemove ); + if ( ulBits & CLIENT_UPDATE_UPMOVE ) + CLIENT_GetLocalBuffer( )->ByteStream.WriteShort( pCmd->ucmd.upmove ); - // Attack button. - if ( pCmd->ucmd.buttons & BT_ATTACK ) - { - if ( players[consoleplayer].ReadyWeapon == NULL ) - CLIENT_GetLocalBuffer( )->ByteStream.WriteShort( 0 ); - else - CLIENT_GetLocalBuffer( )->ByteStream.WriteShort( players[consoleplayer].ReadyWeapon->GetClass( )->getActorNetworkIndex() ); + CLIENT_GetLocalBuffer( )->ByteStream.WriteLong( players[consoleplayer].mo->angle ); + CLIENT_GetLocalBuffer( )->ByteStream.WriteLong( players[consoleplayer].mo->pitch ); + // [BB] Send the checksum of our ticcmd we calculated when we originally generated the ticcmd from the user input. + CLIENT_GetLocalBuffer( )->ByteStream.WriteLong( moveCMD.sdwChecksum ); + + // Attack button. + if ( pCmd->ucmd.buttons & BT_ATTACK ) + CLIENT_GetLocalBuffer( )->ByteStream.WriteShort( moveCMD.usWeaponNetworkIndex ); } } diff -r e7df613d2557 -r 3fa62fab8f20 src/cl_commands.h --- a/src/cl_commands.h Sat Oct 16 01:06:31 2021 -0400 +++ b/src/cl_commands.h Sat Oct 16 13:30:59 2021 -0400 @@ -70,6 +70,7 @@ // PROTOTYPES void CLIENT_ResetFloodTimers( void ); +void CLIENT_ResetBackupCommands( ULONG ulNumCommands ); void CLIENT_IgnoreWeaponSelect( bool bIgnore ); bool CLIENT_GetIgnoreWeaponSelect( void ); bool CLIENT_AllowSVCheatMessage( void ); diff -r e7df613d2557 -r 3fa62fab8f20 src/cl_main.cpp --- a/src/cl_main.cpp Sat Oct 16 01:06:31 2021 -0400 +++ b/src/cl_main.cpp Sat Oct 16 13:30:59 2021 -0400 @@ -169,6 +169,25 @@ // [JS] Always makes us ready when we are in intermission. CVAR( Bool, cl_autoready, false, CVAR_ARCHIVE ) +// [AK] Let the user send backup copies of old move commands, in case of packet loss. +CUSTOM_CVAR( Int, cl_backupcommands, 0, CVAR_ARCHIVE ) +{ + if ( self < 0 ) + self = 0; + + if ( self > MAX_BACKUP_COMMANDS - 1 ) + self = MAX_BACKUP_COMMANDS - 1; + + static ULONG ulOldValue = self; + + // [AK] If we're raising the number of backup commands, clear some, if not all + // of the backup commands we have already saved. + if ( ulOldValue < static_cast( self )) + CLIENT_ResetBackupCommands( self - ulOldValue ); + + ulOldValue = self; +} + //***************************************************************************** // PROTOTYPES @@ -3402,9 +3421,15 @@ if ( ulPlayer == static_cast( consoleplayer )) { - // [AK] Keep track of the tic when we first entered the game. - if (( priorState == PST_ENTER ) || ( priorState == PST_ENTERNOINVENTORY )) + // [AK] Keep track of the tic when we first (re)enter the game. + if (( priorState == PST_ENTER ) || ( priorState == PST_ENTERNOINVENTORY ) || + ( priorState == PST_REBORN ) || ( priorState == PST_REBORNNOINVENTORY )) + { g_ulFirstSpawnedTic = gametic; + + // [AK] Any backup commands we have saved are invalid, so remove them. + CLIENT_ResetBackupCommands( MAX_BACKUP_COMMANDS ); + } } else { diff -r e7df613d2557 -r 3fa62fab8f20 src/cl_main.h --- a/src/cl_main.h Sat Oct 16 01:06:31 2021 -0400 +++ b/src/cl_main.h Sat Oct 16 13:30:59 2021 -0400 @@ -61,6 +61,9 @@ #define CONNECTION_RESEND_TIME ( 3 * TICRATE ) #define GAMESTATE_RESEND_TIME ( 3 * TICRATE ) +// [AK] The maximum number of move commands we're allowed to send per tic. +#define MAX_BACKUP_COMMANDS 3 + //***************************************************************************** enum CONNECTIONSTATE_e { @@ -211,6 +214,7 @@ EXTERN_CVAR( String, cl_password ) EXTERN_CVAR( String, cl_joinpassword ) EXTERN_CVAR( Bool, cl_hitscandecalhack ) +EXTERN_CVAR( Int, cl_backupcommands ) // [AK] // Not in cl_main.cpp, but this seems like a good enough place for it. EXTERN_CVAR( Int, cl_skins ) diff -r e7df613d2557 -r 3fa62fab8f20 src/p_tick.cpp --- a/src/p_tick.cpp Sat Oct 16 01:06:31 2021 -0400 +++ b/src/p_tick.cpp Sat Oct 16 13:30:59 2021 -0400 @@ -333,6 +333,9 @@ ulNumMoveCMDs++; } + // [AK] Check if we should process this client's movement commands. + bool bProcessMoveCMDs = SERVER_HandleBackupCommands( ulIdx, ulNumMoveCMDs ); + // [AK] Handle the skip correction. If it explicity returns false, then we won't process two // movement commands during this tic for the client. if (( sv_smoothplayers ) && ( SERVER_HandleSkipCorrection( ulIdx, ulNumMoveCMDs ) == false )) @@ -342,9 +345,8 @@ { // Process only one movement command. const bool bMovement = client->MoveCMDs[0]->isMoveCmd( ); - client->MoveCMDs[0]->process( ulIdx ); - if ( bMovement ) + if (( bProcessMoveCMDs ) && ( bMovement )) { if ( client->LastMoveCMD != NULL ) delete client->LastMoveCMD; @@ -353,8 +355,12 @@ client->LastMoveCMD = new ClientMoveCommand( *static_cast( client->MoveCMDs[0] )); } - delete client->MoveCMDs[0]; - client->MoveCMDs.Delete(0); + if (( bProcessMoveCMDs ) || ( bMovement == false )) + { + client->MoveCMDs[0]->process( ulIdx ); + delete client->MoveCMDs[0]; + client->MoveCMDs.Delete( 0 ); + } if ( bMovement == true ) break; diff -r e7df613d2557 -r 3fa62fab8f20 src/sv_main.cpp --- a/src/sv_main.cpp Sat Oct 16 01:06:31 2021 -0400 +++ b/src/sv_main.cpp Sat Oct 16 13:30:59 2021 -0400 @@ -5151,17 +5151,19 @@ //***************************************************************************** // template -static bool server_ParseBufferedCommand ( BYTESTREAM_s *pByteStream ) +static bool server_ParseBufferedCommand ( BYTESTREAM_s *pByteStream, bool bProcess = true ) { CommandType *cmd = new CommandType ( pByteStream ); TArray *buffer = &g_aClients[g_lCurrentClient].MoveCMDs; if ( sv_useticbuffer ) { + ULONG ulCMDClientTic = cmd->getClientTic( ); + if (( sv_smoothplayers ) && ( cmd->isMoveCmd( ))) { // [AK] Ignore commands that arrived too late, which we won't account for anymore. - if ( cmd->getClientTic( ) <= g_aClients[g_lCurrentClient].ulClientGameTic ) + if ( ulCMDClientTic <= g_aClients[g_lCurrentClient].ulClientGameTic ) { delete cmd; return false; @@ -5174,7 +5176,7 @@ { // [AK] We want to try filling this buffer only when the client is suffering from a ping // spike, not when they're experiencing packet loss. - if ( cmd->getClientTic( ) <= g_aClients[g_lCurrentClient].ulClientGameTic + g_aClients[g_lCurrentClient].ulExtrapolatedTics ) + if ( ulCMDClientTic <= g_aClients[g_lCurrentClient].ulClientGameTic + g_aClients[g_lCurrentClient].ulExtrapolatedTics ) buffer = &g_aClients[g_lCurrentClient].LateMoveCMDs; } } @@ -5182,7 +5184,22 @@ // [AK] Organize the movement commands in case they arrived in the wrong order. for ( unsigned int i = 0; i < buffer->Size( ); i++ ) { - if ( cmd->getClientTic( ) < ( *buffer )[i]->getClientTic( )) + ULONG ulBufferClientTic = ( *buffer )[i]->getClientTic( ); + + if (( cmd->isMoveCmd( )) && (( *buffer )[i]->isMoveCmd( ))) + { + bool bNotLateCMD = ( buffer == &g_aClients[g_lCurrentClient].MoveCMDs ); + + // [AK] Double-check to make sure we didn't already receive this move command from the client. + // This is especially important if they're sending us backup move commands. + if (( ulBufferClientTic == ulCMDClientTic ) || (( bNotLateCMD ) && ( ulBufferClientTic > ulCMDClientTic ))) + { + delete cmd; + return false; + } + } + + if ( ulCMDClientTic < ulBufferClientTic ) { buffer->Insert( i, cmd ); return false; @@ -5193,7 +5210,9 @@ return false; } - const bool retValue = cmd->process ( g_lCurrentClient ); + // [AK] Never process any backup commands the client sent us if the tic buffer is disabled. + const bool retValue = bProcess ? cmd->process( g_lCurrentClient ) : false; + delete cmd; return retValue; } @@ -5259,6 +5278,40 @@ //***************************************************************************** // +bool SERVER_HandleBackupCommands( ULONG ulClient, ULONG ulNumMoveCMDs ) +{ + const bool bAlreadyProcessed = ( g_aClients[ulClient].lLastMoveTickProcess == gametic ); + bool result = false; + + // [AK] If this client's spectating, don't do anything here. + if ( players[ulClient].bSpectating ) + return true; + + // [AK] If the player wants to send us backups of movement commands, then we should wait until + // we receive enough from them. However, if the tic buffer is too full, process them anyways. + const bool bMustProcess = ( ulNumMoveCMDs >= g_aClients[ulClient].ulNumExpectedCMDs ); + + // [AK] We don't want to process two movement commands in a single tic if we only have as many + // backup commands as we're expecting, unless the tic buffer is too full. + if (( bAlreadyProcessed == false ) || ( bMustProcess )) + { + // [AK] Should we process the player's movement commands during this tic? We don't want to process + // them right away until we receive enough backup copies of the commands. + if (( g_aClients[ulClient].ulNumSentCMDs == g_aClients[ulClient].ulNumExpectedCMDs ) || ( bMustProcess )) + result = true; + } + + // [AK] Is the player suffering from packet loss while we wait for them to send backup copies of the + // movement commands? If so, then we don't want to wait forever to receive them, so we'll eventually + // process whatever we have already. Increment the counter only once per tic. + if (( result == false ) && ( bAlreadyProcessed == false ) && ( g_aClients[ulClient].lLastMoveTick != gametic )) + g_aClients[ulClient].ulNumSentCMDs++; + + return result; +} + +//***************************************************************************** +// bool SERVER_HandleSkipCorrection( ULONG ulClient, ULONG ulNumMoveCMDs ) { CLIENT_s *pClient = &g_aClients[ulClient]; @@ -5363,6 +5416,8 @@ // [AK] We want to reset this client's last backtrace tic only when we reset their tic buffer. g_aClients[ulClient].lLastBacktraceTic = 0; + // [AK] Also reset the backup command counters. + g_aClients[ulClient].ulNumSentCMDs = g_aClients[ulClient].ulNumExpectedCMDs = 0; SERVER_ResetClientExtrapolation( ulClient ); } @@ -5623,14 +5678,40 @@ // static bool server_ClientMove( BYTESTREAM_s *pByteStream ) { + bool result = false; + // Don't timeout. g_aClients[g_lCurrentClient].ulLastCommandTic = gametic; + // [AK] How many movement commands were sent in this packet? How many + // are we expecting from the client? This is so we know when to process + // any backup commands from them or not. + ULONG ulNumSentCMDs = pByteStream->ReadShortByte( 4 ); + ULONG ulNumExpectedCMDs = pByteStream->ReadShortByte( 4 ); + + ULONG ulOldBufferSize = g_aClients[g_lCurrentClient].MoveCMDs.Size( ); + // [BB] We don't process the movement command immediately, but store it // in a buffer. This way we can limit the amount of movement commands // we process for a player in a given tic to prevent the player from // seemingly teleporting in case too many movement commands arrive at once. - return server_ParseBufferedCommand ( pByteStream ); + for ( unsigned int i = 1; i <= ulNumSentCMDs; i++ ) + { + if ( server_ParseBufferedCommand( pByteStream, i == ulNumSentCMDs )) + result = true; + } + + // [AK] Only update the number of sent/expected movement commands for the + // client if it wasn't treated as a late command. In case the packets are + // received in the wrong order, we should only care about what the client + // sent us "now", than what they sent in the "past". + if ( ulOldBufferSize != g_aClients[g_lCurrentClient].MoveCMDs.Size( )) + { + g_aClients[g_lCurrentClient].ulNumSentCMDs = ulNumSentCMDs; + g_aClients[g_lCurrentClient].ulNumExpectedCMDs = ulNumExpectedCMDs; + } + + return result; } ClientMoveCommand::ClientMoveCommand ( BYTESTREAM_s *pByteStream ) diff -r e7df613d2557 -r 3fa62fab8f20 src/sv_main.h --- a/src/sv_main.h Sat Oct 16 01:06:31 2021 -0400 +++ b/src/sv_main.h Sat Oct 16 13:30:59 2021 -0400 @@ -220,6 +220,7 @@ USHORT usWeaponNetworkIndex; ULONG ulGametic; ULONG ulServerGametic; + SWORD sdwChecksum; // [BB] We want to process the command from the lowest gametic first. // This puts the lowest gametic on top of the queue. @@ -457,6 +458,14 @@ // we need to know how much thrust we need to add back in case the backtrace succeeds. fixed_t backtraceThrust[3]; + // [AK] How many movement commands did this player send us in the last packet? If this is greater than + // one, that means they also sent us backups of older commands. + ULONG ulNumSentCMDs; + + // [AK] How many movement commands are we expecting from this player? If this is greater than one, that + // means we're expecting them to also send us backups of older commands. + ULONG ulNumExpectedCMDs; + // [BB] Variables for the account system FString username; unsigned int clientSessionID; @@ -617,6 +626,7 @@ void SERVER_KillCheat( const char* what ); void STACK_ARGS SERVER_PrintWarning( const char* format, ... ) GCCPRINTF( 1, 2 ); void SERVER_FlagsetChanged( FIntCVar& flagset, int maxflags = 2 ); +bool SERVER_HandleBackupCommands( ULONG ulClient, ULONG ulNumMoveCMDs ); bool SERVER_HandleSkipCorrection( ULONG ulClient, ULONG ulNumMoveCMDs ); bool SERVER_IsBacktracingPlayer( ULONG ulClient ); void SERVER_ResetClientTicBuffer( ULONG ulClient, bool bClearMoveCMDs = true ); diff -r e7df613d2557 -r 3fa62fab8f20 wadsrc/static/menudef.za --- a/wadsrc/static/menudef.za Sat Oct 16 01:06:31 2021 -0400 +++ b/wadsrc/static/menudef.za Sat Oct 16 13:30:59 2021 -0400 @@ -93,6 +93,13 @@ 1.0, "DSL" } +OptionValue ZA_BackupRate +{ + 0.0, "None" + 1.0, "One per tick" + 2.0, "Two per tick" +} + OptionMenu ZA_NetworkOptions { Title "NETWORK OPTIONS" @@ -101,6 +108,7 @@ Option "Unlagged", "cl_unlagged", "OnOff" Option "Unlag Type", "cl_ping_unlagged", "ZA_UnlagType" Option "Update Rate", "cl_ticsperupdate", "ZA_UpdateRate" + Option "Send backup commands", "cl_backupcommands", "ZA_BackupRate" Option "Hitscan decals", "cl_hitscandecalhack", "OnOff" Option "Clientside puffs", "cl_clientsidepuffs", "OnOff" Option "Display packet loss", "cl_showpacketloss", "YesNo"