/* =========================================================================== Doom 3 GPL Source Code Copyright (C) 1999-2011 id Software LLC, a ZeniMax Media company. This file is part of the Doom 3 GPL Source Code ("Doom 3 Source Code"). Doom 3 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 3 of the License, or (at your option) any later version. Doom 3 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 Doom 3 Source Code. If not, see . In addition, the Doom 3 Source Code is also subject to certain additional terms. You should have received a copy of these additional terms immediately following the terms and conditions of the GNU General Public License which accompanied the Doom 3 Source Code. If not, please request a copy in writing from id Software at the address below. If you have questions concerning this license or the applicable additional terms, you may contact in writing id Software LLC, c/o ZeniMax Media Inc., Suite 120, Rockville, Maryland 20850 USA. =========================================================================== */ #include "sys/platform.h" #include "idlib/LangDict.h" #include "framework/Session_local.h" #include "framework/Game.h" #include "framework/async/AsyncNetwork.h" const int MIN_RECONNECT_TIME = 2000; const int EMPTY_RESEND_TIME = 500; const int PING_RESEND_TIME = 500; const int NOINPUT_IDLE_TIME = 30000; const int HEARTBEAT_MSEC = 5*60*1000; // must be kept in sync with authReplyMsg_t const char* authReplyMsg[] = { // "Waiting for authorization", "#str_07204", // "Client unknown to auth", "#str_07205", // "Access denied - CD Key in use", "#str_07206", // "Auth custom message", // placeholder - we propagate a message from the master "#str_07207", // "Authorize Server - Waiting for client" "#str_07208" }; const char* authReplyStr[] = { "AUTH_NONE", "AUTH_OK", "AUTH_WAIT", "AUTH_DENY" }; /* ================== idAsyncServer::idAsyncServer ================== */ idAsyncServer::idAsyncServer( void ) { int i; active = false; realTime = 0; serverTime = 0; serverId = 0; serverDataChecksum = 0; localClientNum = -1; gameInitId = 0; gameFrame = 0; gameTime = 0; gameTimeResidual = 0; memset( challenges, 0, sizeof( challenges ) ); memset( userCmds, 0, sizeof( userCmds ) ); for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) { ClearClient( i ); } serverReloadingEngine = false; nextHeartbeatTime = 0; nextAsyncStatsTime = 0; noRconOutput = true; lastAuthTime = 0; memset( stats_outrate, 0, sizeof( stats_outrate ) ); stats_current = 0; stats_average_sum = 0; stats_max = 0; stats_max_index = 0; } /* ================== idAsyncServer::InitPort ================== */ bool idAsyncServer::InitPort( void ) { int lastPort; // if this is the first time we have spawned a server, open the UDP port if ( !serverPort.GetPort() ) { if ( cvarSystem->GetCVarInteger( "net_port" ) != 0 ) { if ( !serverPort.InitForPort( cvarSystem->GetCVarInteger( "net_port" ) ) ) { common->Printf( "Unable to open server on port %d (net_port)\n", cvarSystem->GetCVarInteger( "net_port" ) ); return false; } } else { // scan for multiple ports, in case other servers are running on this IP already for ( lastPort = 0; lastPort < NUM_SERVER_PORTS; lastPort++ ) { if ( serverPort.InitForPort( PORT_SERVER + lastPort ) ) { break; } } if ( lastPort >= NUM_SERVER_PORTS ) { common->Printf( "Unable to open server network port.\n" ); return false; } } } return true; } /* ================== idAsyncServer::ClosePort ================== */ void idAsyncServer::ClosePort( void ) { int i; serverPort.Close(); for ( i = 0; i < MAX_CHALLENGES; i++ ) { challenges[ i ].authReplyPrint.Clear(); } } /* ================== idAsyncServer::Spawn ================== */ void idAsyncServer::Spawn( void ) { int i, size; byte msgBuf[MAX_MESSAGE_SIZE]; netadr_t from; // shutdown any current game session->Stop(); if ( active ) { return; } if ( !InitPort() ) { return; } // trash any currently pending packets while( serverPort.GetPacket( from, msgBuf, size, sizeof( msgBuf ) ) ) { } // reset cheats cvars if ( !idAsyncNetwork::allowCheats.GetBool() ) { cvarSystem->ResetFlaggedVariables( CVAR_CHEAT ); } memset( challenges, 0, sizeof( challenges ) ); memset( userCmds, 0, sizeof( userCmds ) ); for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) { ClearClient( i ); } common->Printf( "Server spawned on port %i.\n", serverPort.GetPort() ); // calculate a checksum on some of the essential data used serverDataChecksum = declManager->GetChecksum(); // get a pseudo random server id, but don't use the id which is reserved for connectionless packets serverId = Sys_Milliseconds() & CONNECTIONLESS_MESSAGE_ID_MASK; active = true; nextHeartbeatTime = 0; nextAsyncStatsTime = 0; ExecuteMapChange(); } /* ================== idAsyncServer::Kill ================== */ void idAsyncServer::Kill( void ) { int i, j; if ( !active ) { return; } // drop all clients for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) { DropClient( i, "#str_07135" ); } // send some empty messages to the zombie clients to make sure they disconnect for ( j = 0; j < 4; j++ ) { for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) { if ( clients[i].clientState == SCS_ZOMBIE ) { if ( clients[i].channel.UnsentFragmentsLeft() ) { clients[i].channel.SendNextFragment( serverPort, serverTime ); } else { SendEmptyToClient( i, true ); } } } Sys_Sleep( 10 ); } // reset any pureness fileSystem->ClearPureChecksums(); active = false; // shutdown any current game session->Stop(); } /* ================== idAsyncServer::ExecuteMapChange ================== */ void idAsyncServer::ExecuteMapChange( void ) { int i; idBitMsg msg; byte msgBuf[MAX_MESSAGE_SIZE]; idStr mapName; findFile_t ff; bool addonReload = false; char bestGameType[ MAX_STRING_CHARS ]; assert( active ); // reset any pureness fileSystem->ClearPureChecksums(); // make sure the map/gametype combo is good game->GetBestGameType( cvarSystem->GetCVarString("si_map"), cvarSystem->GetCVarString("si_gametype"), bestGameType ); cvarSystem->SetCVarString("si_gametype", bestGameType ); // initialize map settings cmdSystem->BufferCommandText( CMD_EXEC_NOW, "rescanSI" ); sprintf( mapName, "maps/%s", sessLocal.mapSpawnData.serverInfo.GetString( "si_map" ) ); mapName.SetFileExtension( ".map" ); ff = fileSystem->FindFile( mapName, !serverReloadingEngine ); switch( ff ) { case FIND_NO: common->Printf( "Can't find map %s\n", mapName.c_str() ); cmdSystem->BufferCommandText( CMD_EXEC_APPEND, "disconnect\n" ); return; case FIND_ADDON: // NOTE: we have no problem with addon dependencies here because if the map is in // an addon pack that's already on search list, then all it's deps are assumed to be on search as well common->Printf( "map %s is in an addon pak - reloading\n", mapName.c_str() ); addonReload = true; break; default: break; } // if we are asked to do a full reload, the strategy is completely different if ( !serverReloadingEngine && ( addonReload || idAsyncNetwork::serverReloadEngine.GetInteger() != 0 ) ) { if ( idAsyncNetwork::serverReloadEngine.GetInteger() != 0 ) { common->Printf( "net_serverReloadEngine enabled - doing a full reload\n" ); } // tell the clients to reconnect // FIXME: shouldn't they wait for the new pure list, then reload? // in a lot of cases this is going to trigger two reloadEngines for the clients // one to restart, the other one to set paks right ( with addon for instance ) // can fix by reconnecting without reloading and waiting for the server to tell.. for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) { if ( clients[ i ].clientState >= SCS_PUREWAIT && i != localClientNum ) { msg.Init( msgBuf, sizeof( msgBuf ) ); msg.WriteByte( SERVER_RELIABLE_MESSAGE_RELOAD ); SendReliableMessage( i, msg ); clients[ i ].clientState = SCS_ZOMBIE; // so we don't bother sending a disconnect } } cmdSystem->BufferCommandText( CMD_EXEC_NOW, "reloadEngine" ); serverReloadingEngine = true; // don't get caught in endless loop cmdSystem->BufferCommandText( CMD_EXEC_APPEND, "spawnServer\n" ); // decrease feature if ( idAsyncNetwork::serverReloadEngine.GetInteger() > 0 ) { idAsyncNetwork::serverReloadEngine.SetInteger( idAsyncNetwork::serverReloadEngine.GetInteger() - 1 ); } return; } serverReloadingEngine = false; serverTime = 0; // initialize game id and time gameInitId ^= Sys_Milliseconds(); // NOTE: make sure the gameInitId is always a positive number because negative numbers have special meaning gameFrame = 0; gameTime = 0; gameTimeResidual = 0; memset( userCmds, 0, sizeof( userCmds ) ); if ( idAsyncNetwork::serverDedicated.GetInteger() == 0 ) { InitLocalClient( 0 ); } else { localClientNum = -1; } // re-initialize all connected clients for the new map for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) { if ( clients[i].clientState >= SCS_PUREWAIT && i != localClientNum ) { InitClient( i, clients[i].clientId, clients[i].clientRate ); SendGameInitToClient( i ); if ( sessLocal.mapSpawnData.serverInfo.GetBool( "si_pure" ) ) { clients[ i ].clientState = SCS_PUREWAIT; } } } // load map sessLocal.ExecuteMapChange(); if ( localClientNum >= 0 ) { BeginLocalClient(); } else { game->SetLocalClient( -1 ); } if ( sessLocal.mapSpawnData.serverInfo.GetInt( "si_pure" ) ) { // lock down the pak list fileSystem->UpdatePureServerChecksums( ); // tell the clients so they can work out their pure lists for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) { if ( clients[ i ].clientState == SCS_PUREWAIT ) { if ( !SendReliablePureToClient( i ) ) { clients[ i ].clientState = SCS_CONNECTED; } } } } // serverTime gets reset, force a heartbeat so timings restart MasterHeartbeat( true ); } /* ================== idAsyncServer::GetPort ================== */ int idAsyncServer::GetPort( void ) const { return serverPort.GetPort(); } /* =============== idAsyncServer::GetBoundAdr =============== */ netadr_t idAsyncServer::GetBoundAdr( void ) const { return serverPort.GetAdr(); } /* ================== idAsyncServer::GetOutgoingRate ================== */ int idAsyncServer::GetOutgoingRate( void ) const { int i, rate; rate = 0; for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) { const serverClient_t &client = clients[i]; if ( client.clientState >= SCS_CONNECTED ) { rate += client.channel.GetOutgoingRate(); } } return rate; } /* ================== idAsyncServer::GetIncomingRate ================== */ int idAsyncServer::GetIncomingRate( void ) const { int i, rate; rate = 0; for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) { const serverClient_t &client = clients[i]; if ( client.clientState >= SCS_CONNECTED ) { rate += client.channel.GetIncomingRate(); } } return rate; } /* ================== idAsyncServer::IsClientInGame ================== */ bool idAsyncServer::IsClientInGame( int clientNum ) const { return ( clients[clientNum].clientState >= SCS_INGAME ); } /* ================== idAsyncServer::GetClientPing ================== */ int idAsyncServer::GetClientPing( int clientNum ) const { const serverClient_t &client = clients[clientNum]; if ( client.clientState < SCS_CONNECTED ) { return 99999; } else { return client.clientPing; } } /* ================== idAsyncServer::GetClientPrediction ================== */ int idAsyncServer::GetClientPrediction( int clientNum ) const { const serverClient_t &client = clients[clientNum]; if ( client.clientState < SCS_CONNECTED ) { return 99999; } else { return client.clientPrediction; } } /* ================== idAsyncServer::GetClientTimeSinceLastPacket ================== */ int idAsyncServer::GetClientTimeSinceLastPacket( int clientNum ) const { const serverClient_t &client = clients[clientNum]; if ( client.clientState < SCS_CONNECTED ) { return 99999; } else { return serverTime - client.lastPacketTime; } } /* ================== idAsyncServer::GetClientTimeSinceLastInput ================== */ int idAsyncServer::GetClientTimeSinceLastInput( int clientNum ) const { const serverClient_t &client = clients[clientNum]; if ( client.clientState < SCS_CONNECTED ) { return 99999; } else { return serverTime - client.lastInputTime; } } /* ================== idAsyncServer::GetClientOutgoingRate ================== */ int idAsyncServer::GetClientOutgoingRate( int clientNum ) const { const serverClient_t &client = clients[clientNum]; if ( client.clientState < SCS_CONNECTED ) { return -1; } else { return client.channel.GetOutgoingRate(); } } /* ================== idAsyncServer::GetClientIncomingRate ================== */ int idAsyncServer::GetClientIncomingRate( int clientNum ) const { const serverClient_t &client = clients[clientNum]; if ( client.clientState < SCS_CONNECTED ) { return -1; } else { return client.channel.GetIncomingRate(); } } /* ================== idAsyncServer::GetClientOutgoingCompression ================== */ float idAsyncServer::GetClientOutgoingCompression( int clientNum ) const { const serverClient_t &client = clients[clientNum]; if ( client.clientState < SCS_CONNECTED ) { return 0.0f; } else { return client.channel.GetOutgoingCompression(); } } /* ================== idAsyncServer::GetClientIncomingCompression ================== */ float idAsyncServer::GetClientIncomingCompression( int clientNum ) const { const serverClient_t &client = clients[clientNum]; if ( client.clientState < SCS_CONNECTED ) { return 0.0f; } else { return client.channel.GetIncomingCompression(); } } /* ================== idAsyncServer::GetClientIncomingPacketLoss ================== */ float idAsyncServer::GetClientIncomingPacketLoss( int clientNum ) const { const serverClient_t &client = clients[clientNum]; if ( client.clientState < SCS_CONNECTED ) { return 0.0f; } else { return client.channel.GetIncomingPacketLoss(); } } /* ================== idAsyncServer::GetNumClients ================== */ int idAsyncServer::GetNumClients( void ) const { int ret = 0; for ( int i = 0; i < MAX_ASYNC_CLIENTS; i++ ) { if ( clients[ i ].clientState >= SCS_CONNECTED ) { ret++; } } return ret; } /* ================== idAsyncServer::GetNumIdleClients ================== */ int idAsyncServer::GetNumIdleClients( void ) const { int ret = 0; for ( int i = 0; i < MAX_ASYNC_CLIENTS; i++ ) { if ( clients[ i ].clientState >= SCS_CONNECTED ) { if ( serverTime - clients[ i ].lastInputTime > NOINPUT_IDLE_TIME ) { ret++; } } } return ret; } /* ================== idAsyncServer::DuplicateUsercmds ================== */ void idAsyncServer::DuplicateUsercmds( int frame, int time ) { int i, previousIndex, currentIndex; previousIndex = ( frame - 1 ) & ( MAX_USERCMD_BACKUP - 1 ); currentIndex = frame & ( MAX_USERCMD_BACKUP - 1 ); // duplicate previous user commands if no new commands are available for a client for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) { if ( clients[i].clientState == SCS_FREE ) { continue; } if ( idAsyncNetwork::DuplicateUsercmd( userCmds[previousIndex][i], userCmds[currentIndex][i], frame, time ) ) { clients[i].numDuplicatedUsercmds++; } } } /* ================== idAsyncServer::ClearClient ================== */ void idAsyncServer::ClearClient( int clientNum ) { serverClient_t &client = clients[clientNum]; client.clientId = 0; client.clientState = SCS_FREE; client.clientPrediction = 0; client.clientAheadTime = 0; client.clientRate = 0; client.clientPing = 0; client.gameInitSequence = 0; client.gameFrame = 0; client.gameTime = 0; client.channel.Shutdown(); client.lastConnectTime = 0; client.lastEmptyTime = 0; client.lastPingTime = 0; client.lastSnapshotTime = 0; client.lastPacketTime = 0; client.lastInputTime = 0; client.snapshotSequence = 0; client.acknowledgeSnapshotSequence = 0; client.numDuplicatedUsercmds = 0; } /* ================== idAsyncServer::InitClient ================== */ void idAsyncServer::InitClient( int clientNum, int clientId, int clientRate ) { int i; // clear the user info sessLocal.mapSpawnData.userInfo[ clientNum ].Clear(); // always start with a clean base // clear the server client serverClient_t &client = clients[clientNum]; client.clientId = clientId; client.clientState = SCS_CONNECTED; client.clientPrediction = 0; client.clientAheadTime = 0; client.gameInitSequence = -1; client.gameFrame = 0; client.gameTime = 0; client.channel.ResetRate(); client.clientRate = clientRate ? clientRate : idAsyncNetwork::serverMaxClientRate.GetInteger(); client.channel.SetMaxOutgoingRate( Min( idAsyncNetwork::serverMaxClientRate.GetInteger(), client.clientRate ) ); client.clientPing = 0; client.lastConnectTime = serverTime; client.lastEmptyTime = serverTime; client.lastPingTime = serverTime; client.lastSnapshotTime = serverTime; client.lastPacketTime = serverTime; client.lastInputTime = serverTime; client.acknowledgeSnapshotSequence = 0; client.numDuplicatedUsercmds = 0; // clear the user commands for ( i = 0; i < MAX_USERCMD_BACKUP; i++ ) { memset( &userCmds[i][clientNum], 0, sizeof( userCmds[i][clientNum] ) ); } // let the game know a player connected game->ServerClientConnect( clientNum, client.guid ); } /* ================== idAsyncServer::InitLocalClient ================== */ void idAsyncServer::InitLocalClient( int clientNum ) { netadr_t badAddress; localClientNum = clientNum; InitClient( clientNum, 0, 0 ); memset( &badAddress, 0, sizeof( badAddress ) ); badAddress.type = NA_BAD; clients[clientNum].channel.Init( badAddress, serverId ); clients[clientNum].clientState = SCS_INGAME; sessLocal.mapSpawnData.userInfo[clientNum] = *cvarSystem->MoveCVarsToDict( CVAR_USERINFO ); } /* ================== idAsyncServer::BeginLocalClient ================== */ void idAsyncServer::BeginLocalClient( void ) { game->SetLocalClient( localClientNum ); game->SetUserInfo( localClientNum, sessLocal.mapSpawnData.userInfo[localClientNum], false, false ); game->ServerClientBegin( localClientNum ); } /* ================== idAsyncServer::LocalClientInput ================== */ void idAsyncServer::LocalClientInput( void ) { int index; if ( localClientNum < 0 ) { return; } index = gameFrame & ( MAX_USERCMD_BACKUP - 1 ); userCmds[index][localClientNum] = usercmdGen->GetDirectUsercmd(); userCmds[index][localClientNum].gameFrame = gameFrame; userCmds[index][localClientNum].gameTime = gameTime; if ( idAsyncNetwork::UsercmdInputChanged( userCmds[( gameFrame - 1 ) & ( MAX_USERCMD_BACKUP - 1 )][localClientNum], userCmds[index][localClientNum] ) ) { clients[localClientNum].lastInputTime = serverTime; } clients[localClientNum].gameFrame = gameFrame; clients[localClientNum].gameTime = gameTime; clients[localClientNum].lastPacketTime = serverTime; } /* ================== idAsyncServer::DropClient ================== */ void idAsyncServer::DropClient( int clientNum, const char *reason ) { int i; idBitMsg msg; byte msgBuf[MAX_MESSAGE_SIZE]; serverClient_t &client = clients[clientNum]; if ( client.clientState <= SCS_ZOMBIE ) { return; } if ( client.clientState >= SCS_PUREWAIT && clientNum != localClientNum ) { msg.Init( msgBuf, sizeof( msgBuf ) ); msg.WriteByte( SERVER_RELIABLE_MESSAGE_DISCONNECT ); msg.WriteInt( clientNum ); msg.WriteString( reason ); for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) { // clientNum so SCS_PUREWAIT client gets it's own disconnect msg if ( i == clientNum || clients[i].clientState >= SCS_CONNECTED ) { SendReliableMessage( i, msg ); } } } reason = common->GetLanguageDict()->GetString( reason ); common->Printf( "client %d %s\n", clientNum, reason ); cmdSystem->BufferCommandText( CMD_EXEC_NOW, va( "addChatLine \"%s^0 %s\"", sessLocal.mapSpawnData.userInfo[ clientNum ].GetString( "ui_name" ), reason ) ); // remove the player from the game game->ServerClientDisconnect( clientNum ); client.clientState = SCS_ZOMBIE; } /* ================== idAsyncServer::SendReliableMessage ================== */ void idAsyncServer::SendReliableMessage( int clientNum, const idBitMsg &msg ) { if ( clientNum == localClientNum ) { return; } if ( !clients[ clientNum ].channel.SendReliableMessage( msg ) ) { clients[ clientNum ].channel.ClearReliableMessages(); DropClient( clientNum, "#str_07136" ); } } /* ================== idAsyncServer::CheckClientTimeouts ================== */ void idAsyncServer::CheckClientTimeouts( void ) { int i, zombieTimeout, clientTimeout; zombieTimeout = serverTime - idAsyncNetwork::serverZombieTimeout.GetInteger() * 1000; clientTimeout = serverTime - idAsyncNetwork::serverClientTimeout.GetInteger() * 1000; for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) { serverClient_t &client = clients[i]; if ( i == localClientNum ) { continue; } if ( client.lastPacketTime > serverTime ) { client.lastPacketTime = serverTime; continue; } if ( client.clientState == SCS_ZOMBIE && client.lastPacketTime < zombieTimeout ) { client.channel.Shutdown(); client.clientState = SCS_FREE; continue; } if ( client.clientState >= SCS_PUREWAIT && client.lastPacketTime < clientTimeout ) { DropClient( i, "#str_07137" ); continue; } } } /* ================== idAsyncServer::SendPrintBroadcast ================== */ void idAsyncServer::SendPrintBroadcast( const char *string ) { int i; idBitMsg msg; byte msgBuf[MAX_MESSAGE_SIZE]; msg.Init( msgBuf, sizeof( msgBuf ) ); msg.WriteByte( SERVER_RELIABLE_MESSAGE_PRINT ); msg.WriteString( string ); for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) { if ( clients[i].clientState >= SCS_CONNECTED ) { SendReliableMessage( i, msg ); } } } /* ================== idAsyncServer::SendPrintToClient ================== */ void idAsyncServer::SendPrintToClient( int clientNum, const char *string ) { idBitMsg msg; byte msgBuf[MAX_MESSAGE_SIZE]; serverClient_t &client = clients[clientNum]; if ( client.clientState < SCS_CONNECTED ) { return; } msg.Init( msgBuf, sizeof( msgBuf ) ); msg.WriteByte( SERVER_RELIABLE_MESSAGE_PRINT ); msg.WriteString( string ); SendReliableMessage( clientNum, msg ); } /* ================== idAsyncServer::SendUserInfoBroadcast ================== */ void idAsyncServer::SendUserInfoBroadcast( int userInfoNum, const idDict &info, bool sendToAll ) { idBitMsg msg; byte msgBuf[MAX_MESSAGE_SIZE]; const idDict *gameInfo; bool gameModifiedInfo; gameInfo = game->SetUserInfo( userInfoNum, info, false, true ); if ( gameInfo ) { gameModifiedInfo = true; } else { gameModifiedInfo = false; gameInfo = &info; } if ( userInfoNum == localClientNum ) { common->DPrintf( "local user info modified by server\n" ); cvarSystem->SetCVarsFromDict( *gameInfo ); cvarSystem->ClearModifiedFlags( CVAR_USERINFO ); // don't emit back } msg.Init( msgBuf, sizeof( msgBuf ) ); msg.WriteByte( SERVER_RELIABLE_MESSAGE_CLIENTINFO ); msg.WriteByte( userInfoNum ); if ( gameModifiedInfo || sendToAll ) { msg.WriteBits( 0, 1 ); } else { msg.WriteBits( 1, 1 ); } #if ID_CLIENTINFO_TAGS msg.WriteInt( sessLocal.mapSpawnData.userInfo[userInfoNum].Checksum() ); common->DPrintf( "broadcast for client %d: 0x%x\n", userInfoNum, sessLocal.mapSpawnData.userInfo[userInfoNum].Checksum() ); sessLocal.mapSpawnData.userInfo[userInfoNum].Print(); #endif if ( gameModifiedInfo || sendToAll ) { msg.WriteDeltaDict( *gameInfo, NULL ); } else { msg.WriteDeltaDict( *gameInfo, &sessLocal.mapSpawnData.userInfo[userInfoNum] ); } for ( int i = 0; i < MAX_ASYNC_CLIENTS; i++ ) { if ( clients[i].clientState >= SCS_CONNECTED && ( sendToAll || i != userInfoNum || gameModifiedInfo ) ) { SendReliableMessage( i, msg ); } } sessLocal.mapSpawnData.userInfo[userInfoNum] = *gameInfo; } /* ================== idAsyncServer::UpdateUI if the game modifies userInfo, it will call this through command system we then need to get the info from the game, and broadcast to clients ( using DeltaDict and our current mapSpawnData as a base ) ================== */ void idAsyncServer::UpdateUI( int clientNum ) { const idDict *info = game->GetUserInfo( clientNum ); if ( !info ) { common->Warning( "idAsyncServer::UpdateUI: no info from game\n" ); return; } SendUserInfoBroadcast( clientNum, *info, true ); } /* ================== idAsyncServer::SendUserInfoToClient ================== */ void idAsyncServer::SendUserInfoToClient( int clientNum, int userInfoNum, const idDict &info ) { idBitMsg msg; byte msgBuf[MAX_MESSAGE_SIZE]; if ( clients[clientNum].clientState < SCS_CONNECTED ) { return; } msg.Init( msgBuf, sizeof( msgBuf ) ); msg.WriteByte( SERVER_RELIABLE_MESSAGE_CLIENTINFO ); msg.WriteByte( userInfoNum ); msg.WriteBits( 0, 1 ); #if ID_CLIENTINFO_TAGS msg.WriteInt( 0 ); common->DPrintf( "user info %d to client %d: NULL base\n", userInfoNum, clientNum ); #endif msg.WriteDeltaDict( info, NULL ); SendReliableMessage( clientNum, msg ); } /* ================== idAsyncServer::SendSyncedCvarsBroadcast ================== */ void idAsyncServer::SendSyncedCvarsBroadcast( const idDict &cvars ) { idBitMsg msg; byte msgBuf[MAX_MESSAGE_SIZE]; int i; msg.Init( msgBuf, sizeof( msgBuf ) ); msg.WriteByte( SERVER_RELIABLE_MESSAGE_SYNCEDCVARS ); msg.WriteDeltaDict( cvars, &sessLocal.mapSpawnData.syncedCVars ); for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) { if ( clients[i].clientState >= SCS_CONNECTED ) { SendReliableMessage( i, msg ); } } sessLocal.mapSpawnData.syncedCVars = cvars; } /* ================== idAsyncServer::SendSyncedCvarsToClient ================== */ void idAsyncServer::SendSyncedCvarsToClient( int clientNum, const idDict &cvars ) { idBitMsg msg; byte msgBuf[MAX_MESSAGE_SIZE]; if ( clients[clientNum].clientState < SCS_CONNECTED ) { return; } msg.Init( msgBuf, sizeof( msgBuf ) ); msg.WriteByte( SERVER_RELIABLE_MESSAGE_SYNCEDCVARS ); msg.WriteDeltaDict( cvars, NULL ); SendReliableMessage( clientNum, msg ); } /* ================== idAsyncServer::SendApplySnapshotToClient ================== */ void idAsyncServer::SendApplySnapshotToClient( int clientNum, int sequence ) { idBitMsg msg; byte msgBuf[MAX_MESSAGE_SIZE]; msg.Init( msgBuf, sizeof( msgBuf ) ); msg.WriteByte( SERVER_RELIABLE_MESSAGE_APPLYSNAPSHOT ); msg.WriteInt( sequence ); SendReliableMessage( clientNum, msg ); } /* ================== idAsyncServer::SendEmptyToClient ================== */ bool idAsyncServer::SendEmptyToClient( int clientNum, bool force ) { idBitMsg msg; byte msgBuf[MAX_MESSAGE_SIZE]; serverClient_t &client = clients[clientNum]; if ( client.lastEmptyTime > realTime ) { client.lastEmptyTime = realTime; } if ( !force && ( realTime - client.lastEmptyTime < EMPTY_RESEND_TIME ) ) { return false; } if ( idAsyncNetwork::verbose.GetInteger() ) { common->Printf( "sending empty to client %d: gameInitId = %d, gameFrame = %d, gameTime = %d\n", clientNum, gameInitId, gameFrame, gameTime ); } msg.Init( msgBuf, sizeof( msgBuf ) ); msg.WriteInt( gameInitId ); msg.WriteByte( SERVER_UNRELIABLE_MESSAGE_EMPTY ); client.channel.SendMessage( serverPort, serverTime, msg ); client.lastEmptyTime = realTime; return true; } /* ================== idAsyncServer::SendPingToClient ================== */ bool idAsyncServer::SendPingToClient( int clientNum ) { idBitMsg msg; byte msgBuf[MAX_MESSAGE_SIZE]; serverClient_t &client = clients[clientNum]; if ( client.lastPingTime > realTime ) { client.lastPingTime = realTime; } if ( realTime - client.lastPingTime < PING_RESEND_TIME ) { return false; } if ( idAsyncNetwork::verbose.GetInteger() == 2 ) { common->Printf( "pinging client %d: gameInitId = %d, gameFrame = %d, gameTime = %d\n", clientNum, gameInitId, gameFrame, gameTime ); } msg.Init( msgBuf, sizeof( msgBuf ) ); msg.WriteInt( gameInitId ); msg.WriteByte( SERVER_UNRELIABLE_MESSAGE_PING ); msg.WriteInt( realTime ); client.channel.SendMessage( serverPort, serverTime, msg ); client.lastPingTime = realTime; return true; } /* ================== idAsyncServer::SendGameInitToClient ================== */ void idAsyncServer::SendGameInitToClient( int clientNum ) { idBitMsg msg; byte msgBuf[MAX_MESSAGE_SIZE]; if ( idAsyncNetwork::verbose.GetInteger() ) { common->Printf( "sending gameinit to client %d: gameInitId = %d, gameFrame = %d, gameTime = %d\n", clientNum, gameInitId, gameFrame, gameTime ); } serverClient_t &client = clients[clientNum]; // clear the unsent fragments. might flood winsock but that's ok while( client.channel.UnsentFragmentsLeft() ) { client.channel.SendNextFragment( serverPort, serverTime ); } msg.Init( msgBuf, sizeof( msgBuf ) ); msg.WriteInt( gameInitId ); msg.WriteByte( SERVER_UNRELIABLE_MESSAGE_GAMEINIT ); msg.WriteInt( gameFrame ); msg.WriteInt( gameTime ); msg.WriteDeltaDict( sessLocal.mapSpawnData.serverInfo, NULL ); client.gameInitSequence = client.channel.SendMessage( serverPort, serverTime, msg ); } /* ================== idAsyncServer::SendSnapshotToClient ================== */ bool idAsyncServer::SendSnapshotToClient( int clientNum ) { int i, j, index, numUsercmds; idBitMsg msg; byte msgBuf[MAX_MESSAGE_SIZE]; usercmd_t * last; byte clientInPVS[MAX_ASYNC_CLIENTS >> 3]; serverClient_t &client = clients[clientNum]; if ( serverTime - client.lastSnapshotTime < idAsyncNetwork::serverSnapshotDelay.GetInteger() ) { return false; } if ( idAsyncNetwork::verbose.GetInteger() == 2 ) { common->Printf( "sending snapshot to client %d: gameInitId = %d, gameFrame = %d, gameTime = %d\n", clientNum, gameInitId, gameFrame, gameTime ); } // how far is the client ahead of the server minus the packet delay client.clientAheadTime = client.gameTime - ( gameTime + gameTimeResidual ); // write the snapshot msg.Init( msgBuf, sizeof( msgBuf ) ); msg.WriteInt( gameInitId ); msg.WriteByte( SERVER_UNRELIABLE_MESSAGE_SNAPSHOT ); msg.WriteInt( client.snapshotSequence ); msg.WriteInt( gameFrame ); msg.WriteInt( gameTime ); msg.WriteByte( idMath::ClampChar( client.numDuplicatedUsercmds ) ); msg.WriteShort( idMath::ClampShort( client.clientAheadTime ) ); // write the game snapshot game->ServerWriteSnapshot( clientNum, client.snapshotSequence, msg, clientInPVS, MAX_ASYNC_CLIENTS ); // write the latest user commands from the other clients in the PVS to the snapshot for ( last = NULL, i = 0; i < MAX_ASYNC_CLIENTS; i++ ) { serverClient_t &client = clients[i]; if ( client.clientState == SCS_FREE || i == clientNum ) { continue; } // if the client is not in the PVS if ( !( clientInPVS[i >> 3] & ( 1 << ( i & 7 ) ) ) ) { continue; } int maxRelay = idMath::ClampInt( 1, MAX_USERCMD_RELAY, idAsyncNetwork::serverMaxUsercmdRelay.GetInteger() ); // Max( 1, to always send at least one cmd, which we know we have because we call DuplicateUsercmds in RunFrame numUsercmds = Max( 1, Min( client.gameFrame, gameFrame + maxRelay ) - gameFrame ); msg.WriteByte( i ); msg.WriteByte( numUsercmds ); for ( j = 0; j < numUsercmds; j++ ) { index = ( gameFrame + j ) & ( MAX_USERCMD_BACKUP - 1 ); idAsyncNetwork::WriteUserCmdDelta( msg, userCmds[index][i], last ); last = &userCmds[index][i]; } } msg.WriteByte( MAX_ASYNC_CLIENTS ); client.channel.SendMessage( serverPort, serverTime, msg ); client.lastSnapshotTime = serverTime; client.snapshotSequence++; client.numDuplicatedUsercmds = 0; return true; } /* ================== idAsyncServer::ProcessUnreliableClientMessage ================== */ void idAsyncServer::ProcessUnreliableClientMessage( int clientNum, const idBitMsg &msg ) { int i, id, acknowledgeSequence, clientGameInitId, clientGameFrame, numUsercmds, index; usercmd_t *last; serverClient_t &client = clients[clientNum]; if ( client.clientState == SCS_ZOMBIE ) { return; } acknowledgeSequence = msg.ReadInt(); clientGameInitId = msg.ReadInt(); // while loading a map the client may send empty messages to keep the connection alive if ( clientGameInitId == GAME_INIT_ID_MAP_LOAD ) { if ( idAsyncNetwork::verbose.GetInteger() ) { common->Printf( "ignore unreliable msg from client %d, gameInitId == ID_MAP_LOAD\n", clientNum ); } return; } // check if the client is in the right game if ( clientGameInitId != gameInitId ) { if ( acknowledgeSequence > client.gameInitSequence ) { // the client is connected but not in the right game client.clientState = SCS_CONNECTED; // send game init to client SendGameInitToClient( clientNum ); if ( sessLocal.mapSpawnData.serverInfo.GetBool( "si_pure" ) ) { client.clientState = SCS_PUREWAIT; if ( !SendReliablePureToClient( clientNum ) ) { client.clientState = SCS_CONNECTED; } } } else if ( idAsyncNetwork::verbose.GetInteger() ) { common->Printf( "ignore unreliable msg from client %d, wrong gameInit, old sequence\n", clientNum ); } return; } client.acknowledgeSnapshotSequence = msg.ReadInt(); if ( client.clientState == SCS_CONNECTED ) { // the client is in the right game client.clientState = SCS_INGAME; // send the user info of other clients for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) { if ( clients[i].clientState >= SCS_CONNECTED && i != clientNum ) { SendUserInfoToClient( clientNum, i, sessLocal.mapSpawnData.userInfo[i] ); } } // send synchronized cvars to client SendSyncedCvarsToClient( clientNum, sessLocal.mapSpawnData.syncedCVars ); SendEnterGameToClient( clientNum ); // get the client running in the game game->ServerClientBegin( clientNum ); // write any reliable messages to initialize the client game state game->ServerWriteInitialReliableMessages( clientNum ); } else if ( client.clientState == SCS_INGAME ) { // apply the last snapshot the client received if ( game->ServerApplySnapshot( clientNum, client.acknowledgeSnapshotSequence ) ) { SendApplySnapshotToClient( clientNum, client.acknowledgeSnapshotSequence ); } } // process the unreliable message id = msg.ReadByte(); switch( id ) { case CLIENT_UNRELIABLE_MESSAGE_EMPTY: { if ( idAsyncNetwork::verbose.GetInteger() ) { common->Printf( "received empty message for client %d\n", clientNum ); } break; } case CLIENT_UNRELIABLE_MESSAGE_PINGRESPONSE: { client.clientPing = realTime - msg.ReadInt(); break; } case CLIENT_UNRELIABLE_MESSAGE_USERCMD: { client.clientPrediction = msg.ReadShort(); // read user commands clientGameFrame = msg.ReadInt(); numUsercmds = msg.ReadByte(); for ( last = NULL, i = clientGameFrame - numUsercmds + 1; i <= clientGameFrame; i++ ) { index = i & ( MAX_USERCMD_BACKUP - 1 ); idAsyncNetwork::ReadUserCmdDelta( msg, userCmds[index][clientNum], last ); userCmds[index][clientNum].gameFrame = i; userCmds[index][clientNum].duplicateCount = 0; if ( idAsyncNetwork::UsercmdInputChanged( userCmds[( i - 1 ) & ( MAX_USERCMD_BACKUP - 1 )][clientNum], userCmds[index][clientNum] ) ) { client.lastInputTime = serverTime; } last = &userCmds[index][clientNum]; } if ( last ) { client.gameFrame = last->gameFrame; client.gameTime = last->gameTime; } if ( idAsyncNetwork::verbose.GetInteger() == 2 ) { common->Printf( "received user command for client %d, gameInitId = %d, gameFrame, %d gameTime %d\n", clientNum, clientGameInitId, client.gameFrame, client.gameTime ); } break; } default: { common->Printf( "unknown unreliable message %d from client %d\n", id, clientNum ); break; } } } /* ================== idAsyncServer::ProcessReliableClientMessages ================== */ void idAsyncServer::ProcessReliableClientMessages( int clientNum ) { idBitMsg msg; byte msgBuf[MAX_MESSAGE_SIZE]; byte id; serverClient_t &client = clients[clientNum]; msg.Init( msgBuf, sizeof( msgBuf ) ); while ( client.channel.GetReliableMessage( msg ) ) { id = msg.ReadByte(); switch( id ) { case CLIENT_RELIABLE_MESSAGE_CLIENTINFO: { idDict info; msg.ReadDeltaDict( info, &sessLocal.mapSpawnData.userInfo[clientNum] ); SendUserInfoBroadcast( clientNum, info ); break; } case CLIENT_RELIABLE_MESSAGE_PRINT: { char string[MAX_STRING_CHARS]; msg.ReadString( string, sizeof( string ) ); common->Printf( "%s\n", string ); break; } case CLIENT_RELIABLE_MESSAGE_DISCONNECT: { DropClient( clientNum, "#str_07138" ); break; } case CLIENT_RELIABLE_MESSAGE_PURE: { // we get this message once the client has successfully updated it's pure list ProcessReliablePure( clientNum, msg ); break; } default: { // pass reliable message on to game code game->ServerProcessReliableMessage( clientNum, msg ); break; } } } } /* ================== idAsyncServer::ProcessAuthMessage ================== */ void idAsyncServer::ProcessAuthMessage( const idBitMsg &msg ) { netadr_t client_from; char client_guid[ 12 ], string[ MAX_STRING_CHARS ]; int i, clientId; authReply_t reply; authReplyMsg_t replyMsg = AUTH_REPLY_WAITING; idStr replyPrintMsg; reply = (authReply_t)msg.ReadByte(); if ( reply <= 0 || reply >= AUTH_MAXSTATES ) { common->DPrintf( "auth: invalid reply %d\n", reply ); return; } clientId = msg.ReadShort( ); msg.ReadNetadr( &client_from ); msg.ReadString( client_guid, sizeof( client_guid ) ); if ( reply != AUTH_OK ) { replyMsg = (authReplyMsg_t)msg.ReadByte(); if ( replyMsg <= 0 || replyMsg >= AUTH_REPLY_MAXSTATES ) { common->DPrintf( "auth: invalid reply msg %d\n", replyMsg ); return; } if ( replyMsg == AUTH_REPLY_PRINT ) { msg.ReadString( string, MAX_STRING_CHARS ); replyPrintMsg = string; } } lastAuthTime = serverTime; // no message parsing below for ( i = 0; i < MAX_CHALLENGES; i++ ) { if ( !challenges[i].connected && challenges[ i ].clientId == clientId ) { // return if something is wrong // break if we have found a valid auth if ( !strlen( challenges[ i ].guid ) ) { common->DPrintf( "auth: client %s has no guid yet\n", Sys_NetAdrToString( challenges[ i ].address ) ); return; } if ( idStr::Cmp( challenges[ i ].guid, client_guid ) ) { common->DPrintf( "auth: client %s %s not matched, auth server says guid %s\n", Sys_NetAdrToString( challenges[ i ].address ), challenges[i].guid, client_guid ); return; } if ( !Sys_CompareNetAdrBase( client_from, challenges[i].address ) ) { // let auth work when server and master don't see the same IP common->DPrintf( "auth: matched guid '%s' for != IPs %s and %s\n", client_guid, Sys_NetAdrToString( client_from ), Sys_NetAdrToString( challenges[i].address ) ); } break; } } if ( i >= MAX_CHALLENGES ) { common->DPrintf( "auth: failed client lookup %s %s\n", Sys_NetAdrToString( client_from ), client_guid ); return; } if ( challenges[ i ].authState != CDK_WAIT ) { common->DWarning( "auth: challenge 0x%x %s authState %d != CDK_WAIT", challenges[ i ].challenge, Sys_NetAdrToString( challenges[ i ].address ), challenges[ i ].authState ); return; } idStr::snPrintf( challenges[ i ].guid, 12, client_guid ); if ( reply == AUTH_OK ) { challenges[ i ].authState = CDK_OK; common->Printf( "client %s %s is authed\n", Sys_NetAdrToString( client_from ), client_guid ); } else { const char *msg; if ( replyMsg != AUTH_REPLY_PRINT ) { msg = authReplyMsg[ replyMsg ]; } else { msg = replyPrintMsg.c_str(); } // maybe localize it const char *l_msg = common->GetLanguageDict()->GetString( msg ); common->DPrintf( "auth: client %s %s - %s %s\n", Sys_NetAdrToString( client_from ), client_guid, authReplyStr[ reply ], l_msg ); challenges[ i ].authReply = reply; challenges[ i ].authReplyMsg = replyMsg; challenges[ i ].authReplyPrint = replyPrintMsg; } } /* ================== idAsyncServer::ProcessChallengeMessage ================== */ void idAsyncServer::ProcessChallengeMessage( const netadr_t from, const idBitMsg &msg ) { int i, clientId, oldest, oldestTime; idBitMsg outMsg; byte msgBuf[MAX_MESSAGE_SIZE]; clientId = msg.ReadInt(); oldest = 0; oldestTime = 0x7fffffff; // see if we already have a challenge for this ip for ( i = 0; i < MAX_CHALLENGES; i++ ) { if ( !challenges[i].connected && Sys_CompareNetAdrBase( from, challenges[i].address ) && clientId == challenges[i].clientId ) { break; } if ( challenges[i].time < oldestTime ) { oldestTime = challenges[i].time; oldest = i; } } if ( i >= MAX_CHALLENGES ) { // this is the first time this client has asked for a challenge i = oldest; challenges[i].address = from; challenges[i].clientId = clientId; challenges[i].challenge = ( (rand() << 16) ^ rand() ) ^ serverTime; challenges[i].time = serverTime; challenges[i].connected = false; challenges[i].authState = CDK_WAIT; challenges[i].authReply = AUTH_NONE; challenges[i].authReplyMsg = AUTH_REPLY_WAITING; challenges[i].authReplyPrint = ""; challenges[i].guid[0] = '\0'; } challenges[i].pingTime = serverTime; common->Printf( "sending challenge 0x%x to %s\n", challenges[i].challenge, Sys_NetAdrToString( from ) ); outMsg.Init( msgBuf, sizeof( msgBuf ) ); outMsg.WriteShort( CONNECTIONLESS_MESSAGE_ID ); outMsg.WriteString( "challengeResponse" ); outMsg.WriteInt( challenges[i].challenge ); outMsg.WriteShort( serverId ); outMsg.WriteString( cvarSystem->GetCVarString( "fs_game_base" ) ); outMsg.WriteString( cvarSystem->GetCVarString( "fs_game" ) ); serverPort.SendPacket( from, outMsg.GetData(), outMsg.GetSize() ); #if ID_ENFORCE_KEY_CLIENT if ( Sys_IsLANAddress( from ) ) { // no CD Key check for LAN clients challenges[i].authState = CDK_OK; } else { if ( idAsyncNetwork::LANServer.GetBool() ) { common->Printf( "net_LANServer is enabled. Client %s is not a LAN address, will be rejected\n", Sys_NetAdrToString( from ) ); challenges[ i ].authState = CDK_ONLYLAN; } else { // emit a cd key confirmation request outMsg.BeginWriting(); outMsg.WriteShort( CONNECTIONLESS_MESSAGE_ID ); outMsg.WriteString( "srvAuth" ); outMsg.WriteInt( ASYNC_PROTOCOL_VERSION ); outMsg.WriteNetadr( from ); outMsg.WriteInt( -1 ); // this identifies "challenge" auth vs "connect" auth // protocol 1.37 addition outMsg.WriteByte( fileSystem->RunningD3XP() ); serverPort.SendPacket( idAsyncNetwork::GetMasterAddress(), outMsg.GetData(), outMsg.GetSize() ); } } #else if (! Sys_IsLANAddress( from ) ) { common->Printf( "Build Does not have CD Key Enforcement enabled. Client %s is not a LAN address, but will be accepted\n", Sys_NetAdrToString( from ) ); } challenges[i].authState = CDK_OK; #endif } /* ================== idAsyncServer::SendPureServerMessage ================== */ bool idAsyncServer::SendPureServerMessage( const netadr_t to ) { idBitMsg outMsg; byte msgBuf[ MAX_MESSAGE_SIZE ]; int serverChecksums[ MAX_PURE_PAKS ]; int i; fileSystem->GetPureServerChecksums( serverChecksums ); if ( !serverChecksums[ 0 ] ) { // happens if you run fully expanded assets with si_pure 1 common->Warning( "pure server has no pak files referenced" ); return false; } common->DPrintf( "client %s: sending pure pak list\n", Sys_NetAdrToString( to ) ); // send our list of required paks outMsg.Init( msgBuf, sizeof( msgBuf ) ); outMsg.WriteShort( CONNECTIONLESS_MESSAGE_ID ); outMsg.WriteString( "pureServer" ); i = 0; while ( serverChecksums[ i ] ) { outMsg.WriteInt( serverChecksums[ i++ ] ); } outMsg.WriteInt( 0 ); serverPort.SendPacket( to, outMsg.GetData(), outMsg.GetSize() ); return true; } /* ================== idAsyncServer::SendReliablePureToClient ================== */ bool idAsyncServer::SendReliablePureToClient( int clientNum ) { idBitMsg msg; byte msgBuf[ MAX_MESSAGE_SIZE ]; int serverChecksums[ MAX_PURE_PAKS ]; int i; fileSystem->GetPureServerChecksums( serverChecksums ); if ( !serverChecksums[ 0 ] ) { // happens if you run fully expanded assets with si_pure 1 common->Warning( "pure server has no pak files referenced" ); return false; } common->DPrintf( "client %d: sending pure pak list (reliable channel) @ gameInitId %d\n", clientNum, gameInitId ); msg.Init( msgBuf, sizeof( msgBuf ) ); msg.WriteByte( SERVER_RELIABLE_MESSAGE_PURE ); msg.WriteInt( gameInitId ); i = 0; while ( serverChecksums[ i ] ) { msg.WriteInt( serverChecksums[ i++ ] ); } msg.WriteInt( 0 ); SendReliableMessage( clientNum, msg ); return true; } /* ================== idAsyncServer::ValidateChallenge ================== */ int idAsyncServer::ValidateChallenge( const netadr_t from, int challenge, int clientId ) { int i; for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) { const serverClient_t &client = clients[i]; if ( client.clientState == SCS_FREE ) { continue; } if ( Sys_CompareNetAdrBase( from, client.channel.GetRemoteAddress() ) && ( clientId == client.clientId || from.port == client.channel.GetRemoteAddress().port ) ) { if ( serverTime - client.lastConnectTime < MIN_RECONNECT_TIME ) { common->Printf( "%s: reconnect rejected : too soon\n", Sys_NetAdrToString( from ) ); return -1; } break; } } for ( i = 0; i < MAX_CHALLENGES; i++ ) { if ( Sys_CompareNetAdrBase( from, challenges[i].address ) && from.port == challenges[i].address.port ) { if ( challenge == challenges[i].challenge ) { break; } } } if ( i == MAX_CHALLENGES ) { PrintOOB( from, SERVER_PRINT_BADCHALLENGE, "#str_04840" ); return -1; } return i; } /* ================== idAsyncServer::ProcessConnectMessage ================== */ void idAsyncServer::ProcessConnectMessage( const netadr_t from, const idBitMsg &msg ) { int clientNum, protocol, clientDataChecksum, challenge, clientId, ping, clientRate; idBitMsg outMsg; byte msgBuf[ MAX_MESSAGE_SIZE ]; char guid[ 12 ]; char password[ 17 ]; int i, ichallenge, islot, numClients; protocol = msg.ReadInt(); // check the protocol version if ( protocol != ASYNC_PROTOCOL_VERSION ) { // that's a msg back to a client, we don't know about it's localization, so send english PrintOOB( from, SERVER_PRINT_BADPROTOCOL, va( "server uses protocol %d.%d\n", ASYNC_PROTOCOL_MAJOR, ASYNC_PROTOCOL_MINOR ) ); return; } clientDataChecksum = msg.ReadInt(); challenge = msg.ReadInt(); clientId = msg.ReadShort(); clientRate = msg.ReadInt(); // check the client data - only for non pure servers if ( !sessLocal.mapSpawnData.serverInfo.GetInt( "si_pure" ) && clientDataChecksum != serverDataChecksum ) { PrintOOB( from, SERVER_PRINT_MISC, "#str_04842" ); return; } if ( ( ichallenge = ValidateChallenge( from, challenge, clientId ) ) == -1 ) { return; } msg.ReadString( guid, sizeof( guid ) ); switch ( challenges[ ichallenge ].authState ) { case CDK_PUREWAIT: SendPureServerMessage( from ); return; case CDK_ONLYLAN: common->DPrintf( "%s: not a lan client\n", Sys_NetAdrToString( from ) ); PrintOOB( from, SERVER_PRINT_MISC, "#str_04843" ); return; case CDK_WAIT: if ( challenges[ ichallenge ].authReply == AUTH_NONE && Min( serverTime - lastAuthTime, serverTime - challenges[ ichallenge ].time ) > AUTHORIZE_TIMEOUT ) { common->DPrintf( "%s: Authorize server timed out\n", Sys_NetAdrToString( from ) ); break; // will continue with the connecting process } const char *msg, *l_msg; if ( challenges[ ichallenge ].authReplyMsg != AUTH_REPLY_PRINT ) { msg = authReplyMsg[ challenges[ ichallenge ].authReplyMsg ]; } else { msg = challenges[ ichallenge ].authReplyPrint.c_str(); } l_msg = common->GetLanguageDict()->GetString( msg ); common->DPrintf( "%s: %s\n", Sys_NetAdrToString( from ), l_msg ); if ( challenges[ ichallenge ].authReplyMsg == AUTH_REPLY_UNKNOWN || challenges[ ichallenge ].authReplyMsg == AUTH_REPLY_WAITING ) { // the client may be trying to connect to us in LAN mode, and the server disagrees // let the client know so it would switch to authed connection idBitMsg outMsg; byte msgBuf[ MAX_MESSAGE_SIZE ]; outMsg.Init( msgBuf, sizeof( msgBuf ) ); outMsg.WriteShort( CONNECTIONLESS_MESSAGE_ID ); outMsg.WriteString( "authrequired" ); serverPort.SendPacket( from, outMsg.GetData(), outMsg.GetSize() ); } PrintOOB( from, SERVER_PRINT_MISC, msg ); // update the guid in the challenges idStr::snPrintf( challenges[ ichallenge ].guid, sizeof( challenges[ ichallenge ].guid ), guid ); // once auth replied denied, stop sending further requests if ( challenges[ ichallenge ].authReply != AUTH_DENY ) { // emit a cd key confirmation request outMsg.Init( msgBuf, sizeof( msgBuf ) ); outMsg.WriteShort( CONNECTIONLESS_MESSAGE_ID ); outMsg.WriteString( "srvAuth" ); outMsg.WriteInt( ASYNC_PROTOCOL_VERSION ); outMsg.WriteNetadr( from ); outMsg.WriteInt( clientId ); outMsg.WriteString( guid ); // protocol 1.37 addition outMsg.WriteByte( fileSystem->RunningD3XP() ); serverPort.SendPacket( idAsyncNetwork::GetMasterAddress(), outMsg.GetData(), outMsg.GetSize() ); } return; default: assert( challenges[ ichallenge ].authState == CDK_OK || challenges[ ichallenge ].authState == CDK_PUREOK ); } numClients = 0; for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) { serverClient_t &client = clients[ i ]; if ( client.clientState >= SCS_PUREWAIT ) { numClients++; } } // game may be passworded, client banned by IP or GUID // if authState == CDK_PUREOK, the check was already performed once before entering pure checks // but meanwhile, the max players may have been reached msg.ReadString( password, sizeof( password ) ); char reason[MAX_STRING_CHARS]; allowReply_t reply = game->ServerAllowClient( numClients, Sys_NetAdrToString( from ), guid, password, reason ); if ( reply != ALLOW_YES ) { common->DPrintf( "game denied connection for %s\n", Sys_NetAdrToString( from ) ); // SERVER_PRINT_GAMEDENY passes the game opcode through. Don't use PrintOOB outMsg.Init( msgBuf, sizeof( msgBuf ) ); outMsg.WriteShort( CONNECTIONLESS_MESSAGE_ID ); outMsg.WriteString( "print" ); outMsg.WriteInt( SERVER_PRINT_GAMEDENY ); outMsg.WriteInt( reply ); outMsg.WriteString( reason ); serverPort.SendPacket( from, outMsg.GetData(), outMsg.GetSize() ); return; } // enter pure checks if necessary if ( sessLocal.mapSpawnData.serverInfo.GetInt( "si_pure" ) && challenges[ ichallenge ].authState != CDK_PUREOK ) { if ( SendPureServerMessage( from ) ) { challenges[ ichallenge ].authState = CDK_PUREWAIT; return; } } // push back decl checksum here when running pure. just an additional safe check if ( sessLocal.mapSpawnData.serverInfo.GetInt( "si_pure" ) && clientDataChecksum != serverDataChecksum ) { PrintOOB( from, SERVER_PRINT_MISC, "#str_04844" ); return; } ping = serverTime - challenges[ ichallenge ].pingTime; common->Printf( "challenge from %s connecting with %d ping\n", Sys_NetAdrToString( from ), ping ); challenges[ ichallenge ].connected = true; // find a slot for the client for ( islot = 0; islot < 3; islot++ ) { for ( clientNum = 0; clientNum < MAX_ASYNC_CLIENTS; clientNum++ ) { serverClient_t &client = clients[ clientNum ]; if ( islot == 0 ) { // if this slot uses the same IP and port if ( Sys_CompareNetAdrBase( from, client.channel.GetRemoteAddress() ) && ( clientId == client.clientId || from.port == client.channel.GetRemoteAddress().port ) ) { break; } } else if ( islot == 1 ) { // if this client is not connected and the slot uses the same IP if ( client.clientState >= SCS_PUREWAIT ) { continue; } if ( Sys_CompareNetAdrBase( from, client.channel.GetRemoteAddress() ) ) { break; } } else if ( islot == 2 ) { // if this slot is free if ( client.clientState == SCS_FREE ) { break; } } } if ( clientNum < MAX_ASYNC_CLIENTS ) { // initialize clients[ clientNum ].channel.Init( from, serverId ); strncpy( clients[ clientNum ].guid, guid, 12 ); clients[ clientNum ].guid[11] = 0; break; } } // if no free spots available if ( clientNum >= MAX_ASYNC_CLIENTS ) { PrintOOB( from, SERVER_PRINT_MISC, "#str_04845" ); return; } common->Printf( "sending connect response to %s\n", Sys_NetAdrToString( from ) ); // send connect response message outMsg.Init( msgBuf, sizeof( msgBuf ) ); outMsg.WriteShort( CONNECTIONLESS_MESSAGE_ID ); outMsg.WriteString( "connectResponse" ); outMsg.WriteInt( clientNum ); outMsg.WriteInt( gameInitId ); outMsg.WriteInt( gameFrame ); outMsg.WriteInt( gameTime ); outMsg.WriteDeltaDict( sessLocal.mapSpawnData.serverInfo, NULL ); serverPort.SendPacket( from, outMsg.GetData(), outMsg.GetSize() ); InitClient( clientNum, clientId, clientRate ); clients[clientNum].gameInitSequence = 1; clients[clientNum].snapshotSequence = 1; // clear the challenge struct so a reconnect from this client IP starts clean memset( &challenges[ ichallenge ], 0, sizeof( challenge_t ) ); } /* ================== idAsyncServer::VerifyChecksumMessage ================== */ bool idAsyncServer::VerifyChecksumMessage( int clientNum, const netadr_t *from, const idBitMsg &msg, idStr &reply ) { int i, numChecksums; int checksums[ MAX_PURE_PAKS ]; int serverChecksums[ MAX_PURE_PAKS ]; // pak checksums, in a 0-terminated list numChecksums = 0; do { i = msg.ReadInt( ); checksums[ numChecksums++ ] = i; // just to make sure a broken client doesn't crash us if ( numChecksums >= MAX_PURE_PAKS ) { common->Warning( "MAX_PURE_PAKS ( %d ) exceeded in idAsyncServer::ProcessPureMessage\n", MAX_PURE_PAKS ); sprintf( reply, "#str_07144" ); return false; } } while ( i ); numChecksums--; fileSystem->GetPureServerChecksums( serverChecksums ); assert( serverChecksums[ 0 ] ); for ( i = 0; serverChecksums[ i ] != 0; i++ ) { if ( checksums[ i ] != serverChecksums[ i ] ) { common->DPrintf( "client %s: pak missing ( 0x%x )\n", from ? Sys_NetAdrToString( *from ) : va( "%d", clientNum ), serverChecksums[ i ] ); sprintf( reply, "pak missing ( 0x%x )\n", serverChecksums[ i ] ); return false; } } if ( checksums[ i ] != 0 ) { common->DPrintf( "client %s: extra pak file referenced ( 0x%x )\n", from ? Sys_NetAdrToString( *from ) : va( "%d", clientNum ), checksums[ i ] ); sprintf( reply, "extra pak file referenced ( 0x%x )\n", checksums[ i ] ); return false; } return true; } /* ================== idAsyncServer::ProcessPureMessage ================== */ void idAsyncServer::ProcessPureMessage( const netadr_t from, const idBitMsg &msg ) { int iclient, challenge, clientId; idStr reply; challenge = msg.ReadInt(); clientId = msg.ReadShort(); if ( ( iclient = ValidateChallenge( from, challenge, clientId ) ) == -1 ) { return; } if ( challenges[ iclient ].authState != CDK_PUREWAIT ) { common->DPrintf( "client %s: got pure message, not in CDK_PUREWAIT\n", Sys_NetAdrToString( from ) ); return; } if ( !VerifyChecksumMessage( iclient, &from, msg, reply ) ) { PrintOOB( from, SERVER_PRINT_MISC, reply ); return; } common->DPrintf( "client %s: passed pure checks\n", Sys_NetAdrToString( from ) ); challenges[ iclient ].authState = CDK_PUREOK; // next connect message will get the client through completely } /* ================== idAsyncServer::ProcessReliablePure ================== */ void idAsyncServer::ProcessReliablePure( int clientNum, const idBitMsg &msg ) { idStr reply; idBitMsg outMsg; byte msgBuf[MAX_MESSAGE_SIZE]; int clientGameInitId; clientGameInitId = msg.ReadInt(); if ( clientGameInitId != gameInitId ) { common->DPrintf( "client %d: ignoring reliable pure from an old gameInit (%d)\n", clientNum, clientGameInitId ); return; } if ( clients[ clientNum ].clientState != SCS_PUREWAIT ) { // should not happen unless something is very wrong. still, don't let this crash us, just get rid of the client common->DPrintf( "client %d: got reliable pure while != SCS_PUREWAIT, sending a reload\n", clientNum ); outMsg.Init( msgBuf, sizeof( msgBuf ) ); outMsg.WriteByte( SERVER_RELIABLE_MESSAGE_RELOAD ); SendReliableMessage( clientNum, msg ); // go back to SCS_CONNECTED to sleep on the client until it goes away for a reconnect clients[ clientNum ].clientState = SCS_CONNECTED; return; } if ( !VerifyChecksumMessage( clientNum, NULL, msg, reply ) ) { DropClient( clientNum, reply ); return; } common->DPrintf( "client %d: passed pure checks (reliable channel)\n", clientNum ); clients[ clientNum ].clientState = SCS_CONNECTED; } /* ================== idAsyncServer::RemoteConsoleOutput ================== */ void idAsyncServer::RemoteConsoleOutput( const char *string ) { noRconOutput = false; PrintOOB( rconAddress, SERVER_PRINT_RCON, string ); } /* ================== RConRedirect ================== */ void RConRedirect( const char *string ) { idAsyncNetwork::server.RemoteConsoleOutput( string ); } /* ================== idAsyncServer::ProcessRemoteConsoleMessage ================== */ void idAsyncServer::ProcessRemoteConsoleMessage( const netadr_t from, const idBitMsg &msg ) { idBitMsg outMsg; byte msgBuf[952]; char string[MAX_STRING_CHARS]; if ( idAsyncNetwork::serverRemoteConsolePassword.GetString()[0] == '\0' ) { PrintOOB( from, SERVER_PRINT_MISC, "#str_04846" ); return; } msg.ReadString( string, sizeof( string ) ); if ( idStr::Icmp( string, idAsyncNetwork::serverRemoteConsolePassword.GetString() ) != 0 ) { PrintOOB( from, SERVER_PRINT_MISC, "#str_04847" ); return; } msg.ReadString( string, sizeof( string ) ); common->Printf( "rcon from %s: %s\n", Sys_NetAdrToString( from ), string ); rconAddress = from; noRconOutput = true; common->BeginRedirect( (char *)msgBuf, sizeof( msgBuf ), RConRedirect ); cmdSystem->BufferCommandText( CMD_EXEC_NOW, string ); common->EndRedirect(); if ( noRconOutput ) { PrintOOB( rconAddress, SERVER_PRINT_RCON, "#str_04848" ); } } /* ================== idAsyncServer::ProcessGetInfoMessage ================== */ void idAsyncServer::ProcessGetInfoMessage( const netadr_t from, const idBitMsg &msg ) { int i, challenge; idBitMsg outMsg; byte msgBuf[MAX_MESSAGE_SIZE]; if ( !IsActive() ) { return; } common->DPrintf( "Sending info response to %s\n", Sys_NetAdrToString( from ) ); challenge = msg.ReadInt(); outMsg.Init( msgBuf, sizeof( msgBuf ) ); outMsg.WriteShort( CONNECTIONLESS_MESSAGE_ID ); outMsg.WriteString( "infoResponse" ); outMsg.WriteInt( challenge ); outMsg.WriteInt( ASYNC_PROTOCOL_VERSION ); outMsg.WriteDeltaDict( sessLocal.mapSpawnData.serverInfo, NULL ); for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) { serverClient_t &client = clients[i]; if ( client.clientState < SCS_CONNECTED ) { continue; } outMsg.WriteByte( i ); outMsg.WriteShort( client.clientPing ); outMsg.WriteInt( client.channel.GetMaxOutgoingRate() ); outMsg.WriteString( sessLocal.mapSpawnData.userInfo[i].GetString( "ui_name", "Player" ) ); } outMsg.WriteByte( MAX_ASYNC_CLIENTS ); // Stradex: Originally Doom3 did outMsg.WriteLong( fileSystem->GetOSMask() ); here // dhewm3 eliminated GetOSMask() and WriteLong() became WriteInt() as it's supposed to write an int32 // Sending -1 (instead of nothing at all) restores compatibility with id's masterserver. outMsg.WriteInt( -1 ); serverPort.SendPacket( from, outMsg.GetData(), outMsg.GetSize() ); } /* =============== idAsyncServer::PrintLocalServerInfo see (client) "getInfo" -> (server) "infoResponse" -> (client)ProcessGetInfoMessage =============== */ void idAsyncServer::PrintLocalServerInfo( void ) { int i; common->Printf( "server '%s' IP = %s\nprotocol %d.%d\n", sessLocal.mapSpawnData.serverInfo.GetString( "si_name" ), Sys_NetAdrToString( serverPort.GetAdr() ), ASYNC_PROTOCOL_MAJOR, ASYNC_PROTOCOL_MINOR ); sessLocal.mapSpawnData.serverInfo.Print(); for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) { serverClient_t &client = clients[i]; if ( client.clientState < SCS_CONNECTED ) { continue; } common->Printf( "client %2d: %s, ping = %d, rate = %d\n", i, sessLocal.mapSpawnData.userInfo[i].GetString( "ui_name", "Player" ), client.clientPing, client.channel.GetMaxOutgoingRate() ); } } /* ================== idAsyncServer::ConnectionlessMessage ================== */ bool idAsyncServer::ConnectionlessMessage( const netadr_t from, const idBitMsg &msg ) { char string[MAX_STRING_CHARS*2]; // M. Quinn - Even Balance - PB Packets need more than 1024 msg.ReadString( string, sizeof( string ) ); // info request if ( idStr::Icmp( string, "getInfo" ) == 0 ) { ProcessGetInfoMessage( from, msg ); return false; } // remote console if ( idStr::Icmp( string, "rcon" ) == 0 ) { ProcessRemoteConsoleMessage( from, msg ); return true; } if ( !active ) { PrintOOB( from, SERVER_PRINT_MISC, "#str_04849" ); return false; } // challenge from a client if ( idStr::Icmp( string, "challenge" ) == 0 ) { ProcessChallengeMessage( from, msg ); return false; } // connect from a client if ( idStr::Icmp( string, "connect" ) == 0 ) { ProcessConnectMessage( from, msg ); return false; } // pure mesasge from a client if ( idStr::Icmp( string, "pureClient" ) == 0 ) { ProcessPureMessage( from, msg ); return false; } // download request if ( idStr::Icmp( string, "downloadRequest" ) == 0 ) { ProcessDownloadRequestMessage( from, msg ); } // auth server if ( idStr::Icmp( string, "auth" ) == 0 ) { if ( !Sys_CompareNetAdrBase( from, idAsyncNetwork::GetMasterAddress() ) ) { common->Printf( "auth: bad source %s\n", Sys_NetAdrToString( from ) ); return false; } if ( idAsyncNetwork::LANServer.GetBool() ) { common->Printf( "auth message from master. net_LANServer is enabled, ignored.\n" ); } ProcessAuthMessage( msg ); return false; } return false; } /* ================== idAsyncServer::ProcessMessage ================== */ bool idAsyncServer::ProcessMessage( const netadr_t from, idBitMsg &msg ) { int i, id, sequence; idBitMsg outMsg; byte msgBuf[MAX_MESSAGE_SIZE]; id = msg.ReadShort(); // check for a connectionless message if ( id == CONNECTIONLESS_MESSAGE_ID ) { return ConnectionlessMessage( from, msg ); } if ( msg.GetRemaingData() < 4 ) { common->DPrintf( "%s: tiny packet\n", Sys_NetAdrToString( from ) ); return false; } // find out which client the message is from for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) { serverClient_t &client = clients[i]; if ( client.clientState == SCS_FREE ) { continue; } // This does not compare the UDP port, because some address translating // routers will change that at arbitrary times. if ( !Sys_CompareNetAdrBase( from, client.channel.GetRemoteAddress() ) || id != client.clientId ) { continue; } // make sure it is a valid, in sequence packet if ( !client.channel.Process( from, serverTime, msg, sequence ) ) { return false; // out of order, duplicated, fragment, etc. } // zombie clients still need to do the channel processing to make sure they don't // need to retransmit the final reliable message, but they don't do any other processing if ( client.clientState == SCS_ZOMBIE ) { return false; } client.lastPacketTime = serverTime; ProcessReliableClientMessages( i ); ProcessUnreliableClientMessage( i, msg ); return false; } // if we received a sequenced packet from an address we don't recognize, // send an out of band disconnect packet to it outMsg.Init( msgBuf, sizeof( msgBuf ) ); outMsg.WriteShort( CONNECTIONLESS_MESSAGE_ID ); outMsg.WriteString( "disconnect" ); serverPort.SendPacket( from, outMsg.GetData(), outMsg.GetSize() ); return false; } /* ================== idAsyncServer::SendReliableGameMessage ================== */ void idAsyncServer::SendReliableGameMessage( int clientNum, const idBitMsg &msg ) { int i; idBitMsg outMsg; byte msgBuf[MAX_MESSAGE_SIZE]; outMsg.Init( msgBuf, sizeof( msgBuf ) ); outMsg.WriteByte( SERVER_RELIABLE_MESSAGE_GAME ); outMsg.WriteData( msg.GetData(), msg.GetSize() ); if ( clientNum >= 0 && clientNum < MAX_ASYNC_CLIENTS ) { if ( clients[clientNum].clientState == SCS_INGAME ) { SendReliableMessage( clientNum, outMsg ); } return; } for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) { if ( clients[i].clientState != SCS_INGAME ) { continue; } SendReliableMessage( i, outMsg ); } } /* ================== idAsyncServer::LocalClientSendReliableMessageExcluding ================== */ void idAsyncServer::SendReliableGameMessageExcluding( int clientNum, const idBitMsg &msg ) { int i; idBitMsg outMsg; byte msgBuf[MAX_MESSAGE_SIZE]; assert( clientNum >= 0 && clientNum < MAX_ASYNC_CLIENTS ); outMsg.Init( msgBuf, sizeof( msgBuf ) ); outMsg.WriteByte( SERVER_RELIABLE_MESSAGE_GAME ); outMsg.WriteData( msg.GetData(), msg.GetSize() ); for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) { if ( i == clientNum ) { continue; } if ( clients[i].clientState != SCS_INGAME ) { continue; } SendReliableMessage( i, outMsg ); } } /* ================== idAsyncServer::LocalClientSendReliableMessage ================== */ void idAsyncServer::LocalClientSendReliableMessage( const idBitMsg &msg ) { if ( localClientNum < 0 ) { common->Printf( "LocalClientSendReliableMessage: no local client\n" ); return; } game->ServerProcessReliableMessage( localClientNum, msg ); } /* ================== idAsyncServer::ProcessConnectionLessMessages ================== */ void idAsyncServer::ProcessConnectionLessMessages( void ) { int size, id; idBitMsg msg; byte msgBuf[MAX_MESSAGE_SIZE]; netadr_t from; if ( !serverPort.GetPort() ) { return; } while( serverPort.GetPacket( from, msgBuf, size, sizeof( msgBuf ) ) ) { msg.Init( msgBuf, sizeof( msgBuf ) ); msg.SetSize( size ); msg.BeginReading(); id = msg.ReadShort(); if ( id == CONNECTIONLESS_MESSAGE_ID ) { ConnectionlessMessage( from, msg ); } } } /* ================== idAsyncServer::UpdateTime ================== */ int idAsyncServer::UpdateTime( int clamp ) { int time, msec; time = Sys_Milliseconds(); msec = idMath::ClampInt( 0, clamp, time - realTime ); realTime = time; serverTime += msec; return msec; } /* ================== idAsyncServer::RunFrame ================== */ void idAsyncServer::RunFrame( void ) { int i, msec, size; bool newPacket; idBitMsg msg; byte msgBuf[MAX_MESSAGE_SIZE]; netadr_t from; int outgoingRate, incomingRate; float outgoingCompression, incomingCompression; msec = UpdateTime( 100 ); if ( !serverPort.GetPort() ) { return; } if ( !active ) { ProcessConnectionLessMessages(); return; } gameTimeResidual += msec; // spin in place processing incoming packets until enough time lapsed to run a new game frame do { do { // blocking read with game time residual timeout newPacket = serverPort.GetPacketBlocking( from, msgBuf, size, sizeof( msgBuf ), USERCMD_MSEC - gameTimeResidual - 1 ); if ( newPacket ) { msg.Init( msgBuf, sizeof( msgBuf ) ); msg.SetSize( size ); msg.BeginReading(); if ( ProcessMessage( from, msg ) ) { return; // return because rcon was used } } msec = UpdateTime( 100 ); gameTimeResidual += msec; } while( newPacket ); } while( gameTimeResidual < USERCMD_MSEC ); // send heart beat to master servers MasterHeartbeat(); // check for clients that timed out CheckClientTimeouts(); if ( idAsyncNetwork::idleServer.GetBool() == ( !GetNumClients() || GetNumIdleClients() != GetNumClients() ) ) { idAsyncNetwork::idleServer.SetBool( !idAsyncNetwork::idleServer.GetBool() ); // the need to propagate right away, only this sessLocal.mapSpawnData.serverInfo.Set( "si_idleServer", idAsyncNetwork::idleServer.GetString() ); game->SetServerInfo( sessLocal.mapSpawnData.serverInfo ); } // make sure the time doesn't wrap if ( serverTime > 0x70000000 ) { ExecuteMapChange(); return; } // check for synchronized cvar changes if ( cvarSystem->GetModifiedFlags() & CVAR_NETWORKSYNC ) { idDict newCvars; newCvars = *cvarSystem->MoveCVarsToDict( CVAR_NETWORKSYNC ); SendSyncedCvarsBroadcast( newCvars ); cvarSystem->ClearModifiedFlags( CVAR_NETWORKSYNC ); } // check for user info changes of the local client if ( cvarSystem->GetModifiedFlags() & CVAR_USERINFO ) { if ( localClientNum >= 0 ) { idDict newInfo; game->ThrottleUserInfo( ); newInfo = *cvarSystem->MoveCVarsToDict( CVAR_USERINFO ); SendUserInfoBroadcast( localClientNum, newInfo ); } cvarSystem->ClearModifiedFlags( CVAR_USERINFO ); } // advance the server game while( gameTimeResidual >= USERCMD_MSEC ) { // sample input for the local client LocalClientInput(); // duplicate usercmds for clients if no new ones are available DuplicateUsercmds( gameFrame, gameTime ); // advance game gameReturn_t ret = game->RunFrame( userCmds[gameFrame & ( MAX_USERCMD_BACKUP - 1 ) ] ); idAsyncNetwork::ExecuteSessionCommand( ret.sessionCommand ); // update time gameFrame++; gameTime += USERCMD_MSEC; gameTimeResidual -= USERCMD_MSEC; } // duplicate usercmds so there is always at least one available to send with snapshots DuplicateUsercmds( gameFrame, gameTime ); // send snapshots to connected clients for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) { serverClient_t &client = clients[i]; if ( client.clientState == SCS_FREE || i == localClientNum ) { continue; } // modify maximum rate if necesary if ( idAsyncNetwork::serverMaxClientRate.IsModified() ) { client.channel.SetMaxOutgoingRate( Min( client.clientRate, idAsyncNetwork::serverMaxClientRate.GetInteger() ) ); } // if the channel is not yet ready to send new data if ( !client.channel.ReadyToSend( serverTime ) ) { continue; } // send additional message fragments if the last message was too large to send at once if ( client.channel.UnsentFragmentsLeft() ) { client.channel.SendNextFragment( serverPort, serverTime ); continue; } if ( client.clientState == SCS_INGAME ) { if ( !SendSnapshotToClient( i ) ) { SendPingToClient( i ); } } else { SendEmptyToClient( i ); } } if ( com_showAsyncStats.GetBool() ) { UpdateAsyncStatsAvg(); // dedicated will verbose to console if ( idAsyncNetwork::serverDedicated.GetBool() && serverTime >= nextAsyncStatsTime ) { common->Printf( "delay = %d msec, total outgoing rate = %d KB/s, total incoming rate = %d KB/s\n", GetDelay(), GetOutgoingRate() >> 10, GetIncomingRate() >> 10 ); for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) { outgoingRate = GetClientOutgoingRate( i ); incomingRate = GetClientIncomingRate( i ); outgoingCompression = GetClientOutgoingCompression( i ); incomingCompression = GetClientIncomingCompression( i ); if ( outgoingRate != -1 && incomingRate != -1 ) { common->Printf( "client %d: out rate = %d B/s (% -2.1f%%), in rate = %d B/s (% -2.1f%%)\n", i, outgoingRate, outgoingCompression, incomingRate, incomingCompression ); } } idStr msg; GetAsyncStatsAvgMsg( msg ); common->Printf( va( "%s\n", msg.c_str() ) ); nextAsyncStatsTime = serverTime + 1000; } } idAsyncNetwork::serverMaxClientRate.ClearModified(); } /* ================== idAsyncServer::PacifierUpdate ================== */ void idAsyncServer::PacifierUpdate( void ) { int i; if ( !IsActive() ) { return; } realTime = Sys_Milliseconds(); ProcessConnectionLessMessages(); for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) { if ( clients[i].clientState >= SCS_PUREWAIT ) { if ( clients[i].channel.UnsentFragmentsLeft() ) { clients[i].channel.SendNextFragment( serverPort, serverTime ); } else { SendEmptyToClient( i ); } } } } /* ================== idAsyncServer::PrintOOB ================== */ void idAsyncServer::PrintOOB( const netadr_t to, int opcode, const char *string ) { idBitMsg outMsg; byte msgBuf[ MAX_MESSAGE_SIZE ]; outMsg.Init( msgBuf, sizeof( msgBuf ) ); outMsg.WriteShort( CONNECTIONLESS_MESSAGE_ID ); outMsg.WriteString( "print" ); outMsg.WriteInt( opcode ); outMsg.WriteString( string ); serverPort.SendPacket( to, outMsg.GetData(), outMsg.GetSize() ); } /* ================== idAsyncServer::MasterHeartbeat ================== */ void idAsyncServer::MasterHeartbeat( bool force ) { if ( idAsyncNetwork::LANServer.GetBool() ) { if ( force ) { common->Printf( "net_LANServer is enabled. Not sending heartbeats\n" ); } return; } if ( force ) { nextHeartbeatTime = 0; } // not yet if ( serverTime < nextHeartbeatTime ) { return; } nextHeartbeatTime = serverTime + HEARTBEAT_MSEC; for ( int i = 0 ; i < MAX_MASTER_SERVERS ; i++ ) { netadr_t adr; if ( idAsyncNetwork::GetMasterAddress( i, adr ) ) { common->Printf( "Sending heartbeat to %s\n", Sys_NetAdrToString( adr ) ); idBitMsg outMsg; byte msgBuf[ MAX_MESSAGE_SIZE ]; outMsg.Init( msgBuf, sizeof( msgBuf ) ); outMsg.WriteShort( CONNECTIONLESS_MESSAGE_ID ); outMsg.WriteString( "heartbeat" ); serverPort.SendPacket( adr, outMsg.GetData(), outMsg.GetSize() ); } } } /* =============== idAsyncServer::SendEnterGameToClient =============== */ void idAsyncServer::SendEnterGameToClient( int clientNum ) { idBitMsg msg; byte msgBuf[ MAX_MESSAGE_SIZE ]; msg.Init( msgBuf, sizeof( msgBuf ) ); msg.WriteByte( SERVER_RELIABLE_MESSAGE_ENTERGAME ); SendReliableMessage( clientNum, msg ); } /* =============== idAsyncServer::UpdateAsyncStatsAvg =============== */ void idAsyncServer::UpdateAsyncStatsAvg( void ) { stats_average_sum -= stats_outrate[ stats_current ]; stats_outrate[ stats_current ] = idAsyncNetwork::server.GetOutgoingRate(); if ( stats_outrate[ stats_current ] > stats_max ) { stats_max = stats_outrate[ stats_current ]; stats_max_index = stats_current; } else if ( stats_current == stats_max_index ) { // find the new max int i; stats_max = 0; for ( i = 0; i < stats_numsamples ; i++ ) { if ( stats_outrate[ i ] > stats_max ) { stats_max = stats_outrate[ i ]; stats_max_index = i; } } } stats_average_sum += stats_outrate[ stats_current ]; stats_current++; stats_current %= stats_numsamples; } /* =============== idAsyncServer::GetAsyncStatsAvgMsg =============== */ void idAsyncServer::GetAsyncStatsAvgMsg( idStr &msg ) { sprintf( msg, "avrg out: %d B/s - max %d B/s ( over %d ms )", stats_average_sum / stats_numsamples, stats_max, idAsyncNetwork::serverSnapshotDelay.GetInteger() * stats_numsamples ); } /* =============== idAsyncServer::ProcessDownloadRequestMessage =============== */ void idAsyncServer::ProcessDownloadRequestMessage( const netadr_t from, const idBitMsg &msg ) { int challenge, clientId, iclient, numPaks, i; int dlPakChecksum; int dlSize[ MAX_PURE_PAKS ]; // sizes idStrList pakNames; // relative path idStrList pakURLs; // game URLs char pakbuf[ MAX_STRING_CHARS ]; idStr paklist; byte msgBuf[ MAX_MESSAGE_SIZE ]; byte tmpBuf[ MAX_MESSAGE_SIZE ]; idBitMsg outMsg, tmpMsg; int dlRequest; int voidSlots = 0; // to count and verbose the right number of paks requested for downloads challenge = msg.ReadInt(); clientId = msg.ReadShort(); dlRequest = msg.ReadInt(); if ( ( iclient = ValidateChallenge( from, challenge, clientId ) ) == -1 ) { return; } if ( challenges[ iclient ].authState != CDK_PUREWAIT ) { common->DPrintf( "client %s: got download request message, not in CDK_PUREWAIT\n", Sys_NetAdrToString( from ) ); return; } pakNames.Append( pakbuf ); numPaks = 1; // read the checksums, build path names and pass that to the game code dlPakChecksum = msg.ReadInt(); while ( dlPakChecksum ) { if ( !( dlSize[ numPaks ] = fileSystem->ValidateDownloadPakForChecksum( dlPakChecksum, pakbuf ) ) ) { // we pass an empty token to the game so our list doesn't get offset common->Warning( "client requested an unknown pak 0x%x", dlPakChecksum ); pakbuf[ 0 ] = '\0'; voidSlots++; } pakNames.Append( pakbuf ); numPaks++; dlPakChecksum = msg.ReadInt(); } for ( i = 0; i < pakNames.Num(); i++ ) { if ( i > 0 ) { paklist += ";"; } paklist += pakNames[ i ].c_str(); } // read the message and pass it to the game code common->DPrintf( "got download request for %d paks - %s\n", numPaks - voidSlots, paklist.c_str() ); outMsg.Init( msgBuf, sizeof( msgBuf ) ); outMsg.WriteShort( CONNECTIONLESS_MESSAGE_ID ); outMsg.WriteString( "downloadInfo" ); outMsg.WriteInt( dlRequest ); if ( !game->DownloadRequest( Sys_NetAdrToString( from ), challenges[ iclient ].guid, paklist.c_str(), pakbuf ) ) { common->DPrintf( "game: no downloads\n" ); outMsg.WriteByte( SERVER_DL_NONE ); serverPort.SendPacket( from, outMsg.GetData(), outMsg.GetSize() ); return; } char *token, *next; int type = 0; token = pakbuf; next = strchr( token, ';' ); while ( token ) { if ( next ) { *next = '\0'; } if ( type == 0 ) { type = atoi( token ); } else if ( type == SERVER_DL_REDIRECT ) { common->DPrintf( "download request: redirect to URL %s\n", token ); outMsg.WriteByte( SERVER_DL_REDIRECT ); outMsg.WriteString( token ); serverPort.SendPacket( from, outMsg.GetData(), outMsg.GetSize() ); return; } else if ( type == SERVER_DL_LIST ) { pakURLs.Append( token ); } else { common->DPrintf( "wrong op type %d\n", type ); next = token = NULL; } if ( next ) { token = next + 1; next = strchr( token, ';' ); } else { token = NULL; } } if ( type == SERVER_DL_LIST ) { int totalDlSize = 0; int numActualPaks = 0; // put the answer packet together outMsg.WriteByte( SERVER_DL_LIST ); tmpMsg.Init( tmpBuf, MAX_MESSAGE_SIZE ); for ( i = 0; i < pakURLs.Num(); i++ ) { tmpMsg.BeginWriting(); if ( !dlSize[ i ] || !pakURLs[ i ].Length() ) { // still send the relative path so the client knows what it missed tmpMsg.WriteByte( SERVER_PAK_NO ); tmpMsg.WriteString( pakNames[ i ] ); } else { totalDlSize += dlSize[ i ]; numActualPaks++; tmpMsg.WriteByte( SERVER_PAK_YES ); tmpMsg.WriteString( pakNames[ i ] ); tmpMsg.WriteString( pakURLs[ i ] ); tmpMsg.WriteInt( dlSize[ i ] ); } // keep last 5 bytes for an 'end of message' - SERVER_PAK_END and the totalDlSize long if ( outMsg.GetRemainingSpace() - tmpMsg.GetSize() > 5 ) { outMsg.WriteData( tmpMsg.GetData(), tmpMsg.GetSize() ); } else { outMsg.WriteByte( SERVER_PAK_END ); break; } } if ( i == pakURLs.Num() ) { // put a closure even if size not exceeded outMsg.WriteByte( SERVER_PAK_END ); } common->DPrintf( "download request: download %d paks, %d bytes\n", numActualPaks, totalDlSize ); serverPort.SendPacket( from, outMsg.GetData(), outMsg.GetSize() ); } }