Index: ps/trunk/source/network/NetServer.cpp =================================================================== --- ps/trunk/source/network/NetServer.cpp (revision 22011) +++ ps/trunk/source/network/NetServer.cpp (revision 22012) @@ -1,1614 +1,1624 @@ -/* Copyright (C) 2018 Wildfire Games. +/* Copyright (C) 2019 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 "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/Profile.h" #include "ps/ThreadUtil.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/ScriptRuntime.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" static const int CHANNEL_COUNT = 1; /** * 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; 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; CJoinSyncStartMessage message; 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, int autostartPlayers) : m_AutostartPlayers(autostartPlayers), m_LobbyAuth(useLobbyAuth), m_Shutdown(false), m_ScriptInterface(NULL), m_NextHostID(1), m_Host(NULL), m_HostGUID(), 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 { CScopeLock lock(m_WorkerMutex); m_Shutdown = true; } // Wait for it to shut down cleanly pthread_join(m_WorkerThread, NULL); } // 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; } 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 = enet_host_create(&addr, MAX_CLIENTS, CHANNEL_COUNT, 0, 0); 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 int ret = pthread_create(&m_WorkerThread, NULL, &RunThread, this); ENSURE(ret == 0); #if CONFIG2_MINIUPNPC // Launch the UPnP thread ret = pthread_create(&m_UPnPThread, NULL, &SetupUPnP, NULL); ENSURE(ret == 0); #endif return true; } #if CONFIG2_MINIUPNPC void* CNetServerWorker::SetupUPnP(void*) { // 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. struct UPNPUrls urls; struct IGDdatas data; struct UPNPDev* devlist = NULL; // 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; bool allocatedUrls = false; // 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."); return NULL; } 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)); return NULL; } 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)); return NULL; } // 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)); return NULL; } 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); // Make sure everything is properly freed. if (allocatedUrls) FreeUPNPUrls(&urls); freeUPNPDevlist(devlist); return NULL; } #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(), session->GetCurrState()) != targetStates.end() && !session->SendMessage(message)) ok = false; return ok; } void* CNetServerWorker::RunThread(void* data) { debug_SetThreadName("NetServer"); static_cast(data)->Run(); return NULL; } void CNetServerWorker::Run() { // The script runtime uses the profiler and therefore the thread must be registered before the runtime is created g_Profiler2.RegisterCurrentThread("Net server"); // To avoid the need for JS_SetContextThread, we create and use and destroy // the script interface entirely within this network thread m_ScriptInterface = new ScriptInterface("Engine", "Net server", ScriptInterface::CreateRuntime(g_ScriptRuntime)); m_GameAttributes.init(m_ScriptInterface->GetJSRuntime(), JS::UndefinedValue()); while (true) { if (!RunStep()) break; // Implement autostart mode if (m_State == SERVER_STATE_PREGAME && (int)m_PlayerAssignments.size() == m_AutostartPlayers) StartGame(); // 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->GetRuntime()->MaybeIncrementalGC(0.5f); JSContext* cx = m_ScriptInterface->GetContext(); JSAutoRequest rq(cx); std::vector newStartGame; std::vector newGameAttributes; std::vector> newLobbyAuths; std::vector newTurnLength; { CScopeLock lock(m_WorkerMutex); if (m_Shutdown) return false; newStartGame.swap(m_StartGameQueue); newGameAttributes.swap(m_GameAttributesQueue); newLobbyAuths.swap(m_LobbyAuthQueue); newTurnLength.swap(m_TurnLengthQueue); } if (!newGameAttributes.empty()) { JS::RootedValue gameAttributesVal(cx); GetScriptInterface().ParseJSON(newGameAttributes.back(), &gameAttributesVal); UpdateGameAttributes(&gameAttributesVal); } if (!newTurnLength.empty()) SetTurnLength(newTurnLength.back()); // Do StartGame last, so we have the most up-to-date game attributes when we start if (!newStartGame.empty()) StartGame(); 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 > DEFAULT_TURN_LENGTH_MP) { 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_LOBBY_AUTHENTICATE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED); session->AddTransition(NSS_LOBBY_AUTHENTICATE, (uint)NMT_AUTHENTICATE, NSS_PREGAME, (void*)&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_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*)&OnStartGame, 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*)&OnInGame, context); - session->AddTransition(NSS_INGAME, (uint)NMT_SYNC_CHECK, NSS_INGAME, (void*)&OnInGame, context); - session->AddTransition(NSS_INGAME, (uint)NMT_END_COMMAND_BATCH, NSS_INGAME, (void*)&OnInGame, 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); // 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()); if (m_HostGUID.empty() && session->IsLocalClient()) m_HostGUID = session->GetGUID(); CGameSetupMessage gameSetupMessage(GetScriptInterface()); gameSetupMessage.m_Data = m_GameAttributes; session->SendMessage(&gameSetupMessage); 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: only for non-observers // 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 i32 playerID = -1; // 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; } } // Otherwise leave the player ID as -1 (observer) and let gamesetup change it as needed. 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_HostGUID) 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_UNKNOWN); 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; } // Either deduplicate or prohibit join if name is in use bool duplicatePlayernames = false; CFG_GET_VAL("network.duplicateplayernames", duplicatePlayernames); if (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; } // TODO: check server password etc? u32 newHostID = server.m_NextHostID++; session->SetUserName(username); session->SetHostID(newHostID); session->SetLocalClient(message->m_IsLocalClient); CAuthenticateResultMessage authenticateResult; authenticateResult.m_Code = isRejoining ? ARC_OK_REJOINING : ARC_OK; authenticateResult.m_HostID = newHostID; authenticateResult.m_Message = L"Logged in"; session->SendMessage(&authenticateResult); server.OnUserJoin(session); if (isRejoining) { // 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); session->SetLongTimeout(true); sourceSession->GetFileTransferer().StartTask( 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(); + JSContext* cx = scriptInterface.GetContext(); + JSAutoRequest rq(cx); + JS::RootedValue settings(cx); + scriptInterface.GetProperty(server.m_GameAttributes, "settings", &settings); + if (scriptInterface.HasProperty(settings, "CheatsEnabled")) + scriptInterface.GetProperty(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::OnInGame(void* context, CFsmEvent* event) +bool CNetServerWorker::OnSyncCheck(void* context, CFsmEvent* event) { - // TODO: should split each of these cases into a separate method + ENSURE(event->GetType() == (uint)NMT_SYNC_CHECK); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); - CNetMessage* message = (CNetMessage*)event->GetParamRef(); - if (message->GetType() == (uint)NMT_SIMULATION_COMMAND) - { - CSimulationMessage* simMessage = static_cast (message); + CSyncCheckMessage* message = (CSyncCheckMessage*)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(); - JSContext* cx = scriptInterface.GetContext(); - JSAutoRequest rq(cx); - JS::RootedValue settings(cx); - scriptInterface.GetProperty(server.m_GameAttributes, "settings", &settings); - if (scriptInterface.HasProperty(settings, "CheatsEnabled")) - scriptInterface.GetProperty(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 != simMessage->m_Player)) - return true; + server.m_ServerTurnManager->NotifyFinishedClientUpdate(*session, message->m_Turn, message->m_Hash); + return true; +} - // Send it back to all clients that have finished - // the loading screen (and the synchronization when rejoining) - server.Broadcast(simMessage, { NSS_INGAME }); +bool CNetServerWorker::OnEndCommandBatch(void* context, CFsmEvent* event) +{ + ENSURE(event->GetType() == (uint)NMT_END_COMMAND_BATCH); - // Save all the received commands - if (server.m_SavedCommands.size() < simMessage->m_Turn + 1) - server.m_SavedCommands.resize(simMessage->m_Turn + 1); - server.m_SavedCommands[simMessage->m_Turn].push_back(*simMessage); + CNetServerSession* session = (CNetServerSession*)context; + CNetServerWorker& server = session->GetServer(); - // TODO: we shouldn't send the message back to the client that first sent it - } - else if (message->GetType() == (uint)NMT_SYNC_CHECK) - { - CSyncCheckMessage* syncMessage = static_cast (message); - server.m_ServerTurnManager->NotifyFinishedClientUpdate(*session, syncMessage->m_Turn, syncMessage->m_Hash); - } - else if (message->GetType() == (uint)NMT_END_COMMAND_BATCH) - { - // The turn-length field is ignored - CEndCommandBatchMessage* endMessage = static_cast (message); - server.m_ServerTurnManager->NotifyFinishedClientCommands(*session, endMessage->m_Turn); - } + 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_HostGUID) 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; if (session->GetGUID() == server.m_HostGUID) { CGameSetupMessage* message = (CGameSetupMessage*)event->GetParamRef(); server.UpdateGameAttributes(&(message->m_Data)); } 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_HostGUID) { CAssignPlayerMessage* message = (CAssignPlayerMessage*)event->GetParamRef(); server.AssignPlayer(message->m_PlayerID, message->m_GUID); } return true; } bool CNetServerWorker::OnStartGame(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_GAME_START); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); if (session->GetGUID() == server.m_HostGUID) server.StartGame(); return true; } bool CNetServerWorker::OnLoadedGame(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_LOADED_GAME); CNetServerSession* loadedSession = (CNetServerSession*)context; CNetServerWorker& server = loadedSession->GetServer(); loadedSession->SetLongTimeout(false); // 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 server.m_ServerTurnManager->InitialiseClient(session->GetHostID(), readyTurn); // 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); } session->SetLongTimeout(false); 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_HostGUID) { 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* session : server.m_Sessions) { if (session->GetCurrState() == NSS_INGAME && message->m_GUID != session->GetGUID()) session->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() { 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) { m_ServerTurnManager->InitialiseClient(session->GetHostID(), 0); // TODO: only for non-observers session->SetLongTimeout(true); } m_State = SERVER_STATE_LOADING; // Send the final setup state to all clients UpdateGameAttributes(&m_GameAttributes); // 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(); CGameStartMessage gameStart; Broadcast(&gameStart, { NSS_PREGAME }); } void CNetServerWorker::UpdateGameAttributes(JS::MutableHandleValue attrs) { m_GameAttributes = attrs; if (!m_Host) return; CGameSetupMessage gameSetupMessage(GetScriptInterface()); gameSetupMessage.m_Data = m_GameAttributes; Broadcast(&gameSetupMessage, { 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) { StunClient::SendHolePunchingMessages(m_Host, ipStr.c_str(), port); } CNetServer::CNetServer(bool useLobbyAuth, int autostartPlayers) : m_Worker(new CNetServerWorker(useLobbyAuth, autostartPlayers)), m_LobbyAuth(useLobbyAuth) { } CNetServer::~CNetServer() { delete m_Worker; } bool CNetServer::UseLobbyAuth() const { return m_LobbyAuth; } bool CNetServer::SetupConnection(const u16 port) { return m_Worker->SetupConnection(port); } void CNetServer::StartGame() { CScopeLock lock(m_Worker->m_WorkerMutex); m_Worker->m_StartGameQueue.push_back(true); } void CNetServer::UpdateGameAttributes(JS::MutableHandleValue attrs, const ScriptInterface& scriptInterface) { // Pass the attributes as JSON, since that's the easiest safe // cross-thread way of passing script data std::string attrsJSON = scriptInterface.StringifyJSON(attrs, false); CScopeLock lock(m_Worker->m_WorkerMutex); m_Worker->m_GameAttributesQueue.push_back(attrsJSON); } void CNetServer::OnLobbyAuth(const CStr& name, const CStr& token) { CScopeLock lock(m_Worker->m_WorkerMutex); m_Worker->m_LobbyAuthQueue.push_back(std::make_pair(name, token)); } void CNetServer::SetTurnLength(u32 msecs) { CScopeLock 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/NetServer.h =================================================================== --- ps/trunk/source/network/NetServer.h (revision 22011) +++ ps/trunk/source/network/NetServer.h (revision 22012) @@ -1,384 +1,386 @@ -/* Copyright (C) 2017 Wildfire Games. +/* Copyright (C) 2019 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 NETSERVER_H #define NETSERVER_H #include "NetFileTransfer.h" #include "NetHost.h" #include "lib/config2.h" #include "lib/types.h" #include "ps/ThreadUtil.h" #include "scriptinterface/ScriptTypes.h" #include #include #include class CNetServerSession; class CNetServerTurnManager; class CFsmEvent; class ScriptInterface; class CPlayerAssignmentMessage; class CNetStatsTable; class CSimulationMessage; class CNetServerWorker; enum NetServerState { // We haven't opened the port yet, we're just setting some stuff up. // The worker thread has not been started. SERVER_STATE_UNCONNECTED, // The server is open and accepting connections. This is the screen where // rules are set up by the operator and where players join and select civs // and stuff. SERVER_STATE_PREGAME, // All the hosts are connected and are loading the game SERVER_STATE_LOADING, // The one with all the killing ;-) SERVER_STATE_INGAME, // The game is over and someone has won. Players might linger to chat or // download the replay log. SERVER_STATE_POSTGAME }; /** * Server session representation of client state */ enum NetServerSessionState { // The client has disconnected or been disconnected NSS_UNCONNECTED, // The client has just connected and we're waiting for its handshake message, // to agree on the protocol version NSS_HANDSHAKE, // The client has handshook and we're waiting for its lobby authentication message NSS_LOBBY_AUTHENTICATE, // The client has handshook and we're waiting for its authentication message, // to find its name and check its password etc NSS_AUTHENTICATE, // The client has fully joined, and is in the pregame setup stage // or is loading the game. // Server must be in SERVER_STATE_PREGAME or SERVER_STATE_LOADING. NSS_PREGAME, // The client has authenticated but the game was already started, // so it's synchronising with the game state from other clients NSS_JOIN_SYNCING, // The client is running the game. // Server must be in SERVER_STATE_LOADING or SERVER_STATE_INGAME. NSS_INGAME }; /** * Network server interface. Handles all the coordination between players. * One person runs this object, and every player (including the host) connects their CNetClient to it. * * The actual work is performed by CNetServerWorker in a separate thread. */ class CNetServer { NONCOPYABLE(CNetServer); public: /** * Construct a new network server. * @param autostartPlayers if positive then StartGame will be called automatically * once this many players are connected (intended for the command-line testing mode). */ CNetServer(bool useLobbyAuth = false, int autostartPlayers = -1); ~CNetServer(); /** * Begin listening for network connections. * This function is synchronous (it won't return until the connection is established). * @return true on success, false on error (e.g. port already in use) */ bool SetupConnection(const u16 port); /** * Call from the GUI to asynchronously notify all clients that they should start loading the game. */ void StartGame(); /** * Call from the GUI to update the game setup attributes. * This must be called at least once before starting the game. * The changes will be asynchronously propagated to all clients. * @param attrs game attributes, in the script context of scriptInterface */ void UpdateGameAttributes(JS::MutableHandleValue attrs, const ScriptInterface& scriptInterface); /** * Set the turn length to a fixed value. * TODO: we should replace this with some adapative lag-dependent computation. */ void SetTurnLength(u32 msecs); bool UseLobbyAuth() const; void OnLobbyAuth(const CStr& name, const CStr& token); void SendHolePunchingMessage(const CStr& ip, u16 port); private: CNetServerWorker* m_Worker; const bool m_LobbyAuth; }; /** * Network server worker thread. * (This is run in a thread so that client/server communication is not delayed * by the host player's framerate - the only delay should be the network latency.) * * Thread-safety: * - SetupConnection and constructor/destructor must be called from the main thread. * - The main thread may push commands onto the Queue members, * while holding the m_WorkerMutex lock. * - Public functions (SendMessage, Broadcast) must be called from the network * server thread. */ class CNetServerWorker { NONCOPYABLE(CNetServerWorker); public: // Public functions for CNetSession/CNetServerTurnManager to use: /** * Send a message to the given network peer. */ bool SendMessage(ENetPeer* peer, const CNetMessage* message); /** * Disconnects a player from gamesetup or session. */ void KickPlayer(const CStrW& playerName, const bool ban); /** * Send a message to all clients who match one of the given states. */ bool Broadcast(const CNetMessage* message, const std::vector& targetStates); private: friend class CNetServer; friend class CNetFileReceiveTask_ServerRejoin; CNetServerWorker(bool useLobbyAuth, int autostartPlayers); ~CNetServerWorker(); /** * Begin listening for network connections. * @return true on success, false on error (e.g. port already in use) */ bool SetupConnection(const u16 port); /** * Call from the GUI to update the player assignments. * The given GUID will be (re)assigned to the given player ID. * Any player currently using that ID will be unassigned. * The changes will be propagated to all clients. */ void AssignPlayer(int playerID, const CStr& guid); /** * Call from the GUI to notify all clients that they should start loading the game. */ void StartGame(); /** * Call from the GUI to update the game setup attributes. * This must be called at least once before starting the game. * The changes will be propagated to all clients. * @param attrs game attributes, in the script context of GetScriptInterface() */ void UpdateGameAttributes(JS::MutableHandleValue attrs); /** * Make a player name 'nicer' by limiting the length and removing forbidden characters etc. */ static CStrW SanitisePlayerName(const CStrW& original); /** * Make a player name unique, if it matches any existing session's name. */ CStrW DeduplicatePlayerName(const CStrW& original); /** * Get the script context used for game attributes. */ const ScriptInterface& GetScriptInterface(); /** * Set the turn length to a fixed value. * TODO: we should replace this with some adaptive lag-dependent computation. */ void SetTurnLength(u32 msecs); void ProcessLobbyAuth(const CStr& name, const CStr& token); void AddPlayer(const CStr& guid, const CStrW& name); void RemovePlayer(const CStr& guid); void SendPlayerAssignments(); void ClearAllPlayerReady(); void SetupSession(CNetServerSession* session); bool HandleConnect(CNetServerSession* session); void OnUserJoin(CNetServerSession* session); void OnUserLeave(CNetServerSession* session); static bool OnClientHandshake(void* context, CFsmEvent* event); static bool OnAuthenticate(void* context, CFsmEvent* event); - static bool OnInGame(void* context, CFsmEvent* event); + static bool OnSimulationCommand(void* context, CFsmEvent* event); + static bool OnSyncCheck(void* context, CFsmEvent* event); + static bool OnEndCommandBatch(void* context, CFsmEvent* event); static bool OnChat(void* context, CFsmEvent* event); static bool OnReady(void* context, CFsmEvent* event); static bool OnClearAllReady(void* context, CFsmEvent* event); static bool OnGameSetup(void* context, CFsmEvent* event); static bool OnAssignPlayer(void* context, CFsmEvent* event); static bool OnStartGame(void* context, CFsmEvent* event); static bool OnLoadedGame(void* context, CFsmEvent* event); static bool OnJoinSyncingLoadedGame(void* context, CFsmEvent* event); static bool OnRejoined(void* context, CFsmEvent* event); static bool OnKickPlayer(void* context, CFsmEvent* event); static bool OnDisconnect(void* context, CFsmEvent* event); static bool OnClientPaused(void* context, CFsmEvent* event); /** * Checks if all clients have finished loading. * If so informs the clients about that and change the server state. * * Returns if all clients finished loading. */ bool CheckGameLoadStatus(CNetServerSession* changedSession); void ConstructPlayerAssignmentMessage(CPlayerAssignmentMessage& message); void HandleMessageReceive(const CNetMessage* message, CNetServerSession* session); /** * Send a network warning if the connection to a client is being lost or has bad latency. */ void CheckClientConnections(); void SendHolePunchingMessage(const CStr& ip, u16 port); /** * Internal script context for (de)serializing script messages, * and for storing game attributes. * (TODO: we shouldn't bother deserializing (except for debug printing of messages), * we should just forward messages blindly and efficiently.) */ ScriptInterface* m_ScriptInterface; PlayerAssignmentMap m_PlayerAssignments; /** * Stores the most current game attributes. */ JS::PersistentRootedValue m_GameAttributes; int m_AutostartPlayers; /** * Whether this match requires lobby authentication. */ const bool m_LobbyAuth; ENetHost* m_Host; std::vector m_Sessions; CNetStatsTable* m_Stats; NetServerState m_State; CStrW m_ServerName; std::vector m_BannedIPs; std::vector m_BannedPlayers; /** * Holds the GUIDs of all currently paused players. */ std::vector m_PausingPlayers; u32 m_NextHostID; CNetServerTurnManager* m_ServerTurnManager; CStr m_HostGUID; /** * A copy of all simulation commands received so far, indexed by * turn number, to simplify support for rejoining etc. * TODO: verify this doesn't use too much RAM. */ std::vector> m_SavedCommands; /** * The latest copy of the simulation state, received from an existing * client when a new client has asked to rejoin the game. */ std::string m_JoinSyncFile; /** * Time when the clients connections were last checked for timeouts and latency. */ std::time_t m_LastConnectionCheck; private: // Thread-related stuff: #if CONFIG2_MINIUPNPC /** * Try to find a UPnP root on the network and setup port forwarding. */ static void* SetupUPnP(void*); pthread_t m_UPnPThread; #endif static void* RunThread(void* data); void Run(); bool RunStep(); pthread_t m_WorkerThread; CMutex m_WorkerMutex; // protected by m_WorkerMutex bool m_Shutdown; // Queues for messages sent by the game thread (protected by m_WorkerMutex): std::vector m_StartGameQueue; std::vector m_GameAttributesQueue; std::vector> m_LobbyAuthQueue; std::vector m_TurnLengthQueue; }; /// Global network server for the standard game extern CNetServer *g_NetServer; #endif // NETSERVER_H