Index: ps/trunk/source/network/FSM.cpp =================================================================== --- ps/trunk/source/network/FSM.cpp (revision 27782) +++ ps/trunk/source/network/FSM.cpp (revision 27783) @@ -1,254 +1,253 @@ /* Copyright (C) 2023 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 "FSM.h" CFsmEvent::CFsmEvent(unsigned int type) { m_Type = type; m_Param = nullptr; } CFsmEvent::~CFsmEvent() { m_Param = nullptr; } void CFsmEvent::SetParamRef(void* pParam) { m_Param = pParam; } CFsmTransition::CFsmTransition(const unsigned int state, const CallbackFunction action) : m_CurrState{state}, m_Action{action} {} void CFsmTransition::SetEvent(CFsmEvent* pEvent) { m_Event = pEvent; } void CFsmTransition::SetNextState(unsigned int nextState) { m_NextState = nextState; } bool CFsmTransition::RunAction() const { - return !m_Action.pFunction || - reinterpret_cast(m_Action.pFunction)(m_Action.pContext, m_Event); + return !m_Action.pFunction || m_Action.pFunction(m_Action.pContext, m_Event); } CFsm::CFsm() { m_Done = false; m_FirstState = FSM_INVALID_STATE; m_CurrState = FSM_INVALID_STATE; m_NextState = FSM_INVALID_STATE; } CFsm::~CFsm() { Shutdown(); } void CFsm::Setup() { // Does nothing by default } void CFsm::Shutdown() { // Release transitions TransitionList::iterator itTransition = m_Transitions.begin(); for (; itTransition < m_Transitions.end(); ++itTransition) delete *itTransition; // Release events EventMap::iterator itEvent = m_Events.begin(); for (; itEvent != m_Events.end(); ++itEvent) delete itEvent->second; m_States.clear(); m_Events.clear(); m_Transitions.clear(); m_Done = false; m_FirstState = FSM_INVALID_STATE; m_CurrState = FSM_INVALID_STATE; m_NextState = FSM_INVALID_STATE; } void CFsm::AddState(unsigned int state) { m_States.insert(state); } CFsmEvent* CFsm::AddEvent(unsigned int eventType) { CFsmEvent* pEvent = nullptr; // Lookup event by type EventMap::iterator it = m_Events.find(eventType); if (it != m_Events.end()) { pEvent = it->second; } else { pEvent = new CFsmEvent(eventType); // Store new event into internal map m_Events[eventType] = pEvent; } return pEvent; } CFsmTransition* CFsm::AddTransition(unsigned int state, unsigned int eventType, unsigned int nextState, - void* pAction /* = nullptr */, void* pContext /* = nullptr*/) + Action* pAction /* = nullptr */, void* pContext /* = nullptr*/) { // Make sure we store the current state AddState(state); // Make sure we store the next state AddState(nextState); // Make sure we store the event CFsmEvent* pEvent = AddEvent(eventType); if (!pEvent) return nullptr; // Create new transition CFsmTransition* pNewTransition = new CFsmTransition(state, {pAction, pContext}); // Setup new transition pNewTransition->SetEvent(pEvent); pNewTransition->SetNextState(nextState); // Store new transition m_Transitions.push_back(pNewTransition); return pNewTransition; } CFsmTransition* CFsm::GetTransition(unsigned int state, unsigned int eventType) const { if (!IsValidState(state)) return nullptr; if (!IsValidEvent(eventType)) return nullptr; TransitionList::const_iterator it = m_Transitions.begin(); for (; it != m_Transitions.end(); ++it) { CFsmTransition* pCurrTransition = *it; if (!pCurrTransition) continue; CFsmEvent* pCurrEvent = pCurrTransition->GetEvent(); if (!pCurrEvent) continue; // Is it our transition? if (pCurrTransition->GetCurrState() == state && pCurrEvent->GetType() == eventType) return pCurrTransition; } // No transition found return nullptr; } void CFsm::SetFirstState(unsigned int firstState) { m_FirstState = firstState; } void CFsm::SetCurrState(unsigned int state) { m_CurrState = state; } bool CFsm::IsFirstTime() const { return (m_CurrState == FSM_INVALID_STATE); } bool CFsm::Update(unsigned int eventType, void* pEventParam) { if (!IsValidEvent(eventType)) return false; if (IsFirstTime()) m_CurrState = m_FirstState; // Lookup transition CFsmTransition* pTransition = GetTransition(m_CurrState, eventType); if (!pTransition) return false; // Setup event parameter EventMap::iterator it = m_Events.find(eventType); if (it != m_Events.end()) { CFsmEvent* pEvent = it->second; if (pEvent) pEvent->SetParamRef(pEventParam); } // Save the default state transition (actions might call SetNextState // to override this) SetNextState(pTransition->GetNextState()); if (!pTransition->RunAction()) return false; SetCurrState(GetNextState()); // Reset the next state since it's no longer valid SetNextState(FSM_INVALID_STATE); return true; } bool CFsm::IsDone() const { // By default the internal flag m_Done is tested return m_Done; } bool CFsm::IsValidState(unsigned int state) const { StateSet::const_iterator it = m_States.find(state); if (it == m_States.end()) return false; return true; } bool CFsm::IsValidEvent(unsigned int eventType) const { EventMap::const_iterator it = m_Events.find(eventType); if (it == m_Events.end()) return false; return true; } Index: ps/trunk/source/network/FSM.h =================================================================== --- ps/trunk/source/network/FSM.h (revision 27782) +++ ps/trunk/source/network/FSM.h (revision 27783) @@ -1,262 +1,262 @@ /* Copyright (C) 2023 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 . */ #ifndef FSM_H #define FSM_H #include #include #include #include constexpr unsigned int FSM_INVALID_STATE{std::numeric_limits::max()}; class CFsmEvent; class CFsmTransition; class CFsm; -using Action = bool(void* pContext, const CFsmEvent* pEvent); +using Action = bool(void* pContext, CFsmEvent* pEvent); struct CallbackFunction { - void* pFunction{nullptr}; + Action* pFunction{nullptr}; void* pContext{nullptr}; }; using StateSet = std::set; using EventMap = std::map; using TransitionList = std::vector; /** * Represents a signal in the state machine that a change has occurred. * The CFsmEvent objects are under the control of CFsm so * they are created and deleted via CFsm. */ class CFsmEvent { NONCOPYABLE(CFsmEvent); public: CFsmEvent(unsigned int type); ~CFsmEvent(); unsigned int GetType() const { return m_Type; } void* GetParamRef() { return m_Param; } void SetParamRef(void* pParam); private: unsigned int m_Type; // Event type void* m_Param; // Event paramater }; /** * An association of event, action and next state. */ class CFsmTransition { NONCOPYABLE(CFsmTransition); public: /** * @param action Object executed upon transition. */ CFsmTransition(const unsigned int state, const CallbackFunction action); /** * Set event for which transition will occur. */ void SetEvent(CFsmEvent* pEvent); CFsmEvent* GetEvent() const { return m_Event; } /** * Set next state the transition will switch the system to. */ void SetNextState(unsigned int nextState); unsigned int GetNextState() const { return m_NextState; } unsigned int GetCurrState() const { return m_CurrState; } CallbackFunction GetAction() const { return m_Action; } /** * Executes action for the transition. * @note If there are no action, assume true. * @return whether the action returned true. */ bool RunAction() const; private: unsigned int m_CurrState; unsigned int m_NextState; CFsmEvent* m_Event; CallbackFunction m_Action; }; /** * Manages states, events, actions and transitions * between states. It provides an interface for advertising * events and track the current state. The implementation is * a Mealy state machine, so the system respond to events * and execute some action. * * A Mealy state machine has behaviour associated with state * transitions; Mealy machines are event driven where an * event triggers a state transition. */ class CFsm { NONCOPYABLE(CFsm); public: CFsm(); virtual ~CFsm(); /** * Constructs the state machine. This method must be overriden so that * connections are constructed for the particular state machine implemented. */ virtual void Setup(); /** * Clear event, transition lists and reset state machine. */ void Shutdown(); /** * Adds the specified state to the internal list of states. * @note If a state with the specified ID exists, the state is not added. */ void AddState(unsigned int state); /** * Adds the specified event to the internal list of events. * @note If an eveny with the specified ID exists, the event is not added. * @return a pointer to the new event. */ CFsmEvent* AddEvent(unsigned int eventType); /** * Adds a new transistion to the state machine. * @return a pointer to the new transition. */ CFsmTransition* AddTransition(unsigned int state, unsigned int eventType, unsigned int nextState, - void* pAction = nullptr, void* pContext = nullptr); + Action* pAction = nullptr, void* pContext = nullptr); /** * Looks up the transition given the state, event and next state to transition to. */ CFsmTransition* GetTransition(unsigned int state, unsigned int eventType) const; CFsmTransition* GetEventTransition (unsigned int eventType) const; /** * Sets the initial state for FSM. */ void SetFirstState(unsigned int firstState); /** * Sets the current state and update the last state to the current state. */ void SetCurrState(unsigned int state); unsigned int GetCurrState() const { return m_CurrState; } void SetNextState(unsigned int nextState) { m_NextState = nextState; } unsigned int GetNextState() const { return m_NextState; } const StateSet& GetStates() const { return m_States; } const EventMap& GetEvents() const { return m_Events; } const TransitionList& GetTransitions() const { return m_Transitions; } /** * Updates the FSM and retrieves next state. * @return whether the state was changed. */ bool Update(unsigned int eventType, void* pEventData); /** * Verifies whether the specified state is managed by the FSM. */ bool IsValidState(unsigned int state) const; /** * Verifies whether the specified event is managed by the FSM. */ bool IsValidEvent(unsigned int eventType) const; /** * Tests whether the state machine has finished its work. * @note This is state machine specific. */ virtual bool IsDone() const; private: /** * Verifies whether state machine has already been updated. */ bool IsFirstTime() const; bool m_Done; unsigned int m_FirstState; unsigned int m_CurrState; unsigned int m_NextState; StateSet m_States; EventMap m_Events; TransitionList m_Transitions; }; #endif // FSM_H Index: ps/trunk/source/network/NetClient.cpp =================================================================== --- ps/trunk/source/network/NetClient.cpp (revision 27782) +++ ps/trunk/source/network/NetClient.cpp (revision 27783) @@ -1,1012 +1,1012 @@ /* Copyright (C) 2023 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 "NetClient.h" #include "NetClientTurnManager.h" #include "NetEnet.h" #include "NetMessage.h" #include "NetSession.h" #include "lib/byte_order.h" #include "lib/external_libraries/enet.h" #include "lib/external_libraries/libsdl.h" #include "lib/sysdep/sysdep.h" #include "lobby/IXmppClient.h" #include "ps/CConsole.h" #include "ps/CLogger.h" #include "ps/Compress.h" #include "ps/CStr.h" #include "ps/Game.h" #include "ps/Hashing.h" #include "ps/Loader.h" #include "ps/Profile.h" #include "ps/Threading.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/JSON.h" #include "simulation2/Simulation2.h" #include "network/StunClient.h" /** * Once ping goes above turn length * command delay, * the game will start 'freezing' for other clients while we catch up. * Since commands are sent client -> server -> client, divide by 2. * (duplicated in NetServer.cpp to avoid having to fetch the constants in a header file) */ constexpr u32 NETWORK_BAD_PING = DEFAULT_TURN_LENGTH * COMMAND_DELAY_MP / 2; CNetClient *g_NetClient = NULL; /** * Async task for receiving the initial game state when rejoining an * in-progress network game. */ class CNetFileReceiveTask_ClientRejoin : public CNetFileReceiveTask { NONCOPYABLE(CNetFileReceiveTask_ClientRejoin); public: CNetFileReceiveTask_ClientRejoin(CNetClient& client, const CStr& initAttribs) : m_Client(client), m_InitAttributes(initAttribs) { } virtual void OnComplete() { // We've received the game state from the server // Save it so we can use it after the map has finished loading m_Client.m_JoinSyncBuffer = m_Buffer; // Pretend the server told us to start the game CGameStartMessage start; start.m_InitAttributes = m_InitAttributes; m_Client.HandleMessage(&start); } private: CNetClient& m_Client; CStr m_InitAttributes; }; CNetClient::CNetClient(CGame* game) : m_Session(NULL), m_UserName(L"anonymous"), m_HostID((u32)-1), m_ClientTurnManager(NULL), m_Game(game), m_LastConnectionCheck(0), m_ServerAddress(), m_ServerPort(0), m_Rejoin(false) { m_Game->SetTurnManager(NULL); // delete the old local turn manager so we don't accidentally use it void* context = this; JS_AddExtraGCRootsTracer(GetScriptInterface().GetGeneralJSContext(), CNetClient::Trace, this); // Set up transitions for session - AddTransition(NCS_UNCONNECTED, (uint)NMT_CONNECT_COMPLETE, NCS_CONNECT, (void*)&OnConnect, context); + AddTransition(NCS_UNCONNECTED, (uint)NMT_CONNECT_COMPLETE, NCS_CONNECT, &OnConnect, context); - AddTransition(NCS_CONNECT, (uint)NMT_SERVER_HANDSHAKE, NCS_HANDSHAKE, (void*)&OnHandshake, context); + AddTransition(NCS_CONNECT, (uint)NMT_SERVER_HANDSHAKE, NCS_HANDSHAKE, &OnHandshake, context); - AddTransition(NCS_HANDSHAKE, (uint)NMT_SERVER_HANDSHAKE_RESPONSE, NCS_AUTHENTICATE, (void*)&OnHandshakeResponse, context); + AddTransition(NCS_HANDSHAKE, (uint)NMT_SERVER_HANDSHAKE_RESPONSE, NCS_AUTHENTICATE, &OnHandshakeResponse, context); - AddTransition(NCS_AUTHENTICATE, (uint)NMT_AUTHENTICATE, NCS_AUTHENTICATE, (void*)&OnAuthenticateRequest, context); - AddTransition(NCS_AUTHENTICATE, (uint)NMT_AUTHENTICATE_RESULT, NCS_PREGAME, (void*)&OnAuthenticate, context); + AddTransition(NCS_AUTHENTICATE, (uint)NMT_AUTHENTICATE, NCS_AUTHENTICATE, &OnAuthenticateRequest, context); + AddTransition(NCS_AUTHENTICATE, (uint)NMT_AUTHENTICATE_RESULT, NCS_PREGAME, &OnAuthenticate, context); - AddTransition(NCS_PREGAME, (uint)NMT_CHAT, NCS_PREGAME, (void*)&OnChat, context); - AddTransition(NCS_PREGAME, (uint)NMT_READY, NCS_PREGAME, (void*)&OnReady, context); - AddTransition(NCS_PREGAME, (uint)NMT_GAME_SETUP, NCS_PREGAME, (void*)&OnGameSetup, context); - AddTransition(NCS_PREGAME, (uint)NMT_PLAYER_ASSIGNMENT, NCS_PREGAME, (void*)&OnPlayerAssignment, context); - AddTransition(NCS_PREGAME, (uint)NMT_KICKED, NCS_PREGAME, (void*)&OnKicked, context); - AddTransition(NCS_PREGAME, (uint)NMT_CLIENT_TIMEOUT, NCS_PREGAME, (void*)&OnClientTimeout, context); - AddTransition(NCS_PREGAME, (uint)NMT_CLIENT_PERFORMANCE, NCS_PREGAME, (void*)&OnClientPerformance, context); - AddTransition(NCS_PREGAME, (uint)NMT_GAME_START, NCS_LOADING, (void*)&OnGameStart, context); - AddTransition(NCS_PREGAME, (uint)NMT_JOIN_SYNC_START, NCS_JOIN_SYNCING, (void*)&OnJoinSyncStart, context); - - AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CHAT, NCS_JOIN_SYNCING, (void*)&OnChat, context); - AddTransition(NCS_JOIN_SYNCING, (uint)NMT_GAME_SETUP, NCS_JOIN_SYNCING, (void*)&OnGameSetup, context); - AddTransition(NCS_JOIN_SYNCING, (uint)NMT_PLAYER_ASSIGNMENT, NCS_JOIN_SYNCING, (void*)&OnPlayerAssignment, context); - AddTransition(NCS_JOIN_SYNCING, (uint)NMT_KICKED, NCS_JOIN_SYNCING, (void*)&OnKicked, context); - AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CLIENT_TIMEOUT, NCS_JOIN_SYNCING, (void*)&OnClientTimeout, context); - AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CLIENT_PERFORMANCE, NCS_JOIN_SYNCING, (void*)&OnClientPerformance, context); - AddTransition(NCS_JOIN_SYNCING, (uint)NMT_GAME_START, NCS_JOIN_SYNCING, (void*)&OnGameStart, context); - AddTransition(NCS_JOIN_SYNCING, (uint)NMT_SIMULATION_COMMAND, NCS_JOIN_SYNCING, (void*)&OnInGame, context); - AddTransition(NCS_JOIN_SYNCING, (uint)NMT_END_COMMAND_BATCH, NCS_JOIN_SYNCING, (void*)&OnJoinSyncEndCommandBatch, context); - AddTransition(NCS_JOIN_SYNCING, (uint)NMT_LOADED_GAME, NCS_INGAME, (void*)&OnLoadedGame, context); - - AddTransition(NCS_LOADING, (uint)NMT_CHAT, NCS_LOADING, (void*)&OnChat, context); - AddTransition(NCS_LOADING, (uint)NMT_GAME_SETUP, NCS_LOADING, (void*)&OnGameSetup, context); - AddTransition(NCS_LOADING, (uint)NMT_PLAYER_ASSIGNMENT, NCS_LOADING, (void*)&OnPlayerAssignment, context); - AddTransition(NCS_LOADING, (uint)NMT_KICKED, NCS_LOADING, (void*)&OnKicked, context); - AddTransition(NCS_LOADING, (uint)NMT_CLIENT_TIMEOUT, NCS_LOADING, (void*)&OnClientTimeout, context); - AddTransition(NCS_LOADING, (uint)NMT_CLIENT_PERFORMANCE, NCS_LOADING, (void*)&OnClientPerformance, context); - AddTransition(NCS_LOADING, (uint)NMT_CLIENTS_LOADING, NCS_LOADING, (void*)&OnClientsLoading, context); - AddTransition(NCS_LOADING, (uint)NMT_LOADED_GAME, NCS_INGAME, (void*)&OnLoadedGame, context); - - AddTransition(NCS_INGAME, (uint)NMT_REJOINED, NCS_INGAME, (void*)&OnRejoined, context); - AddTransition(NCS_INGAME, (uint)NMT_KICKED, NCS_INGAME, (void*)&OnKicked, context); - AddTransition(NCS_INGAME, (uint)NMT_CLIENT_TIMEOUT, NCS_INGAME, (void*)&OnClientTimeout, context); - AddTransition(NCS_INGAME, (uint)NMT_CLIENT_PERFORMANCE, NCS_INGAME, (void*)&OnClientPerformance, context); - AddTransition(NCS_INGAME, (uint)NMT_CLIENTS_LOADING, NCS_INGAME, (void*)&OnClientsLoading, context); - AddTransition(NCS_INGAME, (uint)NMT_CLIENT_PAUSED, NCS_INGAME, (void*)&OnClientPaused, context); - AddTransition(NCS_INGAME, (uint)NMT_CHAT, NCS_INGAME, (void*)&OnChat, context); - AddTransition(NCS_INGAME, (uint)NMT_GAME_SETUP, NCS_INGAME, (void*)&OnGameSetup, context); - AddTransition(NCS_INGAME, (uint)NMT_PLAYER_ASSIGNMENT, NCS_INGAME, (void*)&OnPlayerAssignment, context); - AddTransition(NCS_INGAME, (uint)NMT_SIMULATION_COMMAND, NCS_INGAME, (void*)&OnInGame, context); - AddTransition(NCS_INGAME, (uint)NMT_SYNC_ERROR, NCS_INGAME, (void*)&OnInGame, context); - AddTransition(NCS_INGAME, (uint)NMT_END_COMMAND_BATCH, NCS_INGAME, (void*)&OnInGame, context); + AddTransition(NCS_PREGAME, (uint)NMT_CHAT, NCS_PREGAME, &OnChat, context); + AddTransition(NCS_PREGAME, (uint)NMT_READY, NCS_PREGAME, &OnReady, context); + AddTransition(NCS_PREGAME, (uint)NMT_GAME_SETUP, NCS_PREGAME, &OnGameSetup, context); + AddTransition(NCS_PREGAME, (uint)NMT_PLAYER_ASSIGNMENT, NCS_PREGAME, &OnPlayerAssignment, context); + AddTransition(NCS_PREGAME, (uint)NMT_KICKED, NCS_PREGAME, &OnKicked, context); + AddTransition(NCS_PREGAME, (uint)NMT_CLIENT_TIMEOUT, NCS_PREGAME, &OnClientTimeout, context); + AddTransition(NCS_PREGAME, (uint)NMT_CLIENT_PERFORMANCE, NCS_PREGAME, &OnClientPerformance, context); + AddTransition(NCS_PREGAME, (uint)NMT_GAME_START, NCS_LOADING, &OnGameStart, context); + AddTransition(NCS_PREGAME, (uint)NMT_JOIN_SYNC_START, NCS_JOIN_SYNCING, &OnJoinSyncStart, context); + + AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CHAT, NCS_JOIN_SYNCING, &OnChat, context); + AddTransition(NCS_JOIN_SYNCING, (uint)NMT_GAME_SETUP, NCS_JOIN_SYNCING, &OnGameSetup, context); + AddTransition(NCS_JOIN_SYNCING, (uint)NMT_PLAYER_ASSIGNMENT, NCS_JOIN_SYNCING, &OnPlayerAssignment, context); + AddTransition(NCS_JOIN_SYNCING, (uint)NMT_KICKED, NCS_JOIN_SYNCING, &OnKicked, context); + AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CLIENT_TIMEOUT, NCS_JOIN_SYNCING, &OnClientTimeout, context); + AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CLIENT_PERFORMANCE, NCS_JOIN_SYNCING, &OnClientPerformance, context); + AddTransition(NCS_JOIN_SYNCING, (uint)NMT_GAME_START, NCS_JOIN_SYNCING, &OnGameStart, context); + AddTransition(NCS_JOIN_SYNCING, (uint)NMT_SIMULATION_COMMAND, NCS_JOIN_SYNCING, &OnInGame, context); + AddTransition(NCS_JOIN_SYNCING, (uint)NMT_END_COMMAND_BATCH, NCS_JOIN_SYNCING, &OnJoinSyncEndCommandBatch, context); + AddTransition(NCS_JOIN_SYNCING, (uint)NMT_LOADED_GAME, NCS_INGAME, &OnLoadedGame, context); + + AddTransition(NCS_LOADING, (uint)NMT_CHAT, NCS_LOADING, &OnChat, context); + AddTransition(NCS_LOADING, (uint)NMT_GAME_SETUP, NCS_LOADING, &OnGameSetup, context); + AddTransition(NCS_LOADING, (uint)NMT_PLAYER_ASSIGNMENT, NCS_LOADING, &OnPlayerAssignment, context); + AddTransition(NCS_LOADING, (uint)NMT_KICKED, NCS_LOADING, &OnKicked, context); + AddTransition(NCS_LOADING, (uint)NMT_CLIENT_TIMEOUT, NCS_LOADING, &OnClientTimeout, context); + AddTransition(NCS_LOADING, (uint)NMT_CLIENT_PERFORMANCE, NCS_LOADING, &OnClientPerformance, context); + AddTransition(NCS_LOADING, (uint)NMT_CLIENTS_LOADING, NCS_LOADING, &OnClientsLoading, context); + AddTransition(NCS_LOADING, (uint)NMT_LOADED_GAME, NCS_INGAME, &OnLoadedGame, context); + + AddTransition(NCS_INGAME, (uint)NMT_REJOINED, NCS_INGAME, &OnRejoined, context); + AddTransition(NCS_INGAME, (uint)NMT_KICKED, NCS_INGAME, &OnKicked, context); + AddTransition(NCS_INGAME, (uint)NMT_CLIENT_TIMEOUT, NCS_INGAME, &OnClientTimeout, context); + AddTransition(NCS_INGAME, (uint)NMT_CLIENT_PERFORMANCE, NCS_INGAME, &OnClientPerformance, context); + AddTransition(NCS_INGAME, (uint)NMT_CLIENTS_LOADING, NCS_INGAME, &OnClientsLoading, context); + AddTransition(NCS_INGAME, (uint)NMT_CLIENT_PAUSED, NCS_INGAME, &OnClientPaused, context); + AddTransition(NCS_INGAME, (uint)NMT_CHAT, NCS_INGAME, &OnChat, context); + AddTransition(NCS_INGAME, (uint)NMT_GAME_SETUP, NCS_INGAME, &OnGameSetup, context); + AddTransition(NCS_INGAME, (uint)NMT_PLAYER_ASSIGNMENT, NCS_INGAME, &OnPlayerAssignment, context); + AddTransition(NCS_INGAME, (uint)NMT_SIMULATION_COMMAND, NCS_INGAME, &OnInGame, context); + AddTransition(NCS_INGAME, (uint)NMT_SYNC_ERROR, NCS_INGAME, &OnInGame, context); + AddTransition(NCS_INGAME, (uint)NMT_END_COMMAND_BATCH, NCS_INGAME, &OnInGame, context); // Set first state SetFirstState(NCS_UNCONNECTED); } CNetClient::~CNetClient() { // Try to flush messages before dying (probably fails). if (m_ClientTurnManager) m_ClientTurnManager->OnDestroyConnection(); DestroyConnection(); JS_RemoveExtraGCRootsTracer(GetScriptInterface().GetGeneralJSContext(), CNetClient::Trace, this); } void CNetClient::TraceMember(JSTracer *trc) { for (JS::Heap& guiMessage : m_GuiMessageQueue) JS::TraceEdge(trc, &guiMessage, "m_GuiMessageQueue"); } void CNetClient::SetUserName(const CStrW& username) { ENSURE(!m_Session); // must be called before we start the connection m_UserName = username; } void CNetClient::SetHostJID(const CStr& jid) { m_HostJID = jid; } void CNetClient::SetGamePassword(const CStr& hashedPassword) { // Hash on top with the user's name, to make sure not all // hashing data is in control of the host. m_Password = HashCryptographically(hashedPassword, m_UserName.ToUTF8()); } void CNetClient::SetControllerSecret(const std::string& secret) { m_ControllerSecret = secret; } bool CNetClient::SetupConnection(ENetHost* enetClient) { CNetClientSession* session = new CNetClientSession(*this); bool ok = session->Connect(m_ServerAddress, m_ServerPort, enetClient); SetAndOwnSession(session); if (ok) m_PollingThread = std::thread(Threading::HandleExceptions::Wrapper, m_Session); return ok; } void CNetClient::SetupConnectionViaLobby() { g_XmppClient->SendIqGetConnectionData(m_HostJID, m_Password, m_UserName.ToUTF8(), false); } void CNetClient::SetupServerData(CStr address, u16 port, bool stun) { ENSURE(!m_Session); m_ServerAddress = address; m_ServerPort = port; m_UseSTUN = stun; } void CNetClient::HandleGetServerDataFailed(const CStr& error) { if (m_Session) return; PushGuiMessage( "type", "serverdata", "status", "failed", "reason", error ); } bool CNetClient::TryToConnect(const CStr& hostJID, bool localNetwork) { if (m_Session) return false; if (m_ServerAddress.empty()) { PushGuiMessage( "type", "netstatus", "status", "disconnected", "reason", static_cast(NDR_SERVER_REFUSED)); return false; } ENetAddress hostAddr{ ENET_HOST_ANY, ENET_PORT_ANY }; ENetHost* enetClient = PS::Enet::CreateHost(&hostAddr, 1, 1); if (!enetClient) { PushGuiMessage( "type", "netstatus", "status", "disconnected", "reason", static_cast(NDR_STUN_PORT_FAILED)); return false; } CStr ip; u16 port = 0; if (g_XmppClient && m_UseSTUN) { if (!StunClient::FindPublicIP(*enetClient, ip, port)) { PushGuiMessage( "type", "netstatus", "status", "disconnected", "reason", static_cast(NDR_STUN_ENDPOINT_FAILED)); return false; } // If the host is on the same network, we risk failing to connect // on routers that don't support NAT hairpinning/NAT loopback. // To work around that, send again a connection data request, but for internal IP this time. if (ip == m_ServerAddress) { g_XmppClient->SendIqGetConnectionData(m_HostJID, m_Password, m_UserName.ToUTF8(), true); // Return true anyways - we're on a success path here. return true; } } else if (g_XmppClient && localNetwork) { // We may need to punch a hole through the local firewall, so fetch our local IP. // NB: we'll ignore failures here, and hope that the firewall will be open to connection // if we fail to fetch the local IP (which is unlikely anyways). if (!StunClient::FindLocalIP(ip)) ip = ""; // Check if we're hosting on localhost, and if so, explicitly use that // (this circumvents, at least, the 'block all incoming connections' setting // on the MacOS firewall). if (ip == m_ServerAddress) { m_ServerAddress = "127.0.0.1"; ip = ""; } port = enetClient->address.port; } LOGMESSAGE("NetClient: connecting to server at %s:%i", m_ServerAddress, m_ServerPort); if (!ip.empty()) { // UDP hole-punching // Step 0: send a message, via XMPP, to the server with our external IP & port. g_XmppClient->SendStunEndpointToHost(ip, port, hostJID); // Step 1b: Wait some time - we need the host to receive the stun endpoint and start punching a hole themselves before // we try to establish the connection below. SDL_Delay(1000); // Step 2: Send a message ourselves to the server so that the NAT, if any, routes incoming trafic correctly. // TODO: verify if this step is necessary, since we'll try and connect anyways below. StunClient::SendHolePunchingMessages(*enetClient, m_ServerAddress, m_ServerPort); } if (!g_NetClient->SetupConnection(enetClient)) { PushGuiMessage( "type", "netstatus", "status", "disconnected", "reason", static_cast(NDR_UNKNOWN)); return false; } return true; } void CNetClient::SetAndOwnSession(CNetClientSession* session) { delete m_Session; m_Session = session; } void CNetClient::DestroyConnection() { if (m_Session) m_Session->Shutdown(); if (m_PollingThread.joinable()) // Use detach() over join() because we don't want to wait for the session // (which may be polling or trying to send messages). m_PollingThread.detach(); // The polling thread will cleanup the session on its own, // mark it as nullptr here so we know we're done using it. m_Session = nullptr; } void CNetClient::Poll() { if (!m_Session) return; PROFILE3("NetClient::poll"); CheckServerConnection(); m_Session->ProcessPolledMessages(); } void CNetClient::CheckServerConnection() { // Trigger local warnings if the connection to the server is bad. // At most once per second. std::time_t now = std::time(nullptr); if (now <= m_LastConnectionCheck) return; m_LastConnectionCheck = now; // Report if we are losing the connection to the server u32 lastReceived = m_Session->GetLastReceivedTime(); if (lastReceived > NETWORK_WARNING_TIMEOUT) { PushGuiMessage( "type", "netwarn", "warntype", "server-timeout", "lastReceivedTime", lastReceived); return; } // Report if we have a bad ping to the server. u32 meanRTT = m_Session->GetMeanRTT(); if (meanRTT > NETWORK_BAD_PING) { PushGuiMessage( "type", "netwarn", "warntype", "server-latency", "meanRTT", meanRTT); } } void CNetClient::GuiPoll(JS::MutableHandleValue ret) { if (m_GuiMessageQueue.empty()) { ret.setUndefined(); return; } ret.set(m_GuiMessageQueue.front()); m_GuiMessageQueue.pop_front(); } std::string CNetClient::TestReadGuiMessages() { ScriptRequest rq(GetScriptInterface()); std::string r; JS::RootedValue msg(rq.cx); while (true) { GuiPoll(&msg); if (msg.isUndefined()) break; r += Script::ToString(rq, &msg) + "\n"; } return r; } const ScriptInterface& CNetClient::GetScriptInterface() { return m_Game->GetSimulation2()->GetScriptInterface(); } void CNetClient::PostPlayerAssignmentsToScript() { ScriptRequest rq(GetScriptInterface()); JS::RootedValue newAssignments(rq.cx); Script::CreateObject(rq, &newAssignments); for (const std::pair& p : m_PlayerAssignments) { JS::RootedValue assignment(rq.cx); Script::CreateObject( rq, &assignment, "name", p.second.m_Name, "player", p.second.m_PlayerID, "status", p.second.m_Status); Script::SetProperty(rq, newAssignments, p.first.c_str(), assignment); } PushGuiMessage( "type", "players", "newAssignments", newAssignments); } bool CNetClient::SendMessage(const CNetMessage* message) { if (!m_Session) return false; return m_Session->SendMessage(message); } void CNetClient::HandleConnect() { Update((uint)NMT_CONNECT_COMPLETE, NULL); } void CNetClient::HandleDisconnect(u32 reason) { PushGuiMessage( "type", "netstatus", "status", "disconnected", "reason", reason); DestroyConnection(); // Update the state immediately to UNCONNECTED (don't bother with FSM transitions since // we'd need one for every single state, and we don't need to use per-state actions) SetCurrState(NCS_UNCONNECTED); } void CNetClient::SendGameSetupMessage(JS::MutableHandleValue attrs, const ScriptInterface& scriptInterface) { CGameSetupMessage gameSetup(scriptInterface); gameSetup.m_Data = attrs; SendMessage(&gameSetup); } void CNetClient::SendAssignPlayerMessage(const int playerID, const CStr& guid) { CAssignPlayerMessage assignPlayer; assignPlayer.m_PlayerID = playerID; assignPlayer.m_GUID = guid; SendMessage(&assignPlayer); } void CNetClient::SendChatMessage(const std::wstring& text) { CChatMessage chat; chat.m_Message = text; SendMessage(&chat); } void CNetClient::SendReadyMessage(const int status) { CReadyMessage readyStatus; readyStatus.m_Status = status; SendMessage(&readyStatus); } void CNetClient::SendClearAllReadyMessage() { CClearAllReadyMessage clearAllReady; SendMessage(&clearAllReady); } void CNetClient::SendStartGameMessage(const CStr& initAttribs) { CGameStartMessage gameStart; gameStart.m_InitAttributes = initAttribs; SendMessage(&gameStart); } void CNetClient::SendRejoinedMessage() { CRejoinedMessage rejoinedMessage; SendMessage(&rejoinedMessage); } void CNetClient::SendKickPlayerMessage(const CStrW& playerName, bool ban) { CKickedMessage kickPlayer; kickPlayer.m_Name = playerName; kickPlayer.m_Ban = ban; SendMessage(&kickPlayer); } void CNetClient::SendPausedMessage(bool pause) { CClientPausedMessage pausedMessage; pausedMessage.m_Pause = pause; SendMessage(&pausedMessage); } bool CNetClient::HandleMessage(CNetMessage* message) { // Handle non-FSM messages first Status status = m_Session->GetFileTransferer().HandleMessageReceive(*message); if (status == INFO::OK) return true; if (status != INFO::SKIPPED) return false; if (message->GetType() == NMT_FILE_TRANSFER_REQUEST) { CFileTransferRequestMessage* reqMessage = static_cast(message); // TODO: we should support different transfer request types, instead of assuming // it's always requesting the simulation state std::stringstream stream; LOGMESSAGERENDER("Serializing game at turn %u for rejoining player", m_ClientTurnManager->GetCurrentTurn()); u32 turn = to_le32(m_ClientTurnManager->GetCurrentTurn()); stream.write((char*)&turn, sizeof(turn)); bool ok = m_Game->GetSimulation2()->SerializeState(stream); ENSURE(ok); // Compress the content with zlib to save bandwidth // (TODO: if this is still too large, compressing with e.g. LZMA works much better) std::string compressed; CompressZLib(stream.str(), compressed, true); m_Session->GetFileTransferer().StartResponse(reqMessage->m_RequestID, compressed); return true; } // Update FSM bool ok = Update(message->GetType(), message); if (!ok) LOGERROR("Net client: Error running FSM update (type=%d state=%d)", (int)message->GetType(), (int)GetCurrState()); return ok; } void CNetClient::LoadFinished() { if (!m_JoinSyncBuffer.empty()) { // We're rejoining a game, and just finished loading the initial map, // so deserialize the saved game state now std::string state; DecompressZLib(m_JoinSyncBuffer, state, true); std::stringstream stream(state); u32 turn; stream.read((char*)&turn, sizeof(turn)); turn = to_le32(turn); LOGMESSAGE("Rejoining client deserializing state at turn %u\n", turn); bool ok = m_Game->GetSimulation2()->DeserializeState(stream); ENSURE(ok); m_ClientTurnManager->ResetState(turn, turn); PushGuiMessage( "type", "netstatus", "status", "join_syncing"); } else { // Connecting at the start of a game, so we'll wait for other players to finish loading PushGuiMessage( "type", "netstatus", "status", "waiting_for_players"); } CLoadedGameMessage loaded; loaded.m_CurrentTurn = m_ClientTurnManager->GetCurrentTurn(); SendMessage(&loaded); } void CNetClient::SendAuthenticateMessage() { CAuthenticateMessage authenticate; authenticate.m_Name = m_UserName; authenticate.m_Password = m_Password; authenticate.m_ControllerSecret = m_ControllerSecret; SendMessage(&authenticate); } bool CNetClient::OnConnect(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_CONNECT_COMPLETE); CNetClient* client = static_cast(context); client->PushGuiMessage( "type", "netstatus", "status", "connected"); return true; } bool CNetClient::OnHandshake(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_SERVER_HANDSHAKE); CNetClient* client = static_cast(context); CCliHandshakeMessage handshake; handshake.m_MagicResponse = PS_PROTOCOL_MAGIC_RESPONSE; handshake.m_ProtocolVersion = PS_PROTOCOL_VERSION; handshake.m_SoftwareVersion = PS_PROTOCOL_VERSION; client->SendMessage(&handshake); return true; } bool CNetClient::OnHandshakeResponse(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_SERVER_HANDSHAKE_RESPONSE); CNetClient* client = static_cast(context); CSrvHandshakeResponseMessage* message = static_cast(event->GetParamRef()); client->m_GUID = message->m_GUID; if (message->m_Flags & PS_NETWORK_FLAG_REQUIRE_LOBBYAUTH) { if (g_XmppClient && !client->m_HostJID.empty()) g_XmppClient->SendIqLobbyAuth(client->m_HostJID, client->m_GUID); else { client->PushGuiMessage( "type", "netstatus", "status", "disconnected", "reason", static_cast(NDR_LOBBY_AUTH_FAILED)); LOGMESSAGE("Net client: Couldn't send lobby auth xmpp message"); } return true; } client->SendAuthenticateMessage(); return true; } bool CNetClient::OnAuthenticateRequest(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_AUTHENTICATE); CNetClient* client = static_cast(context); client->SendAuthenticateMessage(); return true; } bool CNetClient::OnAuthenticate(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_AUTHENTICATE_RESULT); CNetClient* client = static_cast(context); CAuthenticateResultMessage* message = static_cast(event->GetParamRef()); LOGMESSAGE("Net: Authentication result: host=%u, %s", message->m_HostID, utf8_from_wstring(message->m_Message)); client->m_HostID = message->m_HostID; client->m_Rejoin = message->m_Code == ARC_OK_REJOINING; client->m_IsController = message->m_IsController; client->PushGuiMessage( "type", "netstatus", "status", "authenticated", "rejoining", client->m_Rejoin); return true; } bool CNetClient::OnChat(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_CHAT); CNetClient* client = static_cast(context); CChatMessage* message = static_cast(event->GetParamRef()); client->PushGuiMessage( "type", "chat", "guid", message->m_GUID, "text", message->m_Message); return true; } bool CNetClient::OnReady(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_READY); CNetClient* client = static_cast(context); CReadyMessage* message = static_cast(event->GetParamRef()); client->PushGuiMessage( "type", "ready", "guid", message->m_GUID, "status", message->m_Status); return true; } bool CNetClient::OnGameSetup(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_GAME_SETUP); CNetClient* client = static_cast(context); CGameSetupMessage* message = static_cast(event->GetParamRef()); client->PushGuiMessage( "type", "gamesetup", "data", message->m_Data); return true; } bool CNetClient::OnPlayerAssignment(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_PLAYER_ASSIGNMENT); CNetClient* client = static_cast(context); CPlayerAssignmentMessage* message = static_cast(event->GetParamRef()); // Unpack the message PlayerAssignmentMap newPlayerAssignments; for (size_t i = 0; i < message->m_Hosts.size(); ++i) { PlayerAssignment assignment; assignment.m_Enabled = true; assignment.m_Name = message->m_Hosts[i].m_Name; assignment.m_PlayerID = message->m_Hosts[i].m_PlayerID; assignment.m_Status = message->m_Hosts[i].m_Status; newPlayerAssignments[message->m_Hosts[i].m_GUID] = assignment; } client->m_PlayerAssignments.swap(newPlayerAssignments); client->PostPlayerAssignmentsToScript(); return true; } // This is called either when the host clicks the StartGame button or // if this client rejoins and finishes the download of the simstate. bool CNetClient::OnGameStart(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_GAME_START); CNetClient* client = static_cast(context); CGameStartMessage* message = static_cast(event->GetParamRef()); // Find the player assigned to our GUID int player = -1; if (client->m_PlayerAssignments.find(client->m_GUID) != client->m_PlayerAssignments.end()) player = client->m_PlayerAssignments[client->m_GUID].m_PlayerID; client->m_ClientTurnManager = new CNetClientTurnManager( *client->m_Game->GetSimulation2(), *client, client->m_HostID, client->m_Game->GetReplayLogger()); // Parse init attributes. const ScriptInterface& scriptInterface = client->m_Game->GetSimulation2()->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue initAttribs(rq.cx); Script::ParseJSON(rq, message->m_InitAttributes, &initAttribs); client->m_Game->SetPlayerID(player); client->m_Game->StartGame(&initAttribs, ""); client->PushGuiMessage("type", "start", "initAttributes", initAttribs); return true; } bool CNetClient::OnJoinSyncStart(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_JOIN_SYNC_START); CNetClient* client = static_cast(context); CJoinSyncStartMessage* joinSyncStartMessage = (CJoinSyncStartMessage*)event->GetParamRef(); // The server wants us to start downloading the game state from it, so do so client->m_Session->GetFileTransferer().StartTask( std::shared_ptr(new CNetFileReceiveTask_ClientRejoin(*client, joinSyncStartMessage->m_InitAttributes)) ); return true; } bool CNetClient::OnJoinSyncEndCommandBatch(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_END_COMMAND_BATCH); CNetClient* client = static_cast(context); CEndCommandBatchMessage* endMessage = (CEndCommandBatchMessage*)event->GetParamRef(); client->m_ClientTurnManager->FinishedAllCommands(endMessage->m_Turn, endMessage->m_TurnLength); // Execute all the received commands for the latest turn client->m_ClientTurnManager->UpdateFastForward(); return true; } bool CNetClient::OnRejoined(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_REJOINED); CNetClient* client = static_cast(context); CRejoinedMessage* message = static_cast(event->GetParamRef()); client->PushGuiMessage( "type", "rejoined", "guid", message->m_GUID); return true; } bool CNetClient::OnKicked(void *context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_KICKED); CNetClient* client = static_cast(context); CKickedMessage* message = static_cast(event->GetParamRef()); client->PushGuiMessage( "username", message->m_Name, "type", "kicked", "banned", message->m_Ban != 0); return true; } bool CNetClient::OnClientTimeout(void *context, CFsmEvent* event) { // Report the timeout of some other client ENSURE(event->GetType() == (uint)NMT_CLIENT_TIMEOUT); CNetClient* client = static_cast(context); CClientTimeoutMessage* message = static_cast(event->GetParamRef()); client->PushGuiMessage( "type", "netwarn", "warntype", "client-timeout", "guid", message->m_GUID, "lastReceivedTime", message->m_LastReceivedTime); return true; } bool CNetClient::OnClientPerformance(void *context, CFsmEvent* event) { // Performance statistics for one or multiple clients ENSURE(event->GetType() == (uint)NMT_CLIENT_PERFORMANCE); CNetClient* client = static_cast(context); CClientPerformanceMessage* message = static_cast(event->GetParamRef()); // Display warnings for other clients with bad ping for (size_t i = 0; i < message->m_Clients.size(); ++i) { if (message->m_Clients[i].m_MeanRTT < NETWORK_BAD_PING || message->m_Clients[i].m_GUID == client->m_GUID) continue; client->PushGuiMessage( "type", "netwarn", "warntype", "client-latency", "guid", message->m_Clients[i].m_GUID, "meanRTT", message->m_Clients[i].m_MeanRTT); } return true; } bool CNetClient::OnClientsLoading(void *context, CFsmEvent *event) { ENSURE(event->GetType() == (uint)NMT_CLIENTS_LOADING); CNetClient* client = static_cast(context); CClientsLoadingMessage* message = static_cast(event->GetParamRef()); std::vector guids; guids.reserve(message->m_Clients.size()); for (const CClientsLoadingMessage::S_m_Clients& mClient : message->m_Clients) guids.push_back(mClient.m_GUID); client->PushGuiMessage( "type", "clients-loading", "guids", guids); return true; } bool CNetClient::OnClientPaused(void *context, CFsmEvent *event) { ENSURE(event->GetType() == (uint)NMT_CLIENT_PAUSED); CNetClient* client = static_cast(context); CClientPausedMessage* message = static_cast(event->GetParamRef()); client->PushGuiMessage( "type", "paused", "pause", message->m_Pause != 0, "guid", message->m_GUID); return true; } bool CNetClient::OnLoadedGame(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_LOADED_GAME); CNetClient* client = static_cast(context); // All players have loaded the game - start running the turn manager // so that the game begins client->m_Game->SetTurnManager(client->m_ClientTurnManager); client->PushGuiMessage( "type", "netstatus", "status", "active"); // If we have rejoined an in progress game, send the rejoined message to the server. if (client->m_Rejoin) client->SendRejoinedMessage(); return true; } bool CNetClient::OnInGame(void *context, CFsmEvent* event) { // TODO: should split each of these cases into a separate method CNetClient* client = static_cast(context); CNetMessage* message = static_cast(event->GetParamRef()); if (message) { if (message->GetType() == NMT_SIMULATION_COMMAND) { CSimulationMessage* simMessage = static_cast (message); client->m_ClientTurnManager->OnSimulationMessage(simMessage); } else if (message->GetType() == NMT_SYNC_ERROR) { CSyncErrorMessage* syncMessage = static_cast (message); client->m_ClientTurnManager->OnSyncError(syncMessage->m_Turn, syncMessage->m_HashExpected, syncMessage->m_PlayerNames); } else if (message->GetType() == NMT_END_COMMAND_BATCH) { CEndCommandBatchMessage* endMessage = static_cast (message); client->m_ClientTurnManager->FinishedAllCommands(endMessage->m_Turn, endMessage->m_TurnLength); } } return true; } Index: ps/trunk/source/network/NetServer.cpp =================================================================== --- ps/trunk/source/network/NetServer.cpp (revision 27782) +++ ps/trunk/source/network/NetServer.cpp (revision 27783) @@ -1,1749 +1,1749 @@ /* Copyright (C) 2023 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 "NetServer.h" #include "NetClient.h" #include "NetEnet.h" #include "NetMessage.h" #include "NetSession.h" #include "NetServerTurnManager.h" #include "NetStats.h" #include "lib/external_libraries/enet.h" #include "lib/types.h" #include "network/StunClient.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/GUID.h" #include "ps/Hashing.h" #include "ps/Profile.h" #include "ps/Threading.h" #include "scriptinterface/ScriptContext.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/JSON.h" #include "simulation2/Simulation2.h" #include "simulation2/system/TurnManager.h" #if CONFIG2_MINIUPNPC #include #include #include #include #endif #include /** * Number of peers to allocate for the enet host. * Limited by ENET_PROTOCOL_MAXIMUM_PEER_ID (4096). * * At most 8 players, 32 observers and 1 temporary connection to send the "server full" disconnect-reason. */ #define MAX_CLIENTS 41 #define DEFAULT_SERVER_NAME L"Unnamed Server" constexpr int CHANNEL_COUNT = 1; constexpr int FAILED_PASSWORD_TRIES_BEFORE_BAN = 3; /** * enet_host_service timeout (msecs). * Smaller numbers may hurt performance; larger numbers will * hurt latency responding to messages from game thread. */ static const int HOST_SERVICE_TIMEOUT = 50; /** * Once ping goes above turn length * command delay, * the game will start 'freezing' for other clients while we catch up. * Since commands are sent client -> server -> client, divide by 2. * (duplicated in NetServer.cpp to avoid having to fetch the constants in a header file) */ constexpr u32 NETWORK_BAD_PING = DEFAULT_TURN_LENGTH * COMMAND_DELAY_MP / 2; CNetServer* g_NetServer = NULL; static CStr DebugName(CNetServerSession* session) { if (session == NULL) return "[unknown host]"; if (session->GetGUID().empty()) return "[unauthed host]"; return "[" + session->GetGUID().substr(0, 8) + "...]"; } /** * Async task for receiving the initial game state to be forwarded to another * client that is rejoining an in-progress network game. */ class CNetFileReceiveTask_ServerRejoin : public CNetFileReceiveTask { NONCOPYABLE(CNetFileReceiveTask_ServerRejoin); public: CNetFileReceiveTask_ServerRejoin(CNetServerWorker& server, u32 hostID) : m_Server(server), m_RejoinerHostID(hostID) { } virtual void OnComplete() { // We've received the game state from an existing player - now // we need to send it onwards to the newly rejoining player // Find the session corresponding to the rejoining host (if any) CNetServerSession* session = NULL; for (CNetServerSession* serverSession : m_Server.m_Sessions) { if (serverSession->GetHostID() == m_RejoinerHostID) { session = serverSession; break; } } if (!session) { LOGMESSAGE("Net server: rejoining client disconnected before we sent to it"); return; } // Store the received state file, and tell the client to start downloading it from us // TODO: this will get kind of confused if there's multiple clients downloading in parallel; // they'll race and get whichever happens to be the latest received by the server, // which should still work but isn't great m_Server.m_JoinSyncFile = m_Buffer; // Send the init attributes alongside - these should be correct since the game should be started. CJoinSyncStartMessage message; message.m_InitAttributes = Script::StringifyJSON(ScriptRequest(m_Server.GetScriptInterface()), &m_Server.m_InitAttributes); session->SendMessage(&message); } private: CNetServerWorker& m_Server; u32 m_RejoinerHostID; }; /* * XXX: We use some non-threadsafe functions from the worker thread. * See http://trac.wildfiregames.com/ticket/654 */ CNetServerWorker::CNetServerWorker(bool useLobbyAuth) : m_LobbyAuth(useLobbyAuth), m_Shutdown(false), m_ScriptInterface(NULL), m_NextHostID(1), m_Host(NULL), m_ControllerGUID(), m_Stats(NULL), m_LastConnectionCheck(0) { m_State = SERVER_STATE_UNCONNECTED; m_ServerTurnManager = NULL; m_ServerName = DEFAULT_SERVER_NAME; } CNetServerWorker::~CNetServerWorker() { if (m_State != SERVER_STATE_UNCONNECTED) { // Tell the thread to shut down { std::lock_guard lock(m_WorkerMutex); m_Shutdown = true; } // Wait for it to shut down cleanly m_WorkerThread.join(); } #if CONFIG2_MINIUPNPC if (m_UPnPThread.joinable()) m_UPnPThread.detach(); #endif // Clean up resources delete m_Stats; for (CNetServerSession* session : m_Sessions) { session->DisconnectNow(NDR_SERVER_SHUTDOWN); delete session; } if (m_Host) enet_host_destroy(m_Host); delete m_ServerTurnManager; } void CNetServerWorker::SetPassword(const CStr& hashedPassword) { m_Password = hashedPassword; } void CNetServerWorker::SetControllerSecret(const std::string& secret) { m_ControllerSecret = secret; } bool CNetServerWorker::CheckPassword(const std::string& password, const std::string& salt) const { return HashCryptographically(m_Password, salt) == password; } bool CNetServerWorker::SetupConnection(const u16 port) { ENSURE(m_State == SERVER_STATE_UNCONNECTED); ENSURE(!m_Host); // Bind to default host ENetAddress addr; addr.host = ENET_HOST_ANY; addr.port = port; // Create ENet server m_Host = PS::Enet::CreateHost(&addr, MAX_CLIENTS, CHANNEL_COUNT); if (!m_Host) { LOGERROR("Net server: enet_host_create failed"); return false; } m_Stats = new CNetStatsTable(); if (CProfileViewer::IsInitialised()) g_ProfileViewer.AddRootTable(m_Stats); m_State = SERVER_STATE_PREGAME; // Launch the worker thread m_WorkerThread = std::thread(Threading::HandleExceptions::Wrapper, this); #if CONFIG2_MINIUPNPC // Launch the UPnP thread m_UPnPThread = std::thread(Threading::HandleExceptions::Wrapper); #endif return true; } #if CONFIG2_MINIUPNPC void CNetServerWorker::SetupUPnP() { debug_SetThreadName("UPnP"); // Values we want to set. char psPort[6]; sprintf_s(psPort, ARRAY_SIZE(psPort), "%d", PS_DEFAULT_PORT); const char* leaseDuration = "0"; // Indefinite/permanent lease duration. const char* description = "0AD Multiplayer"; const char* protocall = "UDP"; char internalIPAddress[64]; char externalIPAddress[40]; // Variables to hold the values that actually get set. char intClient[40]; char intPort[6]; char duration[16]; // Intermediate variables. bool allocatedUrls = false; struct UPNPUrls urls; struct IGDdatas data; struct UPNPDev* devlist = NULL; // Make sure everything is properly freed. std::function freeUPnP = [&allocatedUrls, &urls, &devlist]() { if (allocatedUrls) FreeUPNPUrls(&urls); freeUPNPDevlist(devlist); // IGDdatas does not need to be freed according to UPNP_GetIGDFromUrl }; // Cached root descriptor URL. std::string rootDescURL; CFG_GET_VAL("network.upnprootdescurl", rootDescURL); if (!rootDescURL.empty()) LOGMESSAGE("Net server: attempting to use cached root descriptor URL: %s", rootDescURL.c_str()); int ret = 0; // Try a cached URL first if (!rootDescURL.empty() && UPNP_GetIGDFromUrl(rootDescURL.c_str(), &urls, &data, internalIPAddress, sizeof(internalIPAddress))) { LOGMESSAGE("Net server: using cached IGD = %s", urls.controlURL); ret = 1; } // No cached URL, or it did not respond. Try getting a valid UPnP device for 10 seconds. #if defined(MINIUPNPC_API_VERSION) && MINIUPNPC_API_VERSION >= 14 else if ((devlist = upnpDiscover(10000, 0, 0, 0, 0, 2, 0)) != NULL) #else else if ((devlist = upnpDiscover(10000, 0, 0, 0, 0, 0)) != NULL) #endif { ret = UPNP_GetValidIGD(devlist, &urls, &data, internalIPAddress, sizeof(internalIPAddress)); allocatedUrls = ret != 0; // urls is allocated on non-zero return values } else { LOGMESSAGE("Net server: upnpDiscover failed and no working cached URL."); freeUPnP(); return; } switch (ret) { case 0: LOGMESSAGE("Net server: No IGD found"); break; case 1: LOGMESSAGE("Net server: found valid IGD = %s", urls.controlURL); break; case 2: LOGMESSAGE("Net server: found a valid, not connected IGD = %s, will try to continue anyway", urls.controlURL); break; case 3: LOGMESSAGE("Net server: found a UPnP device unrecognized as IGD = %s, will try to continue anyway", urls.controlURL); break; default: debug_warn(L"Unrecognized return value from UPNP_GetValidIGD"); } // Try getting our external/internet facing IP. TODO: Display this on the game-setup page for conviniance. ret = UPNP_GetExternalIPAddress(urls.controlURL, data.first.servicetype, externalIPAddress); if (ret != UPNPCOMMAND_SUCCESS) { LOGMESSAGE("Net server: GetExternalIPAddress failed with code %d (%s)", ret, strupnperror(ret)); freeUPnP(); return; } LOGMESSAGE("Net server: ExternalIPAddress = %s", externalIPAddress); // Try to setup port forwarding. ret = UPNP_AddPortMapping(urls.controlURL, data.first.servicetype, psPort, psPort, internalIPAddress, description, protocall, 0, leaseDuration); if (ret != UPNPCOMMAND_SUCCESS) { LOGMESSAGE("Net server: AddPortMapping(%s, %s, %s) failed with code %d (%s)", psPort, psPort, internalIPAddress, ret, strupnperror(ret)); freeUPnP(); return; } // Check that the port was actually forwarded. ret = UPNP_GetSpecificPortMappingEntry(urls.controlURL, data.first.servicetype, psPort, protocall, #if defined(MINIUPNPC_API_VERSION) && MINIUPNPC_API_VERSION >= 10 NULL/*remoteHost*/, #endif intClient, intPort, NULL/*desc*/, NULL/*enabled*/, duration); if (ret != UPNPCOMMAND_SUCCESS) { LOGMESSAGE("Net server: GetSpecificPortMappingEntry() failed with code %d (%s)", ret, strupnperror(ret)); freeUPnP(); return; } LOGMESSAGE("Net server: External %s:%s %s is redirected to internal %s:%s (duration=%s)", externalIPAddress, psPort, protocall, intClient, intPort, duration); // Cache root descriptor URL to try to avoid discovery next time. g_ConfigDB.SetValueString(CFG_USER, "network.upnprootdescurl", urls.controlURL); g_ConfigDB.WriteValueToFile(CFG_USER, "network.upnprootdescurl", urls.controlURL); LOGMESSAGE("Net server: cached UPnP root descriptor URL as %s", urls.controlURL); freeUPnP(); } #endif // CONFIG2_MINIUPNPC bool CNetServerWorker::SendMessage(ENetPeer* peer, const CNetMessage* message) { ENSURE(m_Host); CNetServerSession* session = static_cast(peer->data); return CNetHost::SendMessage(message, peer, DebugName(session).c_str()); } bool CNetServerWorker::Broadcast(const CNetMessage* message, const std::vector& targetStates) { ENSURE(m_Host); bool ok = true; // TODO: this does lots of repeated message serialisation if we have lots // of remote peers; could do it more efficiently if that's a real problem for (CNetServerSession* session : m_Sessions) if (std::find(targetStates.begin(), targetStates.end(), static_cast(session->GetCurrState())) != targetStates.end() && !session->SendMessage(message)) ok = false; return ok; } void CNetServerWorker::RunThread(CNetServerWorker* data) { debug_SetThreadName("NetServer"); data->Run(); } void CNetServerWorker::Run() { // The script context uses the profiler and therefore the thread must be registered before the context is created g_Profiler2.RegisterCurrentThread("Net server"); // We create a new ScriptContext for this network thread, with a single ScriptInterface. std::shared_ptr netServerContext = ScriptContext::CreateContext(); m_ScriptInterface = new ScriptInterface("Engine", "Net server", netServerContext); m_InitAttributes.init(m_ScriptInterface->GetGeneralJSContext(), JS::UndefinedValue()); while (true) { if (!RunStep()) break; // Update profiler stats m_Stats->LatchHostState(m_Host); } // Clear roots before deleting their context m_SavedCommands.clear(); SAFE_DELETE(m_ScriptInterface); } bool CNetServerWorker::RunStep() { // Check for messages from the game thread. // (Do as little work as possible while the mutex is held open, // to avoid performance problems and deadlocks.) m_ScriptInterface->GetContext()->MaybeIncrementalGC(0.5f); ScriptRequest rq(m_ScriptInterface); std::vector newStartGame; std::vector newGameAttributes; std::vector> newLobbyAuths; std::vector newTurnLength; { std::lock_guard lock(m_WorkerMutex); if (m_Shutdown) return false; newStartGame.swap(m_StartGameQueue); newGameAttributes.swap(m_InitAttributesQueue); newLobbyAuths.swap(m_LobbyAuthQueue); newTurnLength.swap(m_TurnLengthQueue); } if (!newGameAttributes.empty()) { if (m_State != SERVER_STATE_UNCONNECTED && m_State != SERVER_STATE_PREGAME) LOGERROR("NetServer: Init Attributes cannot be changed after the server starts loading."); else { JS::RootedValue gameAttributesVal(rq.cx); Script::ParseJSON(rq, newGameAttributes.back(), &gameAttributesVal); m_InitAttributes = gameAttributesVal; } } if (!newTurnLength.empty()) SetTurnLength(newTurnLength.back()); while (!newLobbyAuths.empty()) { const std::pair& auth = newLobbyAuths.back(); ProcessLobbyAuth(auth.first, auth.second); newLobbyAuths.pop_back(); } // Perform file transfers for (CNetServerSession* session : m_Sessions) session->GetFileTransferer().Poll(); CheckClientConnections(); // Process network events: ENetEvent event; int status = enet_host_service(m_Host, &event, HOST_SERVICE_TIMEOUT); if (status < 0) { LOGERROR("CNetServerWorker: enet_host_service failed (%d)", status); // TODO: notify game that the server has shut down return false; } if (status == 0) { // Reached timeout with no events - try again return true; } // Process the event: switch (event.type) { case ENET_EVENT_TYPE_CONNECT: { // Report the client address char hostname[256] = "(error)"; enet_address_get_host_ip(&event.peer->address, hostname, ARRAY_SIZE(hostname)); LOGMESSAGE("Net server: Received connection from %s:%u", hostname, (unsigned int)event.peer->address.port); // Set up a session object for this peer CNetServerSession* session = new CNetServerSession(*this, event.peer); m_Sessions.push_back(session); SetupSession(session); ENSURE(event.peer->data == NULL); event.peer->data = session; HandleConnect(session); break; } case ENET_EVENT_TYPE_DISCONNECT: { // If there is an active session with this peer, then reset and delete it CNetServerSession* session = static_cast(event.peer->data); if (session) { LOGMESSAGE("Net server: Disconnected %s", DebugName(session).c_str()); // Remove the session first, so we won't send player-update messages to it // when updating the FSM m_Sessions.erase(remove(m_Sessions.begin(), m_Sessions.end(), session), m_Sessions.end()); session->Update((uint)NMT_CONNECTION_LOST, NULL); delete session; event.peer->data = NULL; } if (m_State == SERVER_STATE_LOADING) CheckGameLoadStatus(NULL); break; } case ENET_EVENT_TYPE_RECEIVE: { // If there is an active session with this peer, then process the message CNetServerSession* session = static_cast(event.peer->data); if (session) { // Create message from raw data CNetMessage* msg = CNetMessageFactory::CreateMessage(event.packet->data, event.packet->dataLength, GetScriptInterface()); if (msg) { LOGMESSAGE("Net server: Received message %s of size %lu from %s", msg->ToString().c_str(), (unsigned long)msg->GetSerializedLength(), DebugName(session).c_str()); HandleMessageReceive(msg, session); delete msg; } } // Done using the packet enet_packet_destroy(event.packet); break; } case ENET_EVENT_TYPE_NONE: break; } return true; } void CNetServerWorker::CheckClientConnections() { // Send messages at most once per second std::time_t now = std::time(nullptr); if (now <= m_LastConnectionCheck) return; m_LastConnectionCheck = now; for (size_t i = 0; i < m_Sessions.size(); ++i) { u32 lastReceived = m_Sessions[i]->GetLastReceivedTime(); u32 meanRTT = m_Sessions[i]->GetMeanRTT(); CNetMessage* message = nullptr; // Report if we didn't hear from the client since few seconds if (lastReceived > NETWORK_WARNING_TIMEOUT) { CClientTimeoutMessage* msg = new CClientTimeoutMessage(); msg->m_GUID = m_Sessions[i]->GetGUID(); msg->m_LastReceivedTime = lastReceived; message = msg; } // Report if the client has bad ping else if (meanRTT > NETWORK_BAD_PING) { CClientPerformanceMessage* msg = new CClientPerformanceMessage(); CClientPerformanceMessage::S_m_Clients client; client.m_GUID = m_Sessions[i]->GetGUID(); client.m_MeanRTT = meanRTT; msg->m_Clients.push_back(client); message = msg; } // Send to all clients except the affected one // (since that will show the locally triggered warning instead). // Also send it to clients that finished the loading screen while // the game is still waiting for other clients to finish the loading screen. if (message) for (size_t j = 0; j < m_Sessions.size(); ++j) { if (i != j && ( (m_Sessions[j]->GetCurrState() == NSS_PREGAME && m_State == SERVER_STATE_PREGAME) || m_Sessions[j]->GetCurrState() == NSS_INGAME)) { m_Sessions[j]->SendMessage(message); } } SAFE_DELETE(message); } } void CNetServerWorker::HandleMessageReceive(const CNetMessage* message, CNetServerSession* session) { // Handle non-FSM messages first Status status = session->GetFileTransferer().HandleMessageReceive(*message); if (status != INFO::SKIPPED) return; if (message->GetType() == NMT_FILE_TRANSFER_REQUEST) { CFileTransferRequestMessage* reqMessage = (CFileTransferRequestMessage*)message; // Rejoining client got our JoinSyncStart after we received the state from // another client, and has now requested that we forward it to them ENSURE(!m_JoinSyncFile.empty()); session->GetFileTransferer().StartResponse(reqMessage->m_RequestID, m_JoinSyncFile); return; } // Update FSM if (!session->Update(message->GetType(), (void*)message)) LOGERROR("Net server: Error running FSM update (type=%d state=%d)", (int)message->GetType(), (int)session->GetCurrState()); } void CNetServerWorker::SetupSession(CNetServerSession* session) { void* context = session; // Set up transitions for session session->AddTransition(NSS_UNCONNECTED, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED); session->AddTransition(NSS_HANDSHAKE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED); - session->AddTransition(NSS_HANDSHAKE, (uint)NMT_CLIENT_HANDSHAKE, NSS_AUTHENTICATE, (void*)&OnClientHandshake, context); + session->AddTransition(NSS_HANDSHAKE, (uint)NMT_CLIENT_HANDSHAKE, NSS_AUTHENTICATE, &OnClientHandshake, context); session->AddTransition(NSS_LOBBY_AUTHENTICATE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED); - session->AddTransition(NSS_LOBBY_AUTHENTICATE, (uint)NMT_AUTHENTICATE, NSS_PREGAME, (void*)&OnAuthenticate, context); + session->AddTransition(NSS_LOBBY_AUTHENTICATE, (uint)NMT_AUTHENTICATE, NSS_PREGAME, &OnAuthenticate, context); session->AddTransition(NSS_AUTHENTICATE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED); - session->AddTransition(NSS_AUTHENTICATE, (uint)NMT_AUTHENTICATE, NSS_PREGAME, (void*)&OnAuthenticate, context); + session->AddTransition(NSS_AUTHENTICATE, (uint)NMT_AUTHENTICATE, NSS_PREGAME, &OnAuthenticate, context); - session->AddTransition(NSS_PREGAME, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, (void*)&OnDisconnect, context); - session->AddTransition(NSS_PREGAME, (uint)NMT_CHAT, NSS_PREGAME, (void*)&OnChat, context); - session->AddTransition(NSS_PREGAME, (uint)NMT_READY, NSS_PREGAME, (void*)&OnReady, context); - session->AddTransition(NSS_PREGAME, (uint)NMT_CLEAR_ALL_READY, NSS_PREGAME, (void*)&OnClearAllReady, context); - session->AddTransition(NSS_PREGAME, (uint)NMT_GAME_SETUP, NSS_PREGAME, (void*)&OnGameSetup, context); - session->AddTransition(NSS_PREGAME, (uint)NMT_ASSIGN_PLAYER, NSS_PREGAME, (void*)&OnAssignPlayer, context); - session->AddTransition(NSS_PREGAME, (uint)NMT_KICKED, NSS_PREGAME, (void*)&OnKickPlayer, context); - session->AddTransition(NSS_PREGAME, (uint)NMT_GAME_START, NSS_PREGAME, (void*)&OnGameStart, context); - session->AddTransition(NSS_PREGAME, (uint)NMT_LOADED_GAME, NSS_INGAME, (void*)&OnLoadedGame, context); - - session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_KICKED, NSS_JOIN_SYNCING, (void*)&OnKickPlayer, context); - session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, (void*)&OnDisconnect, context); - session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_LOADED_GAME, NSS_INGAME, (void*)&OnJoinSyncingLoadedGame, context); - - session->AddTransition(NSS_INGAME, (uint)NMT_REJOINED, NSS_INGAME, (void*)&OnRejoined, context); - session->AddTransition(NSS_INGAME, (uint)NMT_KICKED, NSS_INGAME, (void*)&OnKickPlayer, context); - session->AddTransition(NSS_INGAME, (uint)NMT_CLIENT_PAUSED, NSS_INGAME, (void*)&OnClientPaused, context); - session->AddTransition(NSS_INGAME, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, (void*)&OnDisconnect, context); - session->AddTransition(NSS_INGAME, (uint)NMT_CHAT, NSS_INGAME, (void*)&OnChat, context); - session->AddTransition(NSS_INGAME, (uint)NMT_SIMULATION_COMMAND, NSS_INGAME, (void*)&OnSimulationCommand, context); - session->AddTransition(NSS_INGAME, (uint)NMT_SYNC_CHECK, NSS_INGAME, (void*)&OnSyncCheck, context); - session->AddTransition(NSS_INGAME, (uint)NMT_END_COMMAND_BATCH, NSS_INGAME, (void*)&OnEndCommandBatch, context); + session->AddTransition(NSS_PREGAME, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, &OnDisconnect, context); + session->AddTransition(NSS_PREGAME, (uint)NMT_CHAT, NSS_PREGAME, &OnChat, context); + session->AddTransition(NSS_PREGAME, (uint)NMT_READY, NSS_PREGAME, &OnReady, context); + session->AddTransition(NSS_PREGAME, (uint)NMT_CLEAR_ALL_READY, NSS_PREGAME, &OnClearAllReady, context); + session->AddTransition(NSS_PREGAME, (uint)NMT_GAME_SETUP, NSS_PREGAME, &OnGameSetup, context); + session->AddTransition(NSS_PREGAME, (uint)NMT_ASSIGN_PLAYER, NSS_PREGAME, &OnAssignPlayer, context); + session->AddTransition(NSS_PREGAME, (uint)NMT_KICKED, NSS_PREGAME, &OnKickPlayer, context); + session->AddTransition(NSS_PREGAME, (uint)NMT_GAME_START, NSS_PREGAME, &OnGameStart, context); + session->AddTransition(NSS_PREGAME, (uint)NMT_LOADED_GAME, NSS_INGAME, &OnLoadedGame, context); + + session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_KICKED, NSS_JOIN_SYNCING, &OnKickPlayer, context); + session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, &OnDisconnect, context); + session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_LOADED_GAME, NSS_INGAME, &OnJoinSyncingLoadedGame, context); + + session->AddTransition(NSS_INGAME, (uint)NMT_REJOINED, NSS_INGAME, &OnRejoined, context); + session->AddTransition(NSS_INGAME, (uint)NMT_KICKED, NSS_INGAME, &OnKickPlayer, context); + session->AddTransition(NSS_INGAME, (uint)NMT_CLIENT_PAUSED, NSS_INGAME, &OnClientPaused, context); + session->AddTransition(NSS_INGAME, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, &OnDisconnect, context); + session->AddTransition(NSS_INGAME, (uint)NMT_CHAT, NSS_INGAME, &OnChat, context); + session->AddTransition(NSS_INGAME, (uint)NMT_SIMULATION_COMMAND, NSS_INGAME, &OnSimulationCommand, context); + session->AddTransition(NSS_INGAME, (uint)NMT_SYNC_CHECK, NSS_INGAME, &OnSyncCheck, context); + session->AddTransition(NSS_INGAME, (uint)NMT_END_COMMAND_BATCH, NSS_INGAME, &OnEndCommandBatch, context); // Set first state session->SetFirstState(NSS_HANDSHAKE); } bool CNetServerWorker::HandleConnect(CNetServerSession* session) { if (std::find(m_BannedIPs.begin(), m_BannedIPs.end(), session->GetIPAddress()) != m_BannedIPs.end()) { session->Disconnect(NDR_BANNED); return false; } CSrvHandshakeMessage handshake; handshake.m_Magic = PS_PROTOCOL_MAGIC; handshake.m_ProtocolVersion = PS_PROTOCOL_VERSION; handshake.m_SoftwareVersion = PS_PROTOCOL_VERSION; return session->SendMessage(&handshake); } void CNetServerWorker::OnUserJoin(CNetServerSession* session) { AddPlayer(session->GetGUID(), session->GetUserName()); CPlayerAssignmentMessage assignMessage; ConstructPlayerAssignmentMessage(assignMessage); session->SendMessage(&assignMessage); } void CNetServerWorker::OnUserLeave(CNetServerSession* session) { std::vector::iterator pausing = std::find(m_PausingPlayers.begin(), m_PausingPlayers.end(), session->GetGUID()); if (pausing != m_PausingPlayers.end()) m_PausingPlayers.erase(pausing); RemovePlayer(session->GetGUID()); if (m_ServerTurnManager && session->GetCurrState() != NSS_JOIN_SYNCING) m_ServerTurnManager->UninitialiseClient(session->GetHostID()); // TODO: ought to switch the player controlled by that client // back to AI control, or something? } void CNetServerWorker::AddPlayer(const CStr& guid, const CStrW& name) { // Find all player IDs in active use; we mustn't give them to a second player (excluding the unassigned ID: -1) std::set usedIDs; for (const std::pair& p : m_PlayerAssignments) if (p.second.m_Enabled && p.second.m_PlayerID != -1) usedIDs.insert(p.second.m_PlayerID); // If the player is rejoining after disconnecting, try to give them // back their old player ID. Don't do this in pregame however, // as that ID might be invalid for various reasons. i32 playerID = -1; if (m_State != SERVER_STATE_UNCONNECTED && m_State != SERVER_STATE_PREGAME) { // Try to match GUID first for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it) { if (!it->second.m_Enabled && it->first == guid && usedIDs.find(it->second.m_PlayerID) == usedIDs.end()) { playerID = it->second.m_PlayerID; m_PlayerAssignments.erase(it); // delete the old mapping, since we've got a new one now goto found; } } // Try to match username next for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it) { if (!it->second.m_Enabled && it->second.m_Name == name && usedIDs.find(it->second.m_PlayerID) == usedIDs.end()) { playerID = it->second.m_PlayerID; m_PlayerAssignments.erase(it); // delete the old mapping, since we've got a new one now goto found; } } } found: PlayerAssignment assignment; assignment.m_Enabled = true; assignment.m_Name = name; assignment.m_PlayerID = playerID; assignment.m_Status = 0; m_PlayerAssignments[guid] = assignment; // Send the new assignments to all currently active players // (which does not include the one that's just joining) SendPlayerAssignments(); } void CNetServerWorker::RemovePlayer(const CStr& guid) { m_PlayerAssignments[guid].m_Enabled = false; SendPlayerAssignments(); } void CNetServerWorker::ClearAllPlayerReady() { for (std::pair& p : m_PlayerAssignments) if (p.second.m_Status != 2) p.second.m_Status = 0; SendPlayerAssignments(); } void CNetServerWorker::KickPlayer(const CStrW& playerName, const bool ban) { // Find the user with that name std::vector::iterator it = std::find_if(m_Sessions.begin(), m_Sessions.end(), [&](CNetServerSession* session) { return session->GetUserName() == playerName; }); // and return if no one or the host has that name if (it == m_Sessions.end() || (*it)->GetGUID() == m_ControllerGUID) return; if (ban) { // Remember name if (std::find(m_BannedPlayers.begin(), m_BannedPlayers.end(), playerName) == m_BannedPlayers.end()) m_BannedPlayers.push_back(m_LobbyAuth ? CStrW(playerName.substr(0, playerName.find(L" ("))) : playerName); // Remember IP address u32 ipAddress = (*it)->GetIPAddress(); if (std::find(m_BannedIPs.begin(), m_BannedIPs.end(), ipAddress) == m_BannedIPs.end()) m_BannedIPs.push_back(ipAddress); } // Disconnect that user (*it)->Disconnect(ban ? NDR_BANNED : NDR_KICKED); // Send message notifying other clients CKickedMessage kickedMessage; kickedMessage.m_Name = playerName; kickedMessage.m_Ban = ban; Broadcast(&kickedMessage, { NSS_PREGAME, NSS_JOIN_SYNCING, NSS_INGAME }); } void CNetServerWorker::AssignPlayer(int playerID, const CStr& guid) { // Remove anyone who's already assigned to this player for (std::pair& p : m_PlayerAssignments) { if (p.second.m_PlayerID == playerID) p.second.m_PlayerID = -1; } // Update this host's assignment if it exists if (m_PlayerAssignments.find(guid) != m_PlayerAssignments.end()) m_PlayerAssignments[guid].m_PlayerID = playerID; SendPlayerAssignments(); } void CNetServerWorker::ConstructPlayerAssignmentMessage(CPlayerAssignmentMessage& message) { for (const std::pair& p : m_PlayerAssignments) { if (!p.second.m_Enabled) continue; CPlayerAssignmentMessage::S_m_Hosts h; h.m_GUID = p.first; h.m_Name = p.second.m_Name; h.m_PlayerID = p.second.m_PlayerID; h.m_Status = p.second.m_Status; message.m_Hosts.push_back(h); } } void CNetServerWorker::SendPlayerAssignments() { CPlayerAssignmentMessage message; ConstructPlayerAssignmentMessage(message); Broadcast(&message, { NSS_PREGAME, NSS_JOIN_SYNCING, NSS_INGAME }); } const ScriptInterface& CNetServerWorker::GetScriptInterface() { return *m_ScriptInterface; } void CNetServerWorker::SetTurnLength(u32 msecs) { if (m_ServerTurnManager) m_ServerTurnManager->SetTurnLength(msecs); } void CNetServerWorker::ProcessLobbyAuth(const CStr& name, const CStr& token) { LOGMESSAGE("Net Server: Received lobby auth message from %s with %s", name, token); // Find the user with that guid std::vector::iterator it = std::find_if(m_Sessions.begin(), m_Sessions.end(), [&](CNetServerSession* session) { return session->GetGUID() == token; }); if (it == m_Sessions.end()) return; (*it)->SetUserName(name.FromUTF8()); // Send an empty message to request the authentication message from the client // after its identity has been confirmed via the lobby CAuthenticateMessage emptyMessage; (*it)->SendMessage(&emptyMessage); } bool CNetServerWorker::OnClientHandshake(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_CLIENT_HANDSHAKE); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); CCliHandshakeMessage* message = (CCliHandshakeMessage*)event->GetParamRef(); if (message->m_ProtocolVersion != PS_PROTOCOL_VERSION) { session->Disconnect(NDR_INCORRECT_PROTOCOL_VERSION); return false; } CStr guid = ps_generate_guid(); int count = 0; // Ensure unique GUID while(std::find_if( server.m_Sessions.begin(), server.m_Sessions.end(), [&guid] (const CNetServerSession* session) { return session->GetGUID() == guid; }) != server.m_Sessions.end()) { if (++count > 100) { session->Disconnect(NDR_GUID_FAILED); return true; } guid = ps_generate_guid(); } session->SetGUID(guid); CSrvHandshakeResponseMessage handshakeResponse; handshakeResponse.m_UseProtocolVersion = PS_PROTOCOL_VERSION; handshakeResponse.m_GUID = guid; handshakeResponse.m_Flags = 0; if (server.m_LobbyAuth) { handshakeResponse.m_Flags |= PS_NETWORK_FLAG_REQUIRE_LOBBYAUTH; session->SetNextState(NSS_LOBBY_AUTHENTICATE); } session->SendMessage(&handshakeResponse); return true; } bool CNetServerWorker::OnAuthenticate(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_AUTHENTICATE); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); // Prohibit joins while the game is loading if (server.m_State == SERVER_STATE_LOADING) { LOGMESSAGE("Refused connection while the game is loading"); session->Disconnect(NDR_SERVER_LOADING); return true; } CAuthenticateMessage* message = (CAuthenticateMessage*)event->GetParamRef(); CStrW username = SanitisePlayerName(message->m_Name); CStrW usernameWithoutRating(username.substr(0, username.find(L" ("))); // Compare the lowercase names as specified by https://xmpp.org/extensions/xep-0029.html#sect-idm139493404168176 // "[...] comparisons will be made in case-normalized canonical form." if (server.m_LobbyAuth && usernameWithoutRating.LowerCase() != session->GetUserName().LowerCase()) { LOGERROR("Net server: lobby auth: %s tried joining as %s", session->GetUserName().ToUTF8(), usernameWithoutRating.ToUTF8()); session->Disconnect(NDR_LOBBY_AUTH_FAILED); return true; } // Check the password before anything else. // NB: m_Name must match the client's salt, @see CNetClient::SetGamePassword if (!server.CheckPassword(message->m_Password, message->m_Name.ToUTF8())) { // Noisy logerror because players are not supposed to be able to get the IP, // so this might be someone targeting the host for some reason // (or TODO a dedicated server and we do want to log anyways) LOGERROR("Net server: user %s tried joining with the wrong password", session->GetUserName().ToUTF8()); session->Disconnect(NDR_SERVER_REFUSED); return true; } // Either deduplicate or prohibit join if name is in use bool duplicatePlayernames = false; CFG_GET_VAL("network.duplicateplayernames", duplicatePlayernames); // If lobby authentication is enabled, the clients playername has already been registered. // There also can't be any duplicated names. if (!server.m_LobbyAuth && duplicatePlayernames) username = server.DeduplicatePlayerName(username); else { std::vector::iterator it = std::find_if( server.m_Sessions.begin(), server.m_Sessions.end(), [&username] (const CNetServerSession* session) { return session->GetUserName() == username; }); if (it != server.m_Sessions.end() && (*it) != session) { session->Disconnect(NDR_PLAYERNAME_IN_USE); return true; } } // Disconnect banned usernames if (std::find(server.m_BannedPlayers.begin(), server.m_BannedPlayers.end(), server.m_LobbyAuth ? usernameWithoutRating : username) != server.m_BannedPlayers.end()) { session->Disconnect(NDR_BANNED); return true; } int maxObservers = 0; CFG_GET_VAL("network.observerlimit", maxObservers); bool isRejoining = false; bool serverFull = false; if (server.m_State == SERVER_STATE_PREGAME) { // Don't check for maxObservers in the gamesetup, as we don't know yet who will be assigned serverFull = server.m_Sessions.size() >= MAX_CLIENTS; } else { bool isObserver = true; int disconnectedPlayers = 0; int connectedPlayers = 0; // (TODO: if GUIDs were stable, we should use them instead) for (const std::pair& p : server.m_PlayerAssignments) { const PlayerAssignment& assignment = p.second; if (!assignment.m_Enabled && assignment.m_Name == username) { isObserver = assignment.m_PlayerID == -1; isRejoining = true; } if (assignment.m_PlayerID == -1) continue; if (assignment.m_Enabled) ++connectedPlayers; else ++disconnectedPlayers; } // Optionally allow everyone or only buddies to join after the game has started if (!isRejoining) { CStr observerLateJoin; CFG_GET_VAL("network.lateobservers", observerLateJoin); if (observerLateJoin == "everyone") { isRejoining = true; } else if (observerLateJoin == "buddies") { CStr buddies; CFG_GET_VAL("lobby.buddies", buddies); std::wstringstream buddiesStream(wstring_from_utf8(buddies)); CStrW buddy; while (std::getline(buddiesStream, buddy, L',')) { if (buddy == usernameWithoutRating) { isRejoining = true; break; } } } } if (!isRejoining) { LOGMESSAGE("Refused connection after game start from not-previously-known user \"%s\"", utf8_from_wstring(username)); session->Disconnect(NDR_SERVER_ALREADY_IN_GAME); return true; } // Ensure all players will be able to rejoin serverFull = isObserver && ( (int) server.m_Sessions.size() - connectedPlayers > maxObservers || (int) server.m_Sessions.size() + disconnectedPlayers >= MAX_CLIENTS); } if (serverFull) { session->Disconnect(NDR_SERVER_FULL); return true; } u32 newHostID = server.m_NextHostID++; session->SetUserName(username); session->SetHostID(newHostID); CAuthenticateResultMessage authenticateResult; authenticateResult.m_Code = isRejoining ? ARC_OK_REJOINING : ARC_OK; authenticateResult.m_HostID = newHostID; authenticateResult.m_Message = L"Logged in"; authenticateResult.m_IsController = 0; if (message->m_ControllerSecret == server.m_ControllerSecret) { if (server.m_ControllerGUID.empty()) { server.m_ControllerGUID = session->GetGUID(); authenticateResult.m_IsController = 1; } // TODO: we could probably handle having several controllers, or swapping? } session->SendMessage(&authenticateResult); server.OnUserJoin(session); if (isRejoining) { ENSURE(server.m_State != SERVER_STATE_UNCONNECTED && server.m_State != SERVER_STATE_PREGAME); // Request a copy of the current game state from an existing player, // so we can send it on to the new player // Assume session 0 is most likely the local player, so they're // the most efficient client to request a copy from CNetServerSession* sourceSession = server.m_Sessions.at(0); sourceSession->GetFileTransferer().StartTask( std::shared_ptr(new CNetFileReceiveTask_ServerRejoin(server, newHostID)) ); session->SetNextState(NSS_JOIN_SYNCING); } return true; } bool CNetServerWorker::OnSimulationCommand(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_SIMULATION_COMMAND); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); CSimulationMessage* message = (CSimulationMessage*)event->GetParamRef(); // Ignore messages sent by one player on behalf of another player // unless cheating is enabled bool cheatsEnabled = false; const ScriptInterface& scriptInterface = server.GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue settings(rq.cx); Script::GetProperty(rq, server.m_InitAttributes, "settings", &settings); if (Script::HasProperty(rq, settings, "CheatsEnabled")) Script::GetProperty(rq, settings, "CheatsEnabled", cheatsEnabled); PlayerAssignmentMap::iterator it = server.m_PlayerAssignments.find(session->GetGUID()); // When cheating is disabled, fail if the player the message claims to // represent does not exist or does not match the sender's player name if (!cheatsEnabled && (it == server.m_PlayerAssignments.end() || it->second.m_PlayerID != message->m_Player)) return true; // Send it back to all clients that have finished // the loading screen (and the synchronization when rejoining) server.Broadcast(message, { NSS_INGAME }); // Save all the received commands if (server.m_SavedCommands.size() < message->m_Turn + 1) server.m_SavedCommands.resize(message->m_Turn + 1); server.m_SavedCommands[message->m_Turn].push_back(*message); // TODO: we shouldn't send the message back to the client that first sent it return true; } bool CNetServerWorker::OnSyncCheck(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_SYNC_CHECK); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); CSyncCheckMessage* message = (CSyncCheckMessage*)event->GetParamRef(); server.m_ServerTurnManager->NotifyFinishedClientUpdate(*session, message->m_Turn, message->m_Hash); return true; } bool CNetServerWorker::OnEndCommandBatch(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_END_COMMAND_BATCH); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); CEndCommandBatchMessage* message = (CEndCommandBatchMessage*)event->GetParamRef(); // The turn-length field is ignored server.m_ServerTurnManager->NotifyFinishedClientCommands(*session, message->m_Turn); return true; } bool CNetServerWorker::OnChat(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_CHAT); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); CChatMessage* message = (CChatMessage*)event->GetParamRef(); message->m_GUID = session->GetGUID(); server.Broadcast(message, { NSS_PREGAME, NSS_INGAME }); return true; } bool CNetServerWorker::OnReady(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_READY); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); // Occurs if a client presses not-ready // in the very last moment before the hosts starts the game if (server.m_State == SERVER_STATE_LOADING) return true; CReadyMessage* message = (CReadyMessage*)event->GetParamRef(); message->m_GUID = session->GetGUID(); server.Broadcast(message, { NSS_PREGAME }); server.m_PlayerAssignments[message->m_GUID].m_Status = message->m_Status; return true; } bool CNetServerWorker::OnClearAllReady(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_CLEAR_ALL_READY); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); if (session->GetGUID() == server.m_ControllerGUID) server.ClearAllPlayerReady(); return true; } bool CNetServerWorker::OnGameSetup(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_GAME_SETUP); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); // Changing the settings after gamestart is not implemented and would cause an Out-of-sync error. // This happened when doubleclicking on the startgame button. if (server.m_State != SERVER_STATE_PREGAME) return true; // Only the controller is allowed to send game setup updates. // TODO: it would be good to allow other players to request changes to some settings, // e.g. their civilisation. // Possibly this should use another message, to enforce a single source of truth. if (session->GetGUID() == server.m_ControllerGUID) { CGameSetupMessage* message = (CGameSetupMessage*)event->GetParamRef(); server.Broadcast(message, { NSS_PREGAME }); } return true; } bool CNetServerWorker::OnAssignPlayer(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_ASSIGN_PLAYER); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); if (session->GetGUID() == server.m_ControllerGUID) { CAssignPlayerMessage* message = (CAssignPlayerMessage*)event->GetParamRef(); server.AssignPlayer(message->m_PlayerID, message->m_GUID); } return true; } bool CNetServerWorker::OnGameStart(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_GAME_START); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); if (session->GetGUID() != server.m_ControllerGUID) return true; CGameStartMessage* message = (CGameStartMessage*)event->GetParamRef(); server.StartGame(message->m_InitAttributes); return true; } bool CNetServerWorker::OnLoadedGame(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_LOADED_GAME); CNetServerSession* loadedSession = (CNetServerSession*)context; CNetServerWorker& server = loadedSession->GetServer(); // We're in the loading state, so wait until every client has loaded // before starting the game ENSURE(server.m_State == SERVER_STATE_LOADING); if (server.CheckGameLoadStatus(loadedSession)) return true; CClientsLoadingMessage message; // We always send all GUIDs of clients in the loading state // so that we don't have to bother about switching GUI pages for (CNetServerSession* session : server.m_Sessions) if (session->GetCurrState() != NSS_INGAME && loadedSession->GetGUID() != session->GetGUID()) { CClientsLoadingMessage::S_m_Clients client; client.m_GUID = session->GetGUID(); message.m_Clients.push_back(client); } // Send to the client who has loaded the game but did not reach the NSS_INGAME state yet loadedSession->SendMessage(&message); server.Broadcast(&message, { NSS_INGAME }); return true; } bool CNetServerWorker::OnJoinSyncingLoadedGame(void* context, CFsmEvent* event) { // A client rejoining an in-progress game has now finished loading the // map and deserialized the initial state. // The simulation may have progressed since then, so send any subsequent // commands to them and set them as an active player so they can participate // in all future turns. // // (TODO: if it takes a long time for them to receive and execute all these // commands, the other players will get frozen for that time and may be unhappy; // we could try repeating this process a few times until the client converges // on the up-to-date state, before setting them as active.) ENSURE(event->GetType() == (uint)NMT_LOADED_GAME); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); CLoadedGameMessage* message = (CLoadedGameMessage*)event->GetParamRef(); u32 turn = message->m_CurrentTurn; u32 readyTurn = server.m_ServerTurnManager->GetReadyTurn(); // Send them all commands received since their saved state, // and turn-ended messages for any turns that have already been processed for (size_t i = turn + 1; i < std::max(readyTurn+1, (u32)server.m_SavedCommands.size()); ++i) { if (i < server.m_SavedCommands.size()) for (size_t j = 0; j < server.m_SavedCommands[i].size(); ++j) session->SendMessage(&server.m_SavedCommands[i][j]); if (i <= readyTurn) { CEndCommandBatchMessage endMessage; endMessage.m_Turn = i; endMessage.m_TurnLength = server.m_ServerTurnManager->GetSavedTurnLength(i); session->SendMessage(&endMessage); } } // Tell the turn manager to expect commands from this new client // Special case: the controller shouldn't be treated as an observer in any case. bool isObserver = server.m_PlayerAssignments[session->GetGUID()].m_PlayerID == -1 && server.m_ControllerGUID != session->GetGUID(); server.m_ServerTurnManager->InitialiseClient(session->GetHostID(), readyTurn, isObserver); // Tell the client that everything has finished loading and it should start now CLoadedGameMessage loaded; loaded.m_CurrentTurn = readyTurn; session->SendMessage(&loaded); return true; } bool CNetServerWorker::OnRejoined(void* context, CFsmEvent* event) { // A client has finished rejoining and the loading screen disappeared. ENSURE(event->GetType() == (uint)NMT_REJOINED); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); // Inform everyone of the client having rejoined CRejoinedMessage* message = (CRejoinedMessage*)event->GetParamRef(); message->m_GUID = session->GetGUID(); server.Broadcast(message, { NSS_INGAME }); // Send all pausing players to the rejoined client. for (const CStr& guid : server.m_PausingPlayers) { CClientPausedMessage pausedMessage; pausedMessage.m_GUID = guid; pausedMessage.m_Pause = true; session->SendMessage(&pausedMessage); } return true; } bool CNetServerWorker::OnKickPlayer(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_KICKED); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); if (session->GetGUID() == server.m_ControllerGUID) { CKickedMessage* message = (CKickedMessage*)event->GetParamRef(); server.KickPlayer(message->m_Name, message->m_Ban); } return true; } bool CNetServerWorker::OnDisconnect(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_CONNECTION_LOST); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); server.OnUserLeave(session); return true; } bool CNetServerWorker::OnClientPaused(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_CLIENT_PAUSED); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); CClientPausedMessage* message = (CClientPausedMessage*)event->GetParamRef(); message->m_GUID = session->GetGUID(); // Update the list of pausing players. std::vector::iterator player = std::find(server.m_PausingPlayers.begin(), server.m_PausingPlayers.end(), session->GetGUID()); if (message->m_Pause) { if (player != server.m_PausingPlayers.end()) return true; server.m_PausingPlayers.push_back(session->GetGUID()); } else { if (player == server.m_PausingPlayers.end()) return true; server.m_PausingPlayers.erase(player); } // Send messages to clients that are in game, and are not the client who paused. for (CNetServerSession* netSession : server.m_Sessions) if (netSession->GetCurrState() == NSS_INGAME && message->m_GUID != netSession->GetGUID()) netSession->SendMessage(message); return true; } bool CNetServerWorker::CheckGameLoadStatus(CNetServerSession* changedSession) { for (const CNetServerSession* session : m_Sessions) if (session != changedSession && session->GetCurrState() != NSS_INGAME) return false; // Inform clients that everyone has loaded the map and that the game can start CLoadedGameMessage loaded; loaded.m_CurrentTurn = 0; // Notice the changedSession is still in the NSS_PREGAME state Broadcast(&loaded, { NSS_PREGAME, NSS_INGAME }); m_State = SERVER_STATE_INGAME; return true; } void CNetServerWorker::StartGame(const CStr& initAttribs) { for (std::pair& player : m_PlayerAssignments) if (player.second.m_Enabled && player.second.m_PlayerID != -1 && player.second.m_Status == 0) { LOGERROR("Tried to start the game without player \"%s\" being ready!", utf8_from_wstring(player.second.m_Name).c_str()); return; } m_ServerTurnManager = new CNetServerTurnManager(*this); for (CNetServerSession* session : m_Sessions) { // Special case: the controller shouldn't be treated as an observer in any case. bool isObserver = m_PlayerAssignments[session->GetGUID()].m_PlayerID == -1 && m_ControllerGUID != session->GetGUID(); m_ServerTurnManager->InitialiseClient(session->GetHostID(), 0, isObserver); } m_State = SERVER_STATE_LOADING; // Remove players and observers that are not present when the game starts for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end();) if (it->second.m_Enabled) ++it; else it = m_PlayerAssignments.erase(it); SendPlayerAssignments(); // Update init attributes. They should no longer change. Script::ParseJSON(ScriptRequest(m_ScriptInterface), initAttribs, &m_InitAttributes); CGameStartMessage gameStart; gameStart.m_InitAttributes = initAttribs; Broadcast(&gameStart, { NSS_PREGAME }); } CStrW CNetServerWorker::SanitisePlayerName(const CStrW& original) { const size_t MAX_LENGTH = 32; CStrW name = original; name.Replace(L"[", L"{"); // remove GUI tags name.Replace(L"]", L"}"); // remove for symmetry // Restrict the length if (name.length() > MAX_LENGTH) name = name.Left(MAX_LENGTH); // Don't allow surrounding whitespace name.Trim(PS_TRIM_BOTH); // Don't allow empty name if (name.empty()) name = L"Anonymous"; return name; } CStrW CNetServerWorker::DeduplicatePlayerName(const CStrW& original) { CStrW name = original; // Try names "Foo", "Foo (2)", "Foo (3)", etc size_t id = 2; while (true) { bool unique = true; for (const CNetServerSession* session : m_Sessions) { if (session->GetUserName() == name) { unique = false; break; } } if (unique) return name; name = original + L" (" + CStrW::FromUInt(id++) + L")"; } } void CNetServerWorker::SendHolePunchingMessage(const CStr& ipStr, u16 port) { if (m_Host) StunClient::SendHolePunchingMessages(*m_Host, ipStr, port); } CNetServer::CNetServer(bool useLobbyAuth) : m_Worker(new CNetServerWorker(useLobbyAuth)), m_LobbyAuth(useLobbyAuth), m_UseSTUN(false), m_PublicIp(""), m_PublicPort(20595), m_Password() { } CNetServer::~CNetServer() { delete m_Worker; } bool CNetServer::GetUseSTUN() const { return m_UseSTUN; } bool CNetServer::UseLobbyAuth() const { return m_LobbyAuth; } bool CNetServer::SetupConnection(const u16 port) { return m_Worker->SetupConnection(port); } CStr CNetServer::GetPublicIp() const { return m_PublicIp; } u16 CNetServer::GetPublicPort() const { return m_PublicPort; } u16 CNetServer::GetLocalPort() const { std::lock_guard lock(m_Worker->m_WorkerMutex); if (!m_Worker->m_Host) return 0; return m_Worker->m_Host->address.port; } void CNetServer::SetConnectionData(const CStr& ip, const u16 port) { m_PublicIp = ip; m_PublicPort = port; m_UseSTUN = false; } bool CNetServer::SetConnectionDataViaSTUN() { m_UseSTUN = true; std::lock_guard lock(m_Worker->m_WorkerMutex); if (!m_Worker->m_Host) return false; return StunClient::FindPublicIP(*m_Worker->m_Host, m_PublicIp, m_PublicPort); } bool CNetServer::CheckPasswordAndIncrement(const std::string& username, const std::string& password, const std::string& salt) { std::unordered_map::iterator it = m_FailedAttempts.find(username); if (m_Worker->CheckPassword(password, salt)) { if (it != m_FailedAttempts.end()) it->second = 0; return true; } if (it == m_FailedAttempts.end()) m_FailedAttempts.emplace(username, 1); else it->second++; return false; } bool CNetServer::IsBanned(const std::string& username) const { std::unordered_map::const_iterator it = m_FailedAttempts.find(username); return it != m_FailedAttempts.end() && it->second >= FAILED_PASSWORD_TRIES_BEFORE_BAN; } void CNetServer::SetPassword(const CStr& password) { m_Password = password; std::lock_guard lock(m_Worker->m_WorkerMutex); m_Worker->SetPassword(password); } void CNetServer::SetControllerSecret(const std::string& secret) { std::lock_guard lock(m_Worker->m_WorkerMutex); m_Worker->SetControllerSecret(secret); } void CNetServer::StartGame() { std::lock_guard lock(m_Worker->m_WorkerMutex); m_Worker->m_StartGameQueue.push_back(true); } void CNetServer::UpdateInitAttributes(JS::MutableHandleValue attrs, const ScriptRequest& rq) { // Pass the attributes as JSON, since that's the easiest safe // cross-thread way of passing script data std::string attrsJSON = Script::StringifyJSON(rq, attrs, false); std::lock_guard lock(m_Worker->m_WorkerMutex); m_Worker->m_InitAttributesQueue.push_back(attrsJSON); } void CNetServer::OnLobbyAuth(const CStr& name, const CStr& token) { std::lock_guard lock(m_Worker->m_WorkerMutex); m_Worker->m_LobbyAuthQueue.push_back(std::make_pair(name, token)); } void CNetServer::SetTurnLength(u32 msecs) { std::lock_guard lock(m_Worker->m_WorkerMutex); m_Worker->m_TurnLengthQueue.push_back(msecs); } void CNetServer::SendHolePunchingMessage(const CStr& ip, u16 port) { m_Worker->SendHolePunchingMessage(ip, port); } Index: ps/trunk/source/network/tests/test_FSM.h =================================================================== --- ps/trunk/source/network/tests/test_FSM.h (revision 27782) +++ ps/trunk/source/network/tests/test_FSM.h (revision 27783) @@ -1,163 +1,163 @@ /* Copyright (C) 2023 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 "lib/self_test.h" #include "network/FSM.h" #include class TestFSM : public CxxTest::TestSuite { struct FSMGlobalState { std::array occurCount{0, 0, 0}; }; template - static bool IncrementGlobal(void* state, const CFsmEvent*) + static bool IncrementGlobal(void* state, CFsmEvent*) { ++std::get(reinterpret_cast(state)->occurCount); return true; } static bool IncrementParam(void*, CFsmEvent* event) { ++*reinterpret_cast(event->GetParamRef()); return true; } enum class State : unsigned int { ZERO, ONE, TWO }; enum class Instruction : unsigned int { TO_ZERO, TO_ONE, TO_TWO }; public: void test_global() { FSMGlobalState globalState; CFsm FSMObject; /* Corresponding pseudocode while (true) { // state zero await nextInstruction(); // state one const auto cond = await nextInstruction(); if (cond == instruction::TO_ONE) { //state two await nextInstruction(); } } */ FSMObject.AddTransition(static_cast(State::ZERO), static_cast(Instruction::TO_ONE), static_cast(State::ONE), - reinterpret_cast(&IncrementGlobal<1>), static_cast(&globalState)); + &IncrementGlobal<1>, static_cast(&globalState)); FSMObject.AddTransition(static_cast(State::ONE), static_cast(Instruction::TO_TWO), static_cast(State::TWO), - reinterpret_cast(&IncrementGlobal<2>), static_cast(&globalState)); + &IncrementGlobal<2>, static_cast(&globalState)); FSMObject.AddTransition(static_cast(State::ONE), static_cast(Instruction::TO_ZERO), static_cast(State::ZERO), - reinterpret_cast(&IncrementGlobal<0>), static_cast(&globalState)); + &IncrementGlobal<0>, static_cast(&globalState)); FSMObject.AddTransition(static_cast(State::TWO), static_cast(Instruction::TO_ZERO), static_cast(State::ZERO), - reinterpret_cast(&IncrementGlobal<0>), static_cast(&globalState)); + &IncrementGlobal<0>, static_cast(&globalState)); FSMObject.SetFirstState(static_cast(State::ZERO)); TS_ASSERT(FSMObject.Update(static_cast(Instruction::TO_ONE), nullptr)); TS_ASSERT_EQUALS(std::get<1>(globalState.occurCount), 1); TS_ASSERT_EQUALS(FSMObject.GetCurrState(), static_cast(State::ONE)); TS_ASSERT(FSMObject.Update(static_cast(Instruction::TO_TWO), nullptr)); TS_ASSERT(FSMObject.Update(static_cast(Instruction::TO_ZERO), nullptr)); TS_ASSERT(FSMObject.Update(static_cast(Instruction::TO_ONE), nullptr)); TS_ASSERT(FSMObject.Update(static_cast(Instruction::TO_ZERO), nullptr)); TS_ASSERT(FSMObject.Update(static_cast(Instruction::TO_ONE), nullptr)); TS_ASSERT(FSMObject.Update(static_cast(Instruction::TO_ZERO), nullptr)); TS_ASSERT_EQUALS(std::get<0>(globalState.occurCount), 3); TS_ASSERT_EQUALS(std::get<1>(globalState.occurCount), 3); TS_ASSERT_EQUALS(std::get<2>(globalState.occurCount), 1); // Some transitions do not exist. TS_ASSERT(!FSMObject.Update(static_cast(Instruction::TO_ZERO), nullptr)); TS_ASSERT(!FSMObject.Update(static_cast(Instruction::TO_TWO), nullptr)); TS_ASSERT(FSMObject.Update(static_cast(Instruction::TO_ONE), nullptr)); TS_ASSERT(!FSMObject.Update(static_cast(Instruction::TO_ONE), nullptr)); TS_ASSERT(FSMObject.Update(static_cast(Instruction::TO_TWO), nullptr)); TS_ASSERT(!FSMObject.Update(static_cast(Instruction::TO_TWO), nullptr)); TS_ASSERT_EQUALS(std::get<0>(globalState.occurCount), 3); TS_ASSERT_EQUALS(std::get<1>(globalState.occurCount), 4); TS_ASSERT_EQUALS(std::get<2>(globalState.occurCount), 2); } void test_param() { FSMGlobalState globalState; CFsm FSMObject; // Equal to the FSM in test_global. FSMObject.AddTransition(static_cast(State::ZERO), static_cast(Instruction::TO_ONE), static_cast(State::ONE), - reinterpret_cast(&IncrementParam), nullptr); + &IncrementParam, nullptr); FSMObject.AddTransition(static_cast(State::ONE), static_cast(Instruction::TO_TWO), static_cast(State::TWO), - reinterpret_cast(&IncrementParam), nullptr); + &IncrementParam, nullptr); FSMObject.AddTransition(static_cast(State::ONE), static_cast(Instruction::TO_ZERO), static_cast(State::ZERO), - reinterpret_cast(&IncrementParam), nullptr); + &IncrementParam, nullptr); FSMObject.AddTransition(static_cast(State::TWO), static_cast(Instruction::TO_ZERO), static_cast(State::ZERO), - reinterpret_cast(&IncrementParam), nullptr); + &IncrementParam, nullptr); FSMObject.SetFirstState(static_cast(State::ZERO)); // Some transitions do not exist. TS_ASSERT(!FSMObject.Update(static_cast(Instruction::TO_ZERO), static_cast(&std::get<0>(globalState.occurCount)))); TS_ASSERT(!FSMObject.Update(static_cast(Instruction::TO_TWO), static_cast(&std::get<2>(globalState.occurCount)))); TS_ASSERT(FSMObject.Update(static_cast(Instruction::TO_ONE), static_cast(&std::get<1>(globalState.occurCount)))); TS_ASSERT(!FSMObject.Update(static_cast(Instruction::TO_ONE), static_cast(&std::get<1>(globalState.occurCount)))); TS_ASSERT(FSMObject.Update(static_cast(Instruction::TO_TWO), static_cast(&std::get<2>(globalState.occurCount)))); TS_ASSERT(!FSMObject.Update(static_cast(Instruction::TO_TWO), static_cast(&std::get<2>(globalState.occurCount)))); TS_ASSERT_EQUALS(std::get<0>(globalState.occurCount), 0); TS_ASSERT_EQUALS(std::get<1>(globalState.occurCount), 1); TS_ASSERT_EQUALS(std::get<2>(globalState.occurCount), 1); } };