Darker AO for the stone quary
[0ad.git] / source / network / NetServer.cpp
blob23dd146dcdf1376b5d6c522345ea89dc4d584fca
1 /* Copyright (C) 2021 Wildfire Games.
2 * This file is part of 0 A.D.
4 * 0 A.D. is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 2 of the License, or
7 * (at your option) any later version.
9 * 0 A.D. is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
18 #include "precompiled.h"
20 #include "NetServer.h"
22 #include "NetClient.h"
23 #include "NetMessage.h"
24 #include "NetSession.h"
25 #include "NetServerTurnManager.h"
26 #include "NetStats.h"
28 #include "lib/external_libraries/enet.h"
29 #include "lib/types.h"
30 #include "network/StunClient.h"
31 #include "ps/CLogger.h"
32 #include "ps/ConfigDB.h"
33 #include "ps/GUID.h"
34 #include "ps/Hashing.h"
35 #include "ps/Profile.h"
36 #include "ps/Threading.h"
37 #include "scriptinterface/ScriptContext.h"
38 #include "scriptinterface/ScriptInterface.h"
39 #include "scriptinterface/JSON.h"
40 #include "simulation2/Simulation2.h"
41 #include "simulation2/system/TurnManager.h"
43 #if CONFIG2_MINIUPNPC
44 #include <miniupnpc/miniwget.h>
45 #include <miniupnpc/miniupnpc.h>
46 #include <miniupnpc/upnpcommands.h>
47 #include <miniupnpc/upnperrors.h>
48 #endif
50 #include <string>
52 /**
53 * Number of peers to allocate for the enet host.
54 * Limited by ENET_PROTOCOL_MAXIMUM_PEER_ID (4096).
56 * At most 8 players, 32 observers and 1 temporary connection to send the "server full" disconnect-reason.
58 #define MAX_CLIENTS 41
60 #define DEFAULT_SERVER_NAME L"Unnamed Server"
62 constexpr int CHANNEL_COUNT = 1;
63 constexpr int FAILED_PASSWORD_TRIES_BEFORE_BAN = 3;
65 /**
66 * enet_host_service timeout (msecs).
67 * Smaller numbers may hurt performance; larger numbers will
68 * hurt latency responding to messages from game thread.
70 static const int HOST_SERVICE_TIMEOUT = 50;
72 /**
73 * Once ping goes above turn length * command delay,
74 * the game will start 'freezing' for other clients while we catch up.
75 * Since commands are sent client -> server -> client, divide by 2.
76 * (duplicated in NetServer.cpp to avoid having to fetch the constants in a header file)
78 constexpr u32 NETWORK_BAD_PING = DEFAULT_TURN_LENGTH * COMMAND_DELAY_MP / 2;
80 CNetServer* g_NetServer = NULL;
82 static CStr DebugName(CNetServerSession* session)
84 if (session == NULL)
85 return "[unknown host]";
86 if (session->GetGUID().empty())
87 return "[unauthed host]";
88 return "[" + session->GetGUID().substr(0, 8) + "...]";
91 /**
92 * Async task for receiving the initial game state to be forwarded to another
93 * client that is rejoining an in-progress network game.
95 class CNetFileReceiveTask_ServerRejoin : public CNetFileReceiveTask
97 NONCOPYABLE(CNetFileReceiveTask_ServerRejoin);
98 public:
99 CNetFileReceiveTask_ServerRejoin(CNetServerWorker& server, u32 hostID)
100 : m_Server(server), m_RejoinerHostID(hostID)
104 virtual void OnComplete()
106 // We've received the game state from an existing player - now
107 // we need to send it onwards to the newly rejoining player
109 // Find the session corresponding to the rejoining host (if any)
110 CNetServerSession* session = NULL;
111 for (CNetServerSession* serverSession : m_Server.m_Sessions)
113 if (serverSession->GetHostID() == m_RejoinerHostID)
115 session = serverSession;
116 break;
120 if (!session)
122 LOGMESSAGE("Net server: rejoining client disconnected before we sent to it");
123 return;
126 // Store the received state file, and tell the client to start downloading it from us
127 // TODO: this will get kind of confused if there's multiple clients downloading in parallel;
128 // they'll race and get whichever happens to be the latest received by the server,
129 // which should still work but isn't great
130 m_Server.m_JoinSyncFile = m_Buffer;
132 // Send the init attributes alongside - these should be correct since the game should be started.
133 CJoinSyncStartMessage message;
134 message.m_InitAttributes = Script::StringifyJSON(ScriptRequest(m_Server.GetScriptInterface()), &m_Server.m_InitAttributes);
135 session->SendMessage(&message);
138 private:
139 CNetServerWorker& m_Server;
140 u32 m_RejoinerHostID;
144 * XXX: We use some non-threadsafe functions from the worker thread.
145 * See http://trac.wildfiregames.com/ticket/654
148 CNetServerWorker::CNetServerWorker(bool useLobbyAuth, int autostartPlayers) :
149 m_AutostartPlayers(autostartPlayers),
150 m_LobbyAuth(useLobbyAuth),
151 m_Shutdown(false),
152 m_ScriptInterface(NULL),
153 m_NextHostID(1), m_Host(NULL), m_ControllerGUID(), m_Stats(NULL),
154 m_LastConnectionCheck(0)
156 m_State = SERVER_STATE_UNCONNECTED;
158 m_ServerTurnManager = NULL;
160 m_ServerName = DEFAULT_SERVER_NAME;
163 CNetServerWorker::~CNetServerWorker()
165 if (m_State != SERVER_STATE_UNCONNECTED)
167 // Tell the thread to shut down
169 std::lock_guard<std::mutex> lock(m_WorkerMutex);
170 m_Shutdown = true;
173 // Wait for it to shut down cleanly
174 m_WorkerThread.join();
177 #if CONFIG2_MINIUPNPC
178 if (m_UPnPThread.joinable())
179 m_UPnPThread.detach();
180 #endif
182 // Clean up resources
184 delete m_Stats;
186 for (CNetServerSession* session : m_Sessions)
188 session->DisconnectNow(NDR_SERVER_SHUTDOWN);
189 delete session;
192 if (m_Host)
193 enet_host_destroy(m_Host);
195 delete m_ServerTurnManager;
198 void CNetServerWorker::SetPassword(const CStr& hashedPassword)
200 m_Password = hashedPassword;
204 void CNetServerWorker::SetControllerSecret(const std::string& secret)
206 m_ControllerSecret = secret;
210 bool CNetServerWorker::CheckPassword(const std::string& password, const std::string& salt) const
212 return HashCryptographically(m_Password, salt) == password;
216 bool CNetServerWorker::SetupConnection(const u16 port)
218 ENSURE(m_State == SERVER_STATE_UNCONNECTED);
219 ENSURE(!m_Host);
221 // Bind to default host
222 ENetAddress addr;
223 addr.host = ENET_HOST_ANY;
224 addr.port = port;
226 // Create ENet server
227 m_Host = enet_host_create(&addr, MAX_CLIENTS, CHANNEL_COUNT, 0, 0);
228 if (!m_Host)
230 LOGERROR("Net server: enet_host_create failed");
231 return false;
234 m_Stats = new CNetStatsTable();
235 if (CProfileViewer::IsInitialised())
236 g_ProfileViewer.AddRootTable(m_Stats);
238 m_State = SERVER_STATE_PREGAME;
240 // Launch the worker thread
241 m_WorkerThread = std::thread(Threading::HandleExceptions<RunThread>::Wrapper, this);
243 #if CONFIG2_MINIUPNPC
244 // Launch the UPnP thread
245 m_UPnPThread = std::thread(Threading::HandleExceptions<SetupUPnP>::Wrapper);
246 #endif
248 return true;
251 #if CONFIG2_MINIUPNPC
252 void CNetServerWorker::SetupUPnP()
254 debug_SetThreadName("UPnP");
256 // Values we want to set.
257 char psPort[6];
258 sprintf_s(psPort, ARRAY_SIZE(psPort), "%d", PS_DEFAULT_PORT);
259 const char* leaseDuration = "0"; // Indefinite/permanent lease duration.
260 const char* description = "0AD Multiplayer";
261 const char* protocall = "UDP";
262 char internalIPAddress[64];
263 char externalIPAddress[40];
265 // Variables to hold the values that actually get set.
266 char intClient[40];
267 char intPort[6];
268 char duration[16];
270 // Intermediate variables.
271 bool allocatedUrls = false;
272 struct UPNPUrls urls;
273 struct IGDdatas data;
274 struct UPNPDev* devlist = NULL;
276 // Make sure everything is properly freed.
277 std::function<void()> freeUPnP = [&allocatedUrls, &urls, &devlist]()
279 if (allocatedUrls)
280 FreeUPNPUrls(&urls);
281 freeUPNPDevlist(devlist);
282 // IGDdatas does not need to be freed according to UPNP_GetIGDFromUrl
285 // Cached root descriptor URL.
286 std::string rootDescURL;
287 CFG_GET_VAL("network.upnprootdescurl", rootDescURL);
288 if (!rootDescURL.empty())
289 LOGMESSAGE("Net server: attempting to use cached root descriptor URL: %s", rootDescURL.c_str());
291 int ret = 0;
293 // Try a cached URL first
294 if (!rootDescURL.empty() && UPNP_GetIGDFromUrl(rootDescURL.c_str(), &urls, &data, internalIPAddress, sizeof(internalIPAddress)))
296 LOGMESSAGE("Net server: using cached IGD = %s", urls.controlURL);
297 ret = 1;
299 // No cached URL, or it did not respond. Try getting a valid UPnP device for 10 seconds.
300 #if defined(MINIUPNPC_API_VERSION) && MINIUPNPC_API_VERSION >= 14
301 else if ((devlist = upnpDiscover(10000, 0, 0, 0, 0, 2, 0)) != NULL)
302 #else
303 else if ((devlist = upnpDiscover(10000, 0, 0, 0, 0, 0)) != NULL)
304 #endif
306 ret = UPNP_GetValidIGD(devlist, &urls, &data, internalIPAddress, sizeof(internalIPAddress));
307 allocatedUrls = ret != 0; // urls is allocated on non-zero return values
309 else
311 LOGMESSAGE("Net server: upnpDiscover failed and no working cached URL.");
312 freeUPnP();
313 return;
316 switch (ret)
318 case 0:
319 LOGMESSAGE("Net server: No IGD found");
320 break;
321 case 1:
322 LOGMESSAGE("Net server: found valid IGD = %s", urls.controlURL);
323 break;
324 case 2:
325 LOGMESSAGE("Net server: found a valid, not connected IGD = %s, will try to continue anyway", urls.controlURL);
326 break;
327 case 3:
328 LOGMESSAGE("Net server: found a UPnP device unrecognized as IGD = %s, will try to continue anyway", urls.controlURL);
329 break;
330 default:
331 debug_warn(L"Unrecognized return value from UPNP_GetValidIGD");
334 // Try getting our external/internet facing IP. TODO: Display this on the game-setup page for conviniance.
335 ret = UPNP_GetExternalIPAddress(urls.controlURL, data.first.servicetype, externalIPAddress);
336 if (ret != UPNPCOMMAND_SUCCESS)
338 LOGMESSAGE("Net server: GetExternalIPAddress failed with code %d (%s)", ret, strupnperror(ret));
339 freeUPnP();
340 return;
342 LOGMESSAGE("Net server: ExternalIPAddress = %s", externalIPAddress);
344 // Try to setup port forwarding.
345 ret = UPNP_AddPortMapping(urls.controlURL, data.first.servicetype, psPort, psPort,
346 internalIPAddress, description, protocall, 0, leaseDuration);
347 if (ret != UPNPCOMMAND_SUCCESS)
349 LOGMESSAGE("Net server: AddPortMapping(%s, %s, %s) failed with code %d (%s)",
350 psPort, psPort, internalIPAddress, ret, strupnperror(ret));
351 freeUPnP();
352 return;
355 // Check that the port was actually forwarded.
356 ret = UPNP_GetSpecificPortMappingEntry(urls.controlURL,
357 data.first.servicetype,
358 psPort, protocall,
359 #if defined(MINIUPNPC_API_VERSION) && MINIUPNPC_API_VERSION >= 10
360 NULL/*remoteHost*/,
361 #endif
362 intClient, intPort, NULL/*desc*/,
363 NULL/*enabled*/, duration);
365 if (ret != UPNPCOMMAND_SUCCESS)
367 LOGMESSAGE("Net server: GetSpecificPortMappingEntry() failed with code %d (%s)", ret, strupnperror(ret));
368 freeUPnP();
369 return;
372 LOGMESSAGE("Net server: External %s:%s %s is redirected to internal %s:%s (duration=%s)",
373 externalIPAddress, psPort, protocall, intClient, intPort, duration);
375 // Cache root descriptor URL to try to avoid discovery next time.
376 g_ConfigDB.SetValueString(CFG_USER, "network.upnprootdescurl", urls.controlURL);
377 g_ConfigDB.WriteValueToFile(CFG_USER, "network.upnprootdescurl", urls.controlURL);
378 LOGMESSAGE("Net server: cached UPnP root descriptor URL as %s", urls.controlURL);
380 freeUPnP();
382 #endif // CONFIG2_MINIUPNPC
384 bool CNetServerWorker::SendMessage(ENetPeer* peer, const CNetMessage* message)
386 ENSURE(m_Host);
388 CNetServerSession* session = static_cast<CNetServerSession*>(peer->data);
390 return CNetHost::SendMessage(message, peer, DebugName(session).c_str());
393 bool CNetServerWorker::Broadcast(const CNetMessage* message, const std::vector<NetServerSessionState>& targetStates)
395 ENSURE(m_Host);
397 bool ok = true;
399 // TODO: this does lots of repeated message serialisation if we have lots
400 // of remote peers; could do it more efficiently if that's a real problem
402 for (CNetServerSession* session : m_Sessions)
403 if (std::find(targetStates.begin(), targetStates.end(), static_cast<NetServerSessionState>(session->GetCurrState())) != targetStates.end() &&
404 !session->SendMessage(message))
405 ok = false;
407 return ok;
410 void CNetServerWorker::RunThread(CNetServerWorker* data)
412 debug_SetThreadName("NetServer");
414 data->Run();
417 void CNetServerWorker::Run()
419 // The script context uses the profiler and therefore the thread must be registered before the context is created
420 g_Profiler2.RegisterCurrentThread("Net server");
422 // We create a new ScriptContext for this network thread, with a single ScriptInterface.
423 shared_ptr<ScriptContext> netServerContext = ScriptContext::CreateContext();
424 m_ScriptInterface = new ScriptInterface("Engine", "Net server", netServerContext);
425 m_InitAttributes.init(m_ScriptInterface->GetGeneralJSContext(), JS::UndefinedValue());
427 while (true)
429 if (!RunStep())
430 break;
432 // Implement autostart mode
433 if (m_State == SERVER_STATE_PREGAME && (int)m_PlayerAssignments.size() == m_AutostartPlayers)
434 StartGame(Script::StringifyJSON(ScriptRequest(m_ScriptInterface), &m_InitAttributes));
436 // Update profiler stats
437 m_Stats->LatchHostState(m_Host);
440 // Clear roots before deleting their context
441 m_SavedCommands.clear();
443 SAFE_DELETE(m_ScriptInterface);
446 bool CNetServerWorker::RunStep()
448 // Check for messages from the game thread.
449 // (Do as little work as possible while the mutex is held open,
450 // to avoid performance problems and deadlocks.)
452 m_ScriptInterface->GetContext()->MaybeIncrementalGC(0.5f);
454 ScriptRequest rq(m_ScriptInterface);
456 std::vector<bool> newStartGame;
457 std::vector<std::string> newGameAttributes;
458 std::vector<std::pair<CStr, CStr>> newLobbyAuths;
459 std::vector<u32> newTurnLength;
462 std::lock_guard<std::mutex> lock(m_WorkerMutex);
464 if (m_Shutdown)
465 return false;
467 newStartGame.swap(m_StartGameQueue);
468 newGameAttributes.swap(m_InitAttributesQueue);
469 newLobbyAuths.swap(m_LobbyAuthQueue);
470 newTurnLength.swap(m_TurnLengthQueue);
473 if (!newGameAttributes.empty())
475 if (m_State != SERVER_STATE_UNCONNECTED && m_State != SERVER_STATE_PREGAME)
476 LOGERROR("NetServer: Init Attributes cannot be changed after the server starts loading.");
477 else
479 JS::RootedValue gameAttributesVal(rq.cx);
480 Script::ParseJSON(rq, newGameAttributes.back(), &gameAttributesVal);
481 m_InitAttributes = gameAttributesVal;
485 if (!newTurnLength.empty())
486 SetTurnLength(newTurnLength.back());
488 while (!newLobbyAuths.empty())
490 const std::pair<CStr, CStr>& auth = newLobbyAuths.back();
491 ProcessLobbyAuth(auth.first, auth.second);
492 newLobbyAuths.pop_back();
495 // Perform file transfers
496 for (CNetServerSession* session : m_Sessions)
497 session->GetFileTransferer().Poll();
499 CheckClientConnections();
501 // Process network events:
503 ENetEvent event;
504 int status = enet_host_service(m_Host, &event, HOST_SERVICE_TIMEOUT);
505 if (status < 0)
507 LOGERROR("CNetServerWorker: enet_host_service failed (%d)", status);
508 // TODO: notify game that the server has shut down
509 return false;
512 if (status == 0)
514 // Reached timeout with no events - try again
515 return true;
518 // Process the event:
520 switch (event.type)
522 case ENET_EVENT_TYPE_CONNECT:
524 // Report the client address
525 char hostname[256] = "(error)";
526 enet_address_get_host_ip(&event.peer->address, hostname, ARRAY_SIZE(hostname));
527 LOGMESSAGE("Net server: Received connection from %s:%u", hostname, (unsigned int)event.peer->address.port);
529 // Set up a session object for this peer
531 CNetServerSession* session = new CNetServerSession(*this, event.peer);
533 m_Sessions.push_back(session);
535 SetupSession(session);
537 ENSURE(event.peer->data == NULL);
538 event.peer->data = session;
540 HandleConnect(session);
542 break;
545 case ENET_EVENT_TYPE_DISCONNECT:
547 // If there is an active session with this peer, then reset and delete it
549 CNetServerSession* session = static_cast<CNetServerSession*>(event.peer->data);
550 if (session)
552 LOGMESSAGE("Net server: Disconnected %s", DebugName(session).c_str());
554 // Remove the session first, so we won't send player-update messages to it
555 // when updating the FSM
556 m_Sessions.erase(remove(m_Sessions.begin(), m_Sessions.end(), session), m_Sessions.end());
558 session->Update((uint)NMT_CONNECTION_LOST, NULL);
560 delete session;
561 event.peer->data = NULL;
564 if (m_State == SERVER_STATE_LOADING)
565 CheckGameLoadStatus(NULL);
567 break;
570 case ENET_EVENT_TYPE_RECEIVE:
572 // If there is an active session with this peer, then process the message
574 CNetServerSession* session = static_cast<CNetServerSession*>(event.peer->data);
575 if (session)
577 // Create message from raw data
578 CNetMessage* msg = CNetMessageFactory::CreateMessage(event.packet->data, event.packet->dataLength, GetScriptInterface());
579 if (msg)
581 LOGMESSAGE("Net server: Received message %s of size %lu from %s", msg->ToString().c_str(), (unsigned long)msg->GetSerializedLength(), DebugName(session).c_str());
583 HandleMessageReceive(msg, session);
585 delete msg;
589 // Done using the packet
590 enet_packet_destroy(event.packet);
592 break;
595 case ENET_EVENT_TYPE_NONE:
596 break;
599 return true;
602 void CNetServerWorker::CheckClientConnections()
604 // Send messages at most once per second
605 std::time_t now = std::time(nullptr);
606 if (now <= m_LastConnectionCheck)
607 return;
609 m_LastConnectionCheck = now;
611 for (size_t i = 0; i < m_Sessions.size(); ++i)
613 u32 lastReceived = m_Sessions[i]->GetLastReceivedTime();
614 u32 meanRTT = m_Sessions[i]->GetMeanRTT();
616 CNetMessage* message = nullptr;
618 // Report if we didn't hear from the client since few seconds
619 if (lastReceived > NETWORK_WARNING_TIMEOUT)
621 CClientTimeoutMessage* msg = new CClientTimeoutMessage();
622 msg->m_GUID = m_Sessions[i]->GetGUID();
623 msg->m_LastReceivedTime = lastReceived;
624 message = msg;
626 // Report if the client has bad ping
627 else if (meanRTT > NETWORK_BAD_PING)
629 CClientPerformanceMessage* msg = new CClientPerformanceMessage();
630 CClientPerformanceMessage::S_m_Clients client;
631 client.m_GUID = m_Sessions[i]->GetGUID();
632 client.m_MeanRTT = meanRTT;
633 msg->m_Clients.push_back(client);
634 message = msg;
637 // Send to all clients except the affected one
638 // (since that will show the locally triggered warning instead).
639 // Also send it to clients that finished the loading screen while
640 // the game is still waiting for other clients to finish the loading screen.
641 if (message)
642 for (size_t j = 0; j < m_Sessions.size(); ++j)
644 if (i != j && (
645 (m_Sessions[j]->GetCurrState() == NSS_PREGAME && m_State == SERVER_STATE_PREGAME) ||
646 m_Sessions[j]->GetCurrState() == NSS_INGAME))
648 m_Sessions[j]->SendMessage(message);
652 SAFE_DELETE(message);
656 void CNetServerWorker::HandleMessageReceive(const CNetMessage* message, CNetServerSession* session)
658 // Handle non-FSM messages first
659 Status status = session->GetFileTransferer().HandleMessageReceive(*message);
660 if (status != INFO::SKIPPED)
661 return;
663 if (message->GetType() == NMT_FILE_TRANSFER_REQUEST)
665 CFileTransferRequestMessage* reqMessage = (CFileTransferRequestMessage*)message;
667 // Rejoining client got our JoinSyncStart after we received the state from
668 // another client, and has now requested that we forward it to them
670 ENSURE(!m_JoinSyncFile.empty());
671 session->GetFileTransferer().StartResponse(reqMessage->m_RequestID, m_JoinSyncFile);
673 return;
676 // Update FSM
677 if (!session->Update(message->GetType(), (void*)message))
678 LOGERROR("Net server: Error running FSM update (type=%d state=%d)", (int)message->GetType(), (int)session->GetCurrState());
681 void CNetServerWorker::SetupSession(CNetServerSession* session)
683 void* context = session;
685 // Set up transitions for session
687 session->AddTransition(NSS_UNCONNECTED, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
689 session->AddTransition(NSS_HANDSHAKE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
690 session->AddTransition(NSS_HANDSHAKE, (uint)NMT_CLIENT_HANDSHAKE, NSS_AUTHENTICATE, (void*)&OnClientHandshake, context);
692 session->AddTransition(NSS_LOBBY_AUTHENTICATE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
693 session->AddTransition(NSS_LOBBY_AUTHENTICATE, (uint)NMT_AUTHENTICATE, NSS_PREGAME, (void*)&OnAuthenticate, context);
695 session->AddTransition(NSS_AUTHENTICATE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
696 session->AddTransition(NSS_AUTHENTICATE, (uint)NMT_AUTHENTICATE, NSS_PREGAME, (void*)&OnAuthenticate, context);
698 session->AddTransition(NSS_PREGAME, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, (void*)&OnDisconnect, context);
699 session->AddTransition(NSS_PREGAME, (uint)NMT_CHAT, NSS_PREGAME, (void*)&OnChat, context);
700 session->AddTransition(NSS_PREGAME, (uint)NMT_READY, NSS_PREGAME, (void*)&OnReady, context);
701 session->AddTransition(NSS_PREGAME, (uint)NMT_CLEAR_ALL_READY, NSS_PREGAME, (void*)&OnClearAllReady, context);
702 session->AddTransition(NSS_PREGAME, (uint)NMT_GAME_SETUP, NSS_PREGAME, (void*)&OnGameSetup, context);
703 session->AddTransition(NSS_PREGAME, (uint)NMT_ASSIGN_PLAYER, NSS_PREGAME, (void*)&OnAssignPlayer, context);
704 session->AddTransition(NSS_PREGAME, (uint)NMT_KICKED, NSS_PREGAME, (void*)&OnKickPlayer, context);
705 session->AddTransition(NSS_PREGAME, (uint)NMT_GAME_START, NSS_PREGAME, (void*)&OnGameStart, context);
706 session->AddTransition(NSS_PREGAME, (uint)NMT_LOADED_GAME, NSS_INGAME, (void*)&OnLoadedGame, context);
708 session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_KICKED, NSS_JOIN_SYNCING, (void*)&OnKickPlayer, context);
709 session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, (void*)&OnDisconnect, context);
710 session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_LOADED_GAME, NSS_INGAME, (void*)&OnJoinSyncingLoadedGame, context);
712 session->AddTransition(NSS_INGAME, (uint)NMT_REJOINED, NSS_INGAME, (void*)&OnRejoined, context);
713 session->AddTransition(NSS_INGAME, (uint)NMT_KICKED, NSS_INGAME, (void*)&OnKickPlayer, context);
714 session->AddTransition(NSS_INGAME, (uint)NMT_CLIENT_PAUSED, NSS_INGAME, (void*)&OnClientPaused, context);
715 session->AddTransition(NSS_INGAME, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, (void*)&OnDisconnect, context);
716 session->AddTransition(NSS_INGAME, (uint)NMT_CHAT, NSS_INGAME, (void*)&OnChat, context);
717 session->AddTransition(NSS_INGAME, (uint)NMT_SIMULATION_COMMAND, NSS_INGAME, (void*)&OnSimulationCommand, context);
718 session->AddTransition(NSS_INGAME, (uint)NMT_SYNC_CHECK, NSS_INGAME, (void*)&OnSyncCheck, context);
719 session->AddTransition(NSS_INGAME, (uint)NMT_END_COMMAND_BATCH, NSS_INGAME, (void*)&OnEndCommandBatch, context);
721 // Set first state
722 session->SetFirstState(NSS_HANDSHAKE);
725 bool CNetServerWorker::HandleConnect(CNetServerSession* session)
727 if (std::find(m_BannedIPs.begin(), m_BannedIPs.end(), session->GetIPAddress()) != m_BannedIPs.end())
729 session->Disconnect(NDR_BANNED);
730 return false;
733 CSrvHandshakeMessage handshake;
734 handshake.m_Magic = PS_PROTOCOL_MAGIC;
735 handshake.m_ProtocolVersion = PS_PROTOCOL_VERSION;
736 handshake.m_SoftwareVersion = PS_PROTOCOL_VERSION;
737 return session->SendMessage(&handshake);
740 void CNetServerWorker::OnUserJoin(CNetServerSession* session)
742 AddPlayer(session->GetGUID(), session->GetUserName());
744 CPlayerAssignmentMessage assignMessage;
745 ConstructPlayerAssignmentMessage(assignMessage);
746 session->SendMessage(&assignMessage);
749 void CNetServerWorker::OnUserLeave(CNetServerSession* session)
751 std::vector<CStr>::iterator pausing = std::find(m_PausingPlayers.begin(), m_PausingPlayers.end(), session->GetGUID());
752 if (pausing != m_PausingPlayers.end())
753 m_PausingPlayers.erase(pausing);
755 RemovePlayer(session->GetGUID());
757 if (m_ServerTurnManager && session->GetCurrState() != NSS_JOIN_SYNCING)
758 m_ServerTurnManager->UninitialiseClient(session->GetHostID());
760 // TODO: ought to switch the player controlled by that client
761 // back to AI control, or something?
764 void CNetServerWorker::AddPlayer(const CStr& guid, const CStrW& name)
766 // Find all player IDs in active use; we mustn't give them to a second player (excluding the unassigned ID: -1)
767 std::set<i32> usedIDs;
768 for (const std::pair<const CStr, PlayerAssignment>& p : m_PlayerAssignments)
769 if (p.second.m_Enabled && p.second.m_PlayerID != -1)
770 usedIDs.insert(p.second.m_PlayerID);
772 // If the player is rejoining after disconnecting, try to give them
773 // back their old player ID
775 i32 playerID = -1;
777 // Try to match GUID first
778 for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it)
780 if (!it->second.m_Enabled && it->first == guid && usedIDs.find(it->second.m_PlayerID) == usedIDs.end())
782 playerID = it->second.m_PlayerID;
783 m_PlayerAssignments.erase(it); // delete the old mapping, since we've got a new one now
784 goto found;
788 // Try to match username next
789 for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it)
791 if (!it->second.m_Enabled && it->second.m_Name == name && usedIDs.find(it->second.m_PlayerID) == usedIDs.end())
793 playerID = it->second.m_PlayerID;
794 m_PlayerAssignments.erase(it); // delete the old mapping, since we've got a new one now
795 goto found;
799 // Otherwise leave the player ID as -1 (observer) and let gamesetup change it as needed.
801 found:
802 PlayerAssignment assignment;
803 assignment.m_Enabled = true;
804 assignment.m_Name = name;
805 assignment.m_PlayerID = playerID;
806 assignment.m_Status = 0;
807 m_PlayerAssignments[guid] = assignment;
809 // Send the new assignments to all currently active players
810 // (which does not include the one that's just joining)
811 SendPlayerAssignments();
814 void CNetServerWorker::RemovePlayer(const CStr& guid)
816 m_PlayerAssignments[guid].m_Enabled = false;
818 SendPlayerAssignments();
821 void CNetServerWorker::ClearAllPlayerReady()
823 for (std::pair<const CStr, PlayerAssignment>& p : m_PlayerAssignments)
824 if (p.second.m_Status != 2)
825 p.second.m_Status = 0;
827 SendPlayerAssignments();
830 void CNetServerWorker::KickPlayer(const CStrW& playerName, const bool ban)
832 // Find the user with that name
833 std::vector<CNetServerSession*>::iterator it = std::find_if(m_Sessions.begin(), m_Sessions.end(),
834 [&](CNetServerSession* session) { return session->GetUserName() == playerName; });
836 // and return if no one or the host has that name
837 if (it == m_Sessions.end() || (*it)->GetGUID() == m_ControllerGUID)
838 return;
840 if (ban)
842 // Remember name
843 if (std::find(m_BannedPlayers.begin(), m_BannedPlayers.end(), playerName) == m_BannedPlayers.end())
844 m_BannedPlayers.push_back(m_LobbyAuth ? CStrW(playerName.substr(0, playerName.find(L" ("))) : playerName);
846 // Remember IP address
847 u32 ipAddress = (*it)->GetIPAddress();
848 if (std::find(m_BannedIPs.begin(), m_BannedIPs.end(), ipAddress) == m_BannedIPs.end())
849 m_BannedIPs.push_back(ipAddress);
852 // Disconnect that user
853 (*it)->Disconnect(ban ? NDR_BANNED : NDR_KICKED);
855 // Send message notifying other clients
856 CKickedMessage kickedMessage;
857 kickedMessage.m_Name = playerName;
858 kickedMessage.m_Ban = ban;
859 Broadcast(&kickedMessage, { NSS_PREGAME, NSS_JOIN_SYNCING, NSS_INGAME });
862 void CNetServerWorker::AssignPlayer(int playerID, const CStr& guid)
864 // Remove anyone who's already assigned to this player
865 for (std::pair<const CStr, PlayerAssignment>& p : m_PlayerAssignments)
867 if (p.second.m_PlayerID == playerID)
868 p.second.m_PlayerID = -1;
871 // Update this host's assignment if it exists
872 if (m_PlayerAssignments.find(guid) != m_PlayerAssignments.end())
873 m_PlayerAssignments[guid].m_PlayerID = playerID;
875 SendPlayerAssignments();
878 void CNetServerWorker::ConstructPlayerAssignmentMessage(CPlayerAssignmentMessage& message)
880 for (const std::pair<const CStr, PlayerAssignment>& p : m_PlayerAssignments)
882 if (!p.second.m_Enabled)
883 continue;
885 CPlayerAssignmentMessage::S_m_Hosts h;
886 h.m_GUID = p.first;
887 h.m_Name = p.second.m_Name;
888 h.m_PlayerID = p.second.m_PlayerID;
889 h.m_Status = p.second.m_Status;
890 message.m_Hosts.push_back(h);
894 void CNetServerWorker::SendPlayerAssignments()
896 CPlayerAssignmentMessage message;
897 ConstructPlayerAssignmentMessage(message);
898 Broadcast(&message, { NSS_PREGAME, NSS_JOIN_SYNCING, NSS_INGAME });
901 const ScriptInterface& CNetServerWorker::GetScriptInterface()
903 return *m_ScriptInterface;
906 void CNetServerWorker::SetTurnLength(u32 msecs)
908 if (m_ServerTurnManager)
909 m_ServerTurnManager->SetTurnLength(msecs);
912 void CNetServerWorker::ProcessLobbyAuth(const CStr& name, const CStr& token)
914 LOGMESSAGE("Net Server: Received lobby auth message from %s with %s", name, token);
915 // Find the user with that guid
916 std::vector<CNetServerSession*>::iterator it = std::find_if(m_Sessions.begin(), m_Sessions.end(),
917 [&](CNetServerSession* session)
918 { return session->GetGUID() == token; });
920 if (it == m_Sessions.end())
921 return;
923 (*it)->SetUserName(name.FromUTF8());
924 // Send an empty message to request the authentication message from the client
925 // after its identity has been confirmed via the lobby
926 CAuthenticateMessage emptyMessage;
927 (*it)->SendMessage(&emptyMessage);
930 bool CNetServerWorker::OnClientHandshake(void* context, CFsmEvent* event)
932 ENSURE(event->GetType() == (uint)NMT_CLIENT_HANDSHAKE);
934 CNetServerSession* session = (CNetServerSession*)context;
935 CNetServerWorker& server = session->GetServer();
937 CCliHandshakeMessage* message = (CCliHandshakeMessage*)event->GetParamRef();
938 if (message->m_ProtocolVersion != PS_PROTOCOL_VERSION)
940 session->Disconnect(NDR_INCORRECT_PROTOCOL_VERSION);
941 return false;
944 CStr guid = ps_generate_guid();
945 int count = 0;
946 // Ensure unique GUID
947 while(std::find_if(
948 server.m_Sessions.begin(), server.m_Sessions.end(),
949 [&guid] (const CNetServerSession* session)
950 { return session->GetGUID() == guid; }) != server.m_Sessions.end())
952 if (++count > 100)
954 session->Disconnect(NDR_GUID_FAILED);
955 return true;
957 guid = ps_generate_guid();
960 session->SetGUID(guid);
962 CSrvHandshakeResponseMessage handshakeResponse;
963 handshakeResponse.m_UseProtocolVersion = PS_PROTOCOL_VERSION;
964 handshakeResponse.m_GUID = guid;
965 handshakeResponse.m_Flags = 0;
967 if (server.m_LobbyAuth)
969 handshakeResponse.m_Flags |= PS_NETWORK_FLAG_REQUIRE_LOBBYAUTH;
970 session->SetNextState(NSS_LOBBY_AUTHENTICATE);
973 session->SendMessage(&handshakeResponse);
975 return true;
978 bool CNetServerWorker::OnAuthenticate(void* context, CFsmEvent* event)
980 ENSURE(event->GetType() == (uint)NMT_AUTHENTICATE);
982 CNetServerSession* session = (CNetServerSession*)context;
983 CNetServerWorker& server = session->GetServer();
985 // Prohibit joins while the game is loading
986 if (server.m_State == SERVER_STATE_LOADING)
988 LOGMESSAGE("Refused connection while the game is loading");
989 session->Disconnect(NDR_SERVER_LOADING);
990 return true;
993 CAuthenticateMessage* message = (CAuthenticateMessage*)event->GetParamRef();
994 CStrW username = SanitisePlayerName(message->m_Name);
995 CStrW usernameWithoutRating(username.substr(0, username.find(L" (")));
997 // Compare the lowercase names as specified by https://xmpp.org/extensions/xep-0029.html#sect-idm139493404168176
998 // "[...] comparisons will be made in case-normalized canonical form."
999 if (server.m_LobbyAuth && usernameWithoutRating.LowerCase() != session->GetUserName().LowerCase())
1001 LOGERROR("Net server: lobby auth: %s tried joining as %s",
1002 session->GetUserName().ToUTF8(),
1003 usernameWithoutRating.ToUTF8());
1004 session->Disconnect(NDR_LOBBY_AUTH_FAILED);
1005 return true;
1008 // Check the password before anything else.
1009 // NB: m_Name must match the client's salt, @see CNetClient::SetGamePassword
1010 if (!server.CheckPassword(message->m_Password, message->m_Name.ToUTF8()))
1012 // Noisy logerror because players are not supposed to be able to get the IP,
1013 // so this might be someone targeting the host for some reason
1014 // (or TODO a dedicated server and we do want to log anyways)
1015 LOGERROR("Net server: user %s tried joining with the wrong password",
1016 session->GetUserName().ToUTF8());
1017 session->Disconnect(NDR_SERVER_REFUSED);
1018 return true;
1021 // Either deduplicate or prohibit join if name is in use
1022 bool duplicatePlayernames = false;
1023 CFG_GET_VAL("network.duplicateplayernames", duplicatePlayernames);
1024 // If lobby authentication is enabled, the clients playername has already been registered.
1025 // There also can't be any duplicated names.
1026 if (!server.m_LobbyAuth && duplicatePlayernames)
1027 username = server.DeduplicatePlayerName(username);
1028 else
1030 std::vector<CNetServerSession*>::iterator it = std::find_if(
1031 server.m_Sessions.begin(), server.m_Sessions.end(),
1032 [&username] (const CNetServerSession* session)
1033 { return session->GetUserName() == username; });
1035 if (it != server.m_Sessions.end() && (*it) != session)
1037 session->Disconnect(NDR_PLAYERNAME_IN_USE);
1038 return true;
1042 // Disconnect banned usernames
1043 if (std::find(server.m_BannedPlayers.begin(), server.m_BannedPlayers.end(), server.m_LobbyAuth ? usernameWithoutRating : username) != server.m_BannedPlayers.end())
1045 session->Disconnect(NDR_BANNED);
1046 return true;
1049 int maxObservers = 0;
1050 CFG_GET_VAL("network.observerlimit", maxObservers);
1052 bool isRejoining = false;
1053 bool serverFull = false;
1054 if (server.m_State == SERVER_STATE_PREGAME)
1056 // Don't check for maxObservers in the gamesetup, as we don't know yet who will be assigned
1057 serverFull = server.m_Sessions.size() >= MAX_CLIENTS;
1059 else
1061 bool isObserver = true;
1062 int disconnectedPlayers = 0;
1063 int connectedPlayers = 0;
1064 // (TODO: if GUIDs were stable, we should use them instead)
1065 for (const std::pair<const CStr, PlayerAssignment>& p : server.m_PlayerAssignments)
1067 const PlayerAssignment& assignment = p.second;
1069 if (!assignment.m_Enabled && assignment.m_Name == username)
1071 isObserver = assignment.m_PlayerID == -1;
1072 isRejoining = true;
1075 if (assignment.m_PlayerID == -1)
1076 continue;
1078 if (assignment.m_Enabled)
1079 ++connectedPlayers;
1080 else
1081 ++disconnectedPlayers;
1084 // Optionally allow everyone or only buddies to join after the game has started
1085 if (!isRejoining)
1087 CStr observerLateJoin;
1088 CFG_GET_VAL("network.lateobservers", observerLateJoin);
1090 if (observerLateJoin == "everyone")
1092 isRejoining = true;
1094 else if (observerLateJoin == "buddies")
1096 CStr buddies;
1097 CFG_GET_VAL("lobby.buddies", buddies);
1098 std::wstringstream buddiesStream(wstring_from_utf8(buddies));
1099 CStrW buddy;
1100 while (std::getline(buddiesStream, buddy, L','))
1102 if (buddy == usernameWithoutRating)
1104 isRejoining = true;
1105 break;
1111 if (!isRejoining)
1113 LOGMESSAGE("Refused connection after game start from not-previously-known user \"%s\"", utf8_from_wstring(username));
1114 session->Disconnect(NDR_SERVER_ALREADY_IN_GAME);
1115 return true;
1118 // Ensure all players will be able to rejoin
1119 serverFull = isObserver && (
1120 (int) server.m_Sessions.size() - connectedPlayers > maxObservers ||
1121 (int) server.m_Sessions.size() + disconnectedPlayers >= MAX_CLIENTS);
1124 if (serverFull)
1126 session->Disconnect(NDR_SERVER_FULL);
1127 return true;
1130 u32 newHostID = server.m_NextHostID++;
1132 session->SetUserName(username);
1133 session->SetHostID(newHostID);
1135 CAuthenticateResultMessage authenticateResult;
1136 authenticateResult.m_Code = isRejoining ? ARC_OK_REJOINING : ARC_OK;
1137 authenticateResult.m_HostID = newHostID;
1138 authenticateResult.m_Message = L"Logged in";
1139 authenticateResult.m_IsController = 0;
1141 if (message->m_ControllerSecret == server.m_ControllerSecret)
1143 if (server.m_ControllerGUID.empty())
1145 server.m_ControllerGUID = session->GetGUID();
1146 authenticateResult.m_IsController = 1;
1148 // TODO: we could probably handle having several controllers, or swapping?
1151 session->SendMessage(&authenticateResult);
1153 server.OnUserJoin(session);
1155 if (isRejoining)
1157 ENSURE(server.m_State != SERVER_STATE_UNCONNECTED && server.m_State != SERVER_STATE_PREGAME);
1159 // Request a copy of the current game state from an existing player,
1160 // so we can send it on to the new player
1162 // Assume session 0 is most likely the local player, so they're
1163 // the most efficient client to request a copy from
1164 CNetServerSession* sourceSession = server.m_Sessions.at(0);
1166 sourceSession->GetFileTransferer().StartTask(
1167 shared_ptr<CNetFileReceiveTask>(new CNetFileReceiveTask_ServerRejoin(server, newHostID))
1170 session->SetNextState(NSS_JOIN_SYNCING);
1173 return true;
1175 bool CNetServerWorker::OnSimulationCommand(void* context, CFsmEvent* event)
1177 ENSURE(event->GetType() == (uint)NMT_SIMULATION_COMMAND);
1179 CNetServerSession* session = (CNetServerSession*)context;
1180 CNetServerWorker& server = session->GetServer();
1182 CSimulationMessage* message = (CSimulationMessage*)event->GetParamRef();
1184 // Ignore messages sent by one player on behalf of another player
1185 // unless cheating is enabled
1186 bool cheatsEnabled = false;
1187 const ScriptInterface& scriptInterface = server.GetScriptInterface();
1188 ScriptRequest rq(scriptInterface);
1189 JS::RootedValue settings(rq.cx);
1190 Script::GetProperty(rq, server.m_InitAttributes, "settings", &settings);
1191 if (Script::HasProperty(rq, settings, "CheatsEnabled"))
1192 Script::GetProperty(rq, settings, "CheatsEnabled", cheatsEnabled);
1194 PlayerAssignmentMap::iterator it = server.m_PlayerAssignments.find(session->GetGUID());
1195 // When cheating is disabled, fail if the player the message claims to
1196 // represent does not exist or does not match the sender's player name
1197 if (!cheatsEnabled && (it == server.m_PlayerAssignments.end() || it->second.m_PlayerID != message->m_Player))
1198 return true;
1200 // Send it back to all clients that have finished
1201 // the loading screen (and the synchronization when rejoining)
1202 server.Broadcast(message, { NSS_INGAME });
1204 // Save all the received commands
1205 if (server.m_SavedCommands.size() < message->m_Turn + 1)
1206 server.m_SavedCommands.resize(message->m_Turn + 1);
1207 server.m_SavedCommands[message->m_Turn].push_back(*message);
1209 // TODO: we shouldn't send the message back to the client that first sent it
1210 return true;
1213 bool CNetServerWorker::OnSyncCheck(void* context, CFsmEvent* event)
1215 ENSURE(event->GetType() == (uint)NMT_SYNC_CHECK);
1217 CNetServerSession* session = (CNetServerSession*)context;
1218 CNetServerWorker& server = session->GetServer();
1220 CSyncCheckMessage* message = (CSyncCheckMessage*)event->GetParamRef();
1222 server.m_ServerTurnManager->NotifyFinishedClientUpdate(*session, message->m_Turn, message->m_Hash);
1223 return true;
1226 bool CNetServerWorker::OnEndCommandBatch(void* context, CFsmEvent* event)
1228 ENSURE(event->GetType() == (uint)NMT_END_COMMAND_BATCH);
1230 CNetServerSession* session = (CNetServerSession*)context;
1231 CNetServerWorker& server = session->GetServer();
1233 CEndCommandBatchMessage* message = (CEndCommandBatchMessage*)event->GetParamRef();
1235 // The turn-length field is ignored
1236 server.m_ServerTurnManager->NotifyFinishedClientCommands(*session, message->m_Turn);
1237 return true;
1240 bool CNetServerWorker::OnChat(void* context, CFsmEvent* event)
1242 ENSURE(event->GetType() == (uint)NMT_CHAT);
1244 CNetServerSession* session = (CNetServerSession*)context;
1245 CNetServerWorker& server = session->GetServer();
1247 CChatMessage* message = (CChatMessage*)event->GetParamRef();
1249 message->m_GUID = session->GetGUID();
1251 server.Broadcast(message, { NSS_PREGAME, NSS_INGAME });
1253 return true;
1256 bool CNetServerWorker::OnReady(void* context, CFsmEvent* event)
1258 ENSURE(event->GetType() == (uint)NMT_READY);
1260 CNetServerSession* session = (CNetServerSession*)context;
1261 CNetServerWorker& server = session->GetServer();
1263 // Occurs if a client presses not-ready
1264 // in the very last moment before the hosts starts the game
1265 if (server.m_State == SERVER_STATE_LOADING)
1266 return true;
1268 CReadyMessage* message = (CReadyMessage*)event->GetParamRef();
1269 message->m_GUID = session->GetGUID();
1270 server.Broadcast(message, { NSS_PREGAME });
1272 server.m_PlayerAssignments[message->m_GUID].m_Status = message->m_Status;
1274 return true;
1277 bool CNetServerWorker::OnClearAllReady(void* context, CFsmEvent* event)
1279 ENSURE(event->GetType() == (uint)NMT_CLEAR_ALL_READY);
1281 CNetServerSession* session = (CNetServerSession*)context;
1282 CNetServerWorker& server = session->GetServer();
1284 if (session->GetGUID() == server.m_ControllerGUID)
1285 server.ClearAllPlayerReady();
1287 return true;
1290 bool CNetServerWorker::OnGameSetup(void* context, CFsmEvent* event)
1292 ENSURE(event->GetType() == (uint)NMT_GAME_SETUP);
1294 CNetServerSession* session = (CNetServerSession*)context;
1295 CNetServerWorker& server = session->GetServer();
1297 // Changing the settings after gamestart is not implemented and would cause an Out-of-sync error.
1298 // This happened when doubleclicking on the startgame button.
1299 if (server.m_State != SERVER_STATE_PREGAME)
1300 return true;
1302 // Only the controller is allowed to send game setup updates.
1303 // TODO: it would be good to allow other players to request changes to some settings,
1304 // e.g. their civilisation.
1305 // Possibly this should use another message, to enforce a single source of truth.
1306 if (session->GetGUID() == server.m_ControllerGUID)
1308 CGameSetupMessage* message = (CGameSetupMessage*)event->GetParamRef();
1309 server.Broadcast(message, { NSS_PREGAME });
1311 return true;
1314 bool CNetServerWorker::OnAssignPlayer(void* context, CFsmEvent* event)
1316 ENSURE(event->GetType() == (uint)NMT_ASSIGN_PLAYER);
1317 CNetServerSession* session = (CNetServerSession*)context;
1318 CNetServerWorker& server = session->GetServer();
1320 if (session->GetGUID() == server.m_ControllerGUID)
1322 CAssignPlayerMessage* message = (CAssignPlayerMessage*)event->GetParamRef();
1323 server.AssignPlayer(message->m_PlayerID, message->m_GUID);
1325 return true;
1328 bool CNetServerWorker::OnGameStart(void* context, CFsmEvent* event)
1330 ENSURE(event->GetType() == (uint)NMT_GAME_START);
1331 CNetServerSession* session = (CNetServerSession*)context;
1332 CNetServerWorker& server = session->GetServer();
1334 if (session->GetGUID() != server.m_ControllerGUID)
1335 return true;
1337 CGameStartMessage* message = (CGameStartMessage*)event->GetParamRef();
1338 server.StartGame(message->m_InitAttributes);
1339 return true;
1342 bool CNetServerWorker::OnLoadedGame(void* context, CFsmEvent* event)
1344 ENSURE(event->GetType() == (uint)NMT_LOADED_GAME);
1346 CNetServerSession* loadedSession = (CNetServerSession*)context;
1347 CNetServerWorker& server = loadedSession->GetServer();
1349 // We're in the loading state, so wait until every client has loaded
1350 // before starting the game
1351 ENSURE(server.m_State == SERVER_STATE_LOADING);
1352 if (server.CheckGameLoadStatus(loadedSession))
1353 return true;
1355 CClientsLoadingMessage message;
1356 // We always send all GUIDs of clients in the loading state
1357 // so that we don't have to bother about switching GUI pages
1358 for (CNetServerSession* session : server.m_Sessions)
1359 if (session->GetCurrState() != NSS_INGAME && loadedSession->GetGUID() != session->GetGUID())
1361 CClientsLoadingMessage::S_m_Clients client;
1362 client.m_GUID = session->GetGUID();
1363 message.m_Clients.push_back(client);
1366 // Send to the client who has loaded the game but did not reach the NSS_INGAME state yet
1367 loadedSession->SendMessage(&message);
1368 server.Broadcast(&message, { NSS_INGAME });
1370 return true;
1373 bool CNetServerWorker::OnJoinSyncingLoadedGame(void* context, CFsmEvent* event)
1375 // A client rejoining an in-progress game has now finished loading the
1376 // map and deserialized the initial state.
1377 // The simulation may have progressed since then, so send any subsequent
1378 // commands to them and set them as an active player so they can participate
1379 // in all future turns.
1381 // (TODO: if it takes a long time for them to receive and execute all these
1382 // commands, the other players will get frozen for that time and may be unhappy;
1383 // we could try repeating this process a few times until the client converges
1384 // on the up-to-date state, before setting them as active.)
1386 ENSURE(event->GetType() == (uint)NMT_LOADED_GAME);
1388 CNetServerSession* session = (CNetServerSession*)context;
1389 CNetServerWorker& server = session->GetServer();
1391 CLoadedGameMessage* message = (CLoadedGameMessage*)event->GetParamRef();
1393 u32 turn = message->m_CurrentTurn;
1394 u32 readyTurn = server.m_ServerTurnManager->GetReadyTurn();
1396 // Send them all commands received since their saved state,
1397 // and turn-ended messages for any turns that have already been processed
1398 for (size_t i = turn + 1; i < std::max(readyTurn+1, (u32)server.m_SavedCommands.size()); ++i)
1400 if (i < server.m_SavedCommands.size())
1401 for (size_t j = 0; j < server.m_SavedCommands[i].size(); ++j)
1402 session->SendMessage(&server.m_SavedCommands[i][j]);
1404 if (i <= readyTurn)
1406 CEndCommandBatchMessage endMessage;
1407 endMessage.m_Turn = i;
1408 endMessage.m_TurnLength = server.m_ServerTurnManager->GetSavedTurnLength(i);
1409 session->SendMessage(&endMessage);
1413 // Tell the turn manager to expect commands from this new client
1414 // Special case: the controller shouldn't be treated as an observer in any case.
1415 bool isObserver = server.m_PlayerAssignments[session->GetGUID()].m_PlayerID == -1 && server.m_ControllerGUID != session->GetGUID();
1416 server.m_ServerTurnManager->InitialiseClient(session->GetHostID(), readyTurn, isObserver);
1418 // Tell the client that everything has finished loading and it should start now
1419 CLoadedGameMessage loaded;
1420 loaded.m_CurrentTurn = readyTurn;
1421 session->SendMessage(&loaded);
1423 return true;
1426 bool CNetServerWorker::OnRejoined(void* context, CFsmEvent* event)
1428 // A client has finished rejoining and the loading screen disappeared.
1429 ENSURE(event->GetType() == (uint)NMT_REJOINED);
1431 CNetServerSession* session = (CNetServerSession*)context;
1432 CNetServerWorker& server = session->GetServer();
1434 // Inform everyone of the client having rejoined
1435 CRejoinedMessage* message = (CRejoinedMessage*)event->GetParamRef();
1436 message->m_GUID = session->GetGUID();
1437 server.Broadcast(message, { NSS_INGAME });
1439 // Send all pausing players to the rejoined client.
1440 for (const CStr& guid : server.m_PausingPlayers)
1442 CClientPausedMessage pausedMessage;
1443 pausedMessage.m_GUID = guid;
1444 pausedMessage.m_Pause = true;
1445 session->SendMessage(&pausedMessage);
1448 return true;
1451 bool CNetServerWorker::OnKickPlayer(void* context, CFsmEvent* event)
1453 ENSURE(event->GetType() == (uint)NMT_KICKED);
1455 CNetServerSession* session = (CNetServerSession*)context;
1456 CNetServerWorker& server = session->GetServer();
1458 if (session->GetGUID() == server.m_ControllerGUID)
1460 CKickedMessage* message = (CKickedMessage*)event->GetParamRef();
1461 server.KickPlayer(message->m_Name, message->m_Ban);
1463 return true;
1466 bool CNetServerWorker::OnDisconnect(void* context, CFsmEvent* event)
1468 ENSURE(event->GetType() == (uint)NMT_CONNECTION_LOST);
1470 CNetServerSession* session = (CNetServerSession*)context;
1471 CNetServerWorker& server = session->GetServer();
1473 server.OnUserLeave(session);
1475 return true;
1478 bool CNetServerWorker::OnClientPaused(void* context, CFsmEvent* event)
1480 ENSURE(event->GetType() == (uint)NMT_CLIENT_PAUSED);
1482 CNetServerSession* session = (CNetServerSession*)context;
1483 CNetServerWorker& server = session->GetServer();
1485 CClientPausedMessage* message = (CClientPausedMessage*)event->GetParamRef();
1487 message->m_GUID = session->GetGUID();
1489 // Update the list of pausing players.
1490 std::vector<CStr>::iterator player = std::find(server.m_PausingPlayers.begin(), server.m_PausingPlayers.end(), session->GetGUID());
1492 if (message->m_Pause)
1494 if (player != server.m_PausingPlayers.end())
1495 return true;
1497 server.m_PausingPlayers.push_back(session->GetGUID());
1499 else
1501 if (player == server.m_PausingPlayers.end())
1502 return true;
1504 server.m_PausingPlayers.erase(player);
1507 // Send messages to clients that are in game, and are not the client who paused.
1508 for (CNetServerSession* netSession : server.m_Sessions)
1509 if (netSession->GetCurrState() == NSS_INGAME && message->m_GUID != netSession->GetGUID())
1510 netSession->SendMessage(message);
1512 return true;
1515 bool CNetServerWorker::CheckGameLoadStatus(CNetServerSession* changedSession)
1517 for (const CNetServerSession* session : m_Sessions)
1518 if (session != changedSession && session->GetCurrState() != NSS_INGAME)
1519 return false;
1521 // Inform clients that everyone has loaded the map and that the game can start
1522 CLoadedGameMessage loaded;
1523 loaded.m_CurrentTurn = 0;
1525 // Notice the changedSession is still in the NSS_PREGAME state
1526 Broadcast(&loaded, { NSS_PREGAME, NSS_INGAME });
1528 m_State = SERVER_STATE_INGAME;
1529 return true;
1532 void CNetServerWorker::StartGame(const CStr& initAttribs)
1534 for (std::pair<const CStr, PlayerAssignment>& player : m_PlayerAssignments)
1535 if (player.second.m_Enabled && player.second.m_PlayerID != -1 && player.second.m_Status == 0)
1537 LOGERROR("Tried to start the game without player \"%s\" being ready!", utf8_from_wstring(player.second.m_Name).c_str());
1538 return;
1541 m_ServerTurnManager = new CNetServerTurnManager(*this);
1543 for (CNetServerSession* session : m_Sessions)
1545 // Special case: the controller shouldn't be treated as an observer in any case.
1546 bool isObserver = m_PlayerAssignments[session->GetGUID()].m_PlayerID == -1 && m_ControllerGUID != session->GetGUID();
1547 m_ServerTurnManager->InitialiseClient(session->GetHostID(), 0, isObserver);
1550 m_State = SERVER_STATE_LOADING;
1552 // Remove players and observers that are not present when the game starts
1553 for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end();)
1554 if (it->second.m_Enabled)
1555 ++it;
1556 else
1557 it = m_PlayerAssignments.erase(it);
1559 SendPlayerAssignments();
1561 // Update init attributes. They should no longer change.
1562 Script::ParseJSON(ScriptRequest(m_ScriptInterface), initAttribs, &m_InitAttributes);
1564 CGameStartMessage gameStart;
1565 gameStart.m_InitAttributes = initAttribs;
1566 Broadcast(&gameStart, { NSS_PREGAME });
1569 CStrW CNetServerWorker::SanitisePlayerName(const CStrW& original)
1571 const size_t MAX_LENGTH = 32;
1573 CStrW name = original;
1574 name.Replace(L"[", L"{"); // remove GUI tags
1575 name.Replace(L"]", L"}"); // remove for symmetry
1577 // Restrict the length
1578 if (name.length() > MAX_LENGTH)
1579 name = name.Left(MAX_LENGTH);
1581 // Don't allow surrounding whitespace
1582 name.Trim(PS_TRIM_BOTH);
1584 // Don't allow empty name
1585 if (name.empty())
1586 name = L"Anonymous";
1588 return name;
1591 CStrW CNetServerWorker::DeduplicatePlayerName(const CStrW& original)
1593 CStrW name = original;
1595 // Try names "Foo", "Foo (2)", "Foo (3)", etc
1596 size_t id = 2;
1597 while (true)
1599 bool unique = true;
1600 for (const CNetServerSession* session : m_Sessions)
1602 if (session->GetUserName() == name)
1604 unique = false;
1605 break;
1609 if (unique)
1610 return name;
1612 name = original + L" (" + CStrW::FromUInt(id++) + L")";
1616 void CNetServerWorker::SendHolePunchingMessage(const CStr& ipStr, u16 port)
1618 if (m_Host)
1619 StunClient::SendHolePunchingMessages(*m_Host, ipStr, port);
1625 CNetServer::CNetServer(bool useLobbyAuth, int autostartPlayers) :
1626 m_Worker(new CNetServerWorker(useLobbyAuth, autostartPlayers)),
1627 m_LobbyAuth(useLobbyAuth), m_UseSTUN(false), m_PublicIp(""), m_PublicPort(20595), m_Password()
1631 CNetServer::~CNetServer()
1633 delete m_Worker;
1636 bool CNetServer::GetUseSTUN() const
1638 return m_UseSTUN;
1641 bool CNetServer::UseLobbyAuth() const
1643 return m_LobbyAuth;
1646 bool CNetServer::SetupConnection(const u16 port)
1648 return m_Worker->SetupConnection(port);
1651 CStr CNetServer::GetPublicIp() const
1653 return m_PublicIp;
1656 u16 CNetServer::GetPublicPort() const
1658 return m_PublicPort;
1661 u16 CNetServer::GetLocalPort() const
1663 std::lock_guard<std::mutex> lock(m_Worker->m_WorkerMutex);
1664 if (!m_Worker->m_Host)
1665 return 0;
1666 return m_Worker->m_Host->address.port;
1669 void CNetServer::SetConnectionData(const CStr& ip, const u16 port)
1671 m_PublicIp = ip;
1672 m_PublicPort = port;
1673 m_UseSTUN = false;
1676 bool CNetServer::SetConnectionDataViaSTUN()
1678 m_UseSTUN = true;
1679 std::lock_guard<std::mutex> lock(m_Worker->m_WorkerMutex);
1680 if (!m_Worker->m_Host)
1681 return false;
1682 return StunClient::FindPublicIP(*m_Worker->m_Host, m_PublicIp, m_PublicPort);
1685 bool CNetServer::CheckPasswordAndIncrement(const std::string& username, const std::string& password, const std::string& salt)
1687 std::unordered_map<std::string, int>::iterator it = m_FailedAttempts.find(username);
1688 if (m_Worker->CheckPassword(password, salt))
1690 if (it != m_FailedAttempts.end())
1691 it->second = 0;
1692 return true;
1694 if (it == m_FailedAttempts.end())
1695 m_FailedAttempts.emplace(username, 1);
1696 else
1697 it->second++;
1698 return false;
1701 bool CNetServer::IsBanned(const std::string& username) const
1703 std::unordered_map<std::string, int>::const_iterator it = m_FailedAttempts.find(username);
1704 return it != m_FailedAttempts.end() && it->second >= FAILED_PASSWORD_TRIES_BEFORE_BAN;
1707 void CNetServer::SetPassword(const CStr& password)
1709 m_Password = password;
1710 std::lock_guard<std::mutex> lock(m_Worker->m_WorkerMutex);
1711 m_Worker->SetPassword(password);
1714 void CNetServer::SetControllerSecret(const std::string& secret)
1716 std::lock_guard<std::mutex> lock(m_Worker->m_WorkerMutex);
1717 m_Worker->SetControllerSecret(secret);
1720 void CNetServer::StartGame()
1722 std::lock_guard<std::mutex> lock(m_Worker->m_WorkerMutex);
1723 m_Worker->m_StartGameQueue.push_back(true);
1726 void CNetServer::UpdateInitAttributes(JS::MutableHandleValue attrs, const ScriptRequest& rq)
1728 // Pass the attributes as JSON, since that's the easiest safe
1729 // cross-thread way of passing script data
1730 std::string attrsJSON = Script::StringifyJSON(rq, attrs, false);
1732 std::lock_guard<std::mutex> lock(m_Worker->m_WorkerMutex);
1733 m_Worker->m_InitAttributesQueue.push_back(attrsJSON);
1736 void CNetServer::OnLobbyAuth(const CStr& name, const CStr& token)
1738 std::lock_guard<std::mutex> lock(m_Worker->m_WorkerMutex);
1739 m_Worker->m_LobbyAuthQueue.push_back(std::make_pair(name, token));
1742 void CNetServer::SetTurnLength(u32 msecs)
1744 std::lock_guard<std::mutex> lock(m_Worker->m_WorkerMutex);
1745 m_Worker->m_TurnLengthQueue.push_back(msecs);
1748 void CNetServer::SendHolePunchingMessage(const CStr& ip, u16 port)
1750 m_Worker->SendHolePunchingMessage(ip, port);