/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. 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 0 A.D. If not, see . */ #include "precompiled.h" #include "NetMessage.h" #include "NetServerTurnManager.h" #include "NetServer.h" #include "NetSession.h" #include "lib/utf8.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "simulation2/system/TurnManager.h" #if 0 #include "ps/Util.h" #define NETSERVERTURN_LOG(...) debug_printf(__VA_ARGS__) #else #define NETSERVERTURN_LOG(...) #endif CNetServerTurnManager::CNetServerTurnManager(CNetServerWorker& server) : m_NetServer(server), m_ReadyTurn(COMMAND_DELAY_MP - 1), m_TurnLength(DEFAULT_TURN_LENGTH) { // Turn 0 is not actually executed, store a dummy value. m_SavedTurnLengths.push_back(0); // Turns [1..COMMAND_DELAY - 1] are special: all clients run them without waiting on a server command batch. // Because of this, they are always run with the default MP turn length. for (u32 i = 1; i < COMMAND_DELAY_MP; ++i) m_SavedTurnLengths.push_back(m_TurnLength); } void CNetServerTurnManager::NotifyFinishedClientCommands(CNetServerSession& session, u32 turn) { int client = session.GetHostID(); NETSERVERTURN_LOG("NotifyFinishedClientCommands(client=%d, turn=%d)\n", client, turn); // Must be a client we've already heard of ENSURE(m_ClientsData.find(client) != m_ClientsData.end()); // Clients must advance one turn at a time if (turn != m_ClientsData[client].readyTurn + 1) { LOGERROR("NotifyFinishedClientCommands: Client %d (%s) is ready for turn %d, but expected %d", client, utf8_from_wstring(session.GetUserName()).c_str(), turn, m_ClientsData[client].readyTurn + 1); session.Disconnect(NDR_INCORRECT_READY_TURN_COMMANDS); } m_ClientsData[client].readyTurn = turn; // Check whether this was the final client to become ready CheckClientsReady(); } void CNetServerTurnManager::CheckClientsReady() { int max_observer_lag = -1; CFG_GET_VAL("network.observermaxlag", max_observer_lag); // Clamp to 0-10000 turns, below/above that is no limit. max_observer_lag = max_observer_lag < 0 ? -1 : max_observer_lag > 10000 ? -1 : max_observer_lag; // See if all clients (including self) are ready for a new turn for (const std::pair& clientData : m_ClientsData) { // Observers are allowed to lag more than regular clients. if (clientData.second.isObserver && (max_observer_lag == -1 || clientData.second.readyTurn > m_ReadyTurn - max_observer_lag)) continue; NETSERVERTURN_LOG(" %d: %d <=? %d\n", clientReady.first, clientReady.second, m_ReadyTurn); if (clientData.second.readyTurn <= m_ReadyTurn) return; // wasn't ready for m_ReadyTurn+1 } ++m_ReadyTurn; NETSERVERTURN_LOG("CheckClientsReady: ready for turn %d\n", m_ReadyTurn); // Tell all clients that the next turn is ready CEndCommandBatchMessage msg; msg.m_TurnLength = m_TurnLength; msg.m_Turn = m_ReadyTurn; m_NetServer.Broadcast(&msg, { NSS_INGAME }); ENSURE(m_SavedTurnLengths.size() == m_ReadyTurn); m_SavedTurnLengths.push_back(m_TurnLength); } void CNetServerTurnManager::NotifyFinishedClientUpdate(CNetServerSession& session, u32 turn, const CStr& hash) { int client = session.GetHostID(); const CStrW& playername = session.GetUserName(); // Clients must advance one turn at a time if (turn != m_ClientsData[client].simulatedTurn + 1) { LOGERROR("NotifyFinishedClientUpdate: Client %d (%s) is ready for turn %d, but expected %d", client, utf8_from_wstring(playername).c_str(), turn, m_ClientsData[client].simulatedTurn + 1); session.Disconnect(NDR_INCORRECT_READY_TURN_SIMULATED); } m_ClientsData[client].simulatedTurn = turn; // Check for OOS only if in sync if (m_HasSyncError) return; m_ClientsData[client].playerName = playername; m_ClientStateHashes[turn][client] = hash; // Find the newest turn which we know all clients have simulated u32 newest = std::numeric_limits::max(); for (const std::pair& clientData : m_ClientsData) if (clientData.second.simulatedTurn < newest) newest = clientData.second.simulatedTurn; // For every set of state hashes that all clients have simulated, check for OOS for (const std::pair>& clientStateHash : m_ClientStateHashes) { if (clientStateHash.first > newest) break; // Assume the host is correct (maybe we should choose the most common instead to help debugging) std::string expected = clientStateHash.second.begin()->second; // Find all players that are OOS on that turn std::vector OOSPlayerNames; for (const std::pair& hashPair : clientStateHash.second) { NETSERVERTURN_LOG("sync check %d: %d = %hs\n", clientStateHash.first, hashPair.first, Hexify(hashPair.second).c_str()); if (hashPair.second != expected) { // Oh no, out of sync m_HasSyncError = true; m_ClientsData[hashPair.first].isOOS = true; OOSPlayerNames.push_back(m_ClientsData[hashPair.first].playerName); } } // Tell everyone about it if (m_HasSyncError) { CSyncErrorMessage msg; msg.m_Turn = clientStateHash.first; msg.m_HashExpected = expected; for (const CStrW& oosPlayername : OOSPlayerNames) { CSyncErrorMessage::S_m_PlayerNames h; h.m_Name = oosPlayername; msg.m_PlayerNames.push_back(h); } m_NetServer.Broadcast(&msg, { NSS_INGAME }); break; } } // Delete the saved hashes for all turns that we've already verified m_ClientStateHashes.erase(m_ClientStateHashes.begin(), m_ClientStateHashes.lower_bound(newest+1)); } void CNetServerTurnManager::InitialiseClient(int client, u32 turn, bool observer) { NETSERVERTURN_LOG("InitialiseClient(client=%d, turn=%d)\n", client, turn); ENSURE(m_ClientsData.find(client) == m_ClientsData.end()); Client& data = m_ClientsData[client]; data.readyTurn = turn + COMMAND_DELAY_MP - 1; data.simulatedTurn = turn; data.isObserver = observer; } void CNetServerTurnManager::UninitialiseClient(int client) { NETSERVERTURN_LOG("UninitialiseClient(client=%d)\n", client); ENSURE(m_ClientsData.find(client) != m_ClientsData.end()); bool checkOOS = m_ClientsData[client].isOOS; m_ClientsData.erase(client); // Check whether we're ready for the next turn now that we're not // waiting for this client any more CheckClientsReady(); // Check whether we're still OOS. if (checkOOS) { for (const std::pair& clientData : m_ClientsData) if (clientData.second.isOOS) return; m_HasSyncError = false; } } void CNetServerTurnManager::SetTurnLength(u32 msecs) { m_TurnLength = msecs; } u32 CNetServerTurnManager::GetSavedTurnLength(u32 turn) { ENSURE(turn <= m_ReadyTurn); return m_SavedTurnLengths.at(turn); }