Index: source/network/NetServer.cpp =================================================================== --- source/network/NetServer.cpp +++ source/network/NetServer.cpp @@ -747,7 +747,7 @@ RemovePlayer(session->GetGUID()); if (m_ServerTurnManager && session->GetCurrState() != NSS_JOIN_SYNCING) - m_ServerTurnManager->UninitialiseClient(session->GetHostID()); // TODO: only for non-observers + m_ServerTurnManager->UninitialiseClient(session->GetHostID()); // TODO: ought to switch the player controlled by that client // back to AI control, or something? @@ -1402,7 +1402,7 @@ } // Tell the turn manager to expect commands from this new client - server.m_ServerTurnManager->InitialiseClient(session->GetHostID(), readyTurn); + server.m_ServerTurnManager->InitialiseClient(session->GetHostID(), readyTurn, server.m_PlayerAssignments[session->GetGUID()].m_PlayerID == -1); // Tell the client that everything has finished loading and it should start now CLoadedGameMessage loaded; @@ -1530,7 +1530,7 @@ m_ServerTurnManager = new CNetServerTurnManager(*this); for (CNetServerSession* session : m_Sessions) - m_ServerTurnManager->InitialiseClient(session->GetHostID(), 0); // TODO: only for non-observers + m_ServerTurnManager->InitialiseClient(session->GetHostID(), 0, m_PlayerAssignments[session->GetGUID()].m_PlayerID == -1); m_State = SERVER_STATE_LOADING; Index: source/network/NetServerTurnManager.h =================================================================== --- source/network/NetServerTurnManager.h +++ source/network/NetServerTurnManager.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2018 Wildfire Games. +/* 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 @@ -18,16 +18,18 @@ #ifndef INCLUDED_NETSERVERTURNMANAGER #define INCLUDED_NETSERVERTURNMANAGER -#include #include "ps/CStr.h" +#include +#include + class CNetServerWorker; class CNetServerSession; /** * The server-side counterpart to CNetClientTurnManager. * Records the turn state of each client, and sends turn advancement messages - * when all clients are ready. + * when all non-observer clients are ready. * * Thread-safety: * - This is constructed and used by CNetServerWorker in the network server thread. @@ -43,13 +45,13 @@ void NotifyFinishedClientUpdate(CNetServerSession& session, u32 turn, const CStr& hash); /** - * Inform the turn manager of a new client who will be sending commands. + * Inform the turn manager of a new client + * @param observer - whether this client is an observer (will be sending commands, must account for lag). */ - void InitialiseClient(int client, u32 turn); + void InitialiseClient(int client, u32 turn, bool observer); /** - * Inform the turn manager that a previously-initialised client has left the game - * and will no longer be sending commands. + * Inform the turn manager that a previously-initialised client has left the game. */ void UninitialiseClient(int client); @@ -73,6 +75,9 @@ /// The latest turn for which we have received all commands from all clients u32 m_ReadyTurn; + // Client ID -> whether they are only an observer. + std::unordered_map m_ClientsObserver; + // Client ID -> ready turn number (the latest turn for which all commands have been received from that client) std::map m_ClientsReady; Index: source/network/NetServerTurnManager.cpp =================================================================== --- source/network/NetServerTurnManager.cpp +++ source/network/NetServerTurnManager.cpp @@ -76,6 +76,9 @@ // See if all clients (including self) are ready for a new turn for (const std::pair& clientReady : m_ClientsReady) { + // Ignore observers. + if (m_ClientsObserver[clientReady.first]) + continue; NETSERVERTURN_LOG(" %d: %d <=? %d\n", clientReady.first, clientReady.second, m_ReadyTurn); if (clientReady.second <= m_ReadyTurn) return; // wasn't ready for m_ReadyTurn+1 @@ -145,7 +148,8 @@ if (hashPair.second != expected) { // Oh no, out of sync - m_HasSyncError = true; + if (!m_ClientsObserver[clientStateHash.first]) + m_HasSyncError = true; OOSPlayerNames.push_back(m_ClientPlayernames[hashPair.first]); } } @@ -171,13 +175,14 @@ m_ClientStateHashes.erase(m_ClientStateHashes.begin(), m_ClientStateHashes.lower_bound(newest+1)); } -void CNetServerTurnManager::InitialiseClient(int client, u32 turn) +void CNetServerTurnManager::InitialiseClient(int client, u32 turn, bool observer) { NETSERVERTURN_LOG("InitialiseClient(client=%d, turn=%d)\n", client, turn); ENSURE(m_ClientsReady.find(client) == m_ClientsReady.end()); m_ClientsReady[client] = turn + COMMAND_DELAY_MP - 1; m_ClientsSimulated[client] = turn; + m_ClientsObserver[client] = observer; } void CNetServerTurnManager::UninitialiseClient(int client) @@ -187,6 +192,7 @@ ENSURE(m_ClientsReady.find(client) != m_ClientsReady.end()); m_ClientsReady.erase(client); m_ClientsSimulated.erase(client); + m_ClientsObserver.erase(client); // Check whether we're ready for the next turn now that we're not // waiting for this client any more