Split the FSM-transitions and put them in an unordered_map
[0ad.git] / source / network / NetServer.cpp
blob5e005e074413233eaa923228be68acb962a69158
1 /* Copyright (C) 2023 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 "NetEnet.h"
24 #include "NetMessage.h"
25 #include "NetSession.h"
26 #include "NetServerTurnManager.h"
27 #include "NetStats.h"
29 #include "lib/external_libraries/enet.h"
30 #include "lib/types.h"
31 #include "network/StunClient.h"
32 #include "ps/CLogger.h"
33 #include "ps/ConfigDB.h"
34 #include "ps/GUID.h"
35 #include "ps/Hashing.h"
36 #include "ps/Profile.h"
37 #include "ps/Threading.h"
38 #include "scriptinterface/ScriptContext.h"
39 #include "scriptinterface/ScriptInterface.h"
40 #include "scriptinterface/JSON.h"
41 #include "simulation2/Simulation2.h"
42 #include "simulation2/system/TurnManager.h"
44 #if CONFIG2_MINIUPNPC
45 #include <miniupnpc/miniwget.h>
46 #include <miniupnpc/miniupnpc.h>
47 #include <miniupnpc/upnpcommands.h>
48 #include <miniupnpc/upnperrors.h>
49 #endif
51 #include <string>
53 /**
54 * Number of peers to allocate for the enet host.
55 * Limited by ENET_PROTOCOL_MAXIMUM_PEER_ID (4096).
57 * At most 8 players, 32 observers and 1 temporary connection to send the "server full" disconnect-reason.
59 #define MAX_CLIENTS 41
61 #define DEFAULT_SERVER_NAME L"Unnamed Server"
63 constexpr int CHANNEL_COUNT = 1;
64 constexpr int FAILED_PASSWORD_TRIES_BEFORE_BAN = 3;
66 /**
67 * enet_host_service timeout (msecs).
68 * Smaller numbers may hurt performance; larger numbers will
69 * hurt latency responding to messages from game thread.
71 static const int HOST_SERVICE_TIMEOUT = 50;
73 /**
74 * Once ping goes above turn length * command delay,
75 * the game will start 'freezing' for other clients while we catch up.
76 * Since commands are sent client -> server -> client, divide by 2.
77 * (duplicated in NetServer.cpp to avoid having to fetch the constants in a header file)
79 constexpr u32 NETWORK_BAD_PING = DEFAULT_TURN_LENGTH * COMMAND_DELAY_MP / 2;
81 CNetServer* g_NetServer = NULL;
83 static CStr DebugName(CNetServerSession* session)
85 if (session == NULL)
86 return "[unknown host]";
87 if (session->GetGUID().empty())
88 return "[unauthed host]";
89 return "[" + session->GetGUID().substr(0, 8) + "...]";
92 /**
93 * Async task for receiving the initial game state to be forwarded to another
94 * client that is rejoining an in-progress network game.
96 class CNetFileReceiveTask_ServerRejoin : public CNetFileReceiveTask
98 NONCOPYABLE(CNetFileReceiveTask_ServerRejoin);
99 public:
100 CNetFileReceiveTask_ServerRejoin(CNetServerWorker& server, u32 hostID)
101 : m_Server(server), m_RejoinerHostID(hostID)
105 virtual void OnComplete()
107 // We've received the game state from an existing player - now
108 // we need to send it onwards to the newly rejoining player
110 // Find the session corresponding to the rejoining host (if any)
111 CNetServerSession* session = NULL;
112 for (CNetServerSession* serverSession : m_Server.m_Sessions)
114 if (serverSession->GetHostID() == m_RejoinerHostID)
116 session = serverSession;
117 break;
121 if (!session)
123 LOGMESSAGE("Net server: rejoining client disconnected before we sent to it");
124 return;
127 // Store the received state file, and tell the client to start downloading it from us
128 // TODO: this will get kind of confused if there's multiple clients downloading in parallel;
129 // they'll race and get whichever happens to be the latest received by the server,
130 // which should still work but isn't great
131 m_Server.m_JoinSyncFile = m_Buffer;
133 // Send the init attributes alongside - these should be correct since the game should be started.
134 CJoinSyncStartMessage message;
135 message.m_InitAttributes = Script::StringifyJSON(ScriptRequest(m_Server.GetScriptInterface()), &m_Server.m_InitAttributes);
136 session->SendMessage(&message);
139 private:
140 CNetServerWorker& m_Server;
141 u32 m_RejoinerHostID;
145 * XXX: We use some non-threadsafe functions from the worker thread.
146 * See http://trac.wildfiregames.com/ticket/654
149 CNetServerWorker::CNetServerWorker(bool useLobbyAuth) :
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 = PS::Enet::CreateHost(&addr, MAX_CLIENTS, CHANNEL_COUNT);
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 std::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 // Update profiler stats
433 m_Stats->LatchHostState(m_Host);
436 // Clear roots before deleting their context
437 m_SavedCommands.clear();
439 SAFE_DELETE(m_ScriptInterface);
442 bool CNetServerWorker::RunStep()
444 // Check for messages from the game thread.
445 // (Do as little work as possible while the mutex is held open,
446 // to avoid performance problems and deadlocks.)
448 m_ScriptInterface->GetContext().MaybeIncrementalGC(0.5f);
450 ScriptRequest rq(m_ScriptInterface);
452 std::vector<bool> newStartGame;
453 std::vector<std::string> newGameAttributes;
454 std::vector<std::pair<CStr, CStr>> newLobbyAuths;
455 std::vector<u32> newTurnLength;
458 std::lock_guard<std::mutex> lock(m_WorkerMutex);
460 if (m_Shutdown)
461 return false;
463 newStartGame.swap(m_StartGameQueue);
464 newGameAttributes.swap(m_InitAttributesQueue);
465 newLobbyAuths.swap(m_LobbyAuthQueue);
466 newTurnLength.swap(m_TurnLengthQueue);
469 if (!newGameAttributes.empty())
471 if (m_State != SERVER_STATE_UNCONNECTED && m_State != SERVER_STATE_PREGAME)
472 LOGERROR("NetServer: Init Attributes cannot be changed after the server starts loading.");
473 else
475 JS::RootedValue gameAttributesVal(rq.cx);
476 Script::ParseJSON(rq, newGameAttributes.back(), &gameAttributesVal);
477 m_InitAttributes = gameAttributesVal;
481 if (!newTurnLength.empty())
482 SetTurnLength(newTurnLength.back());
484 while (!newLobbyAuths.empty())
486 const std::pair<CStr, CStr>& auth = newLobbyAuths.back();
487 ProcessLobbyAuth(auth.first, auth.second);
488 newLobbyAuths.pop_back();
491 // Perform file transfers
492 for (CNetServerSession* session : m_Sessions)
493 session->GetFileTransferer().Poll();
495 CheckClientConnections();
497 // Process network events:
499 ENetEvent event;
500 int status = enet_host_service(m_Host, &event, HOST_SERVICE_TIMEOUT);
501 if (status < 0)
503 LOGERROR("CNetServerWorker: enet_host_service failed (%d)", status);
504 // TODO: notify game that the server has shut down
505 return false;
508 if (status == 0)
510 // Reached timeout with no events - try again
511 return true;
514 // Process the event:
516 switch (event.type)
518 case ENET_EVENT_TYPE_CONNECT:
520 // Report the client address
521 char hostname[256] = "(error)";
522 enet_address_get_host_ip(&event.peer->address, hostname, ARRAY_SIZE(hostname));
523 LOGMESSAGE("Net server: Received connection from %s:%u", hostname, (unsigned int)event.peer->address.port);
525 // Set up a session object for this peer
527 CNetServerSession* session = new CNetServerSession(*this, event.peer);
529 m_Sessions.push_back(session);
531 SetupSession(session);
533 ENSURE(event.peer->data == NULL);
534 event.peer->data = session;
536 HandleConnect(session);
538 break;
541 case ENET_EVENT_TYPE_DISCONNECT:
543 // If there is an active session with this peer, then reset and delete it
545 CNetServerSession* session = static_cast<CNetServerSession*>(event.peer->data);
546 if (session)
548 LOGMESSAGE("Net server: Disconnected %s", DebugName(session).c_str());
550 // Remove the session first, so we won't send player-update messages to it
551 // when updating the FSM
552 m_Sessions.erase(remove(m_Sessions.begin(), m_Sessions.end(), session), m_Sessions.end());
554 session->Update((uint)NMT_CONNECTION_LOST, NULL);
556 delete session;
557 event.peer->data = NULL;
560 if (m_State == SERVER_STATE_LOADING)
561 CheckGameLoadStatus(NULL);
563 break;
566 case ENET_EVENT_TYPE_RECEIVE:
568 // If there is an active session with this peer, then process the message
570 CNetServerSession* session = static_cast<CNetServerSession*>(event.peer->data);
571 if (session)
573 // Create message from raw data
574 CNetMessage* msg = CNetMessageFactory::CreateMessage(event.packet->data, event.packet->dataLength, GetScriptInterface());
575 if (msg)
577 LOGMESSAGE("Net server: Received message %s of size %lu from %s", msg->ToString().c_str(), (unsigned long)msg->GetSerializedLength(), DebugName(session).c_str());
579 HandleMessageReceive(msg, session);
581 delete msg;
585 // Done using the packet
586 enet_packet_destroy(event.packet);
588 break;
591 case ENET_EVENT_TYPE_NONE:
592 break;
595 return true;
598 void CNetServerWorker::CheckClientConnections()
600 // Send messages at most once per second
601 std::time_t now = std::time(nullptr);
602 if (now <= m_LastConnectionCheck)
603 return;
605 m_LastConnectionCheck = now;
607 for (size_t i = 0; i < m_Sessions.size(); ++i)
609 u32 lastReceived = m_Sessions[i]->GetLastReceivedTime();
610 u32 meanRTT = m_Sessions[i]->GetMeanRTT();
612 CNetMessage* message = nullptr;
614 // Report if we didn't hear from the client since few seconds
615 if (lastReceived > NETWORK_WARNING_TIMEOUT)
617 CClientTimeoutMessage* msg = new CClientTimeoutMessage();
618 msg->m_GUID = m_Sessions[i]->GetGUID();
619 msg->m_LastReceivedTime = lastReceived;
620 message = msg;
622 // Report if the client has bad ping
623 else if (meanRTT > NETWORK_BAD_PING)
625 CClientPerformanceMessage* msg = new CClientPerformanceMessage();
626 CClientPerformanceMessage::S_m_Clients client;
627 client.m_GUID = m_Sessions[i]->GetGUID();
628 client.m_MeanRTT = meanRTT;
629 msg->m_Clients.push_back(client);
630 message = msg;
633 // Send to all clients except the affected one
634 // (since that will show the locally triggered warning instead).
635 // Also send it to clients that finished the loading screen while
636 // the game is still waiting for other clients to finish the loading screen.
637 if (message)
638 for (size_t j = 0; j < m_Sessions.size(); ++j)
640 if (i != j && (
641 (m_Sessions[j]->GetCurrState() == NSS_PREGAME && m_State == SERVER_STATE_PREGAME) ||
642 m_Sessions[j]->GetCurrState() == NSS_INGAME))
644 m_Sessions[j]->SendMessage(message);
648 SAFE_DELETE(message);
652 void CNetServerWorker::HandleMessageReceive(const CNetMessage* message, CNetServerSession* session)
654 // Handle non-FSM messages first
655 Status status = session->GetFileTransferer().HandleMessageReceive(*message);
656 if (status != INFO::SKIPPED)
657 return;
659 if (message->GetType() == NMT_FILE_TRANSFER_REQUEST)
661 CFileTransferRequestMessage* reqMessage = (CFileTransferRequestMessage*)message;
663 // Rejoining client got our JoinSyncStart after we received the state from
664 // another client, and has now requested that we forward it to them
666 ENSURE(!m_JoinSyncFile.empty());
667 session->GetFileTransferer().StartResponse(reqMessage->m_RequestID, m_JoinSyncFile);
669 return;
672 // Update FSM
673 if (!session->Update(message->GetType(), (void*)message))
674 LOGERROR("Net server: Error running FSM update (type=%d state=%d)", (int)message->GetType(), (int)session->GetCurrState());
677 void CNetServerWorker::SetupSession(CNetServerSession* session)
679 void* context = session;
681 // Set up transitions for session
683 session->AddTransition(NSS_UNCONNECTED, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
685 session->AddTransition(NSS_HANDSHAKE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
686 session->AddTransition(NSS_HANDSHAKE, (uint)NMT_CLIENT_HANDSHAKE, NSS_AUTHENTICATE, &OnClientHandshake, context);
688 session->AddTransition(NSS_LOBBY_AUTHENTICATE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
689 session->AddTransition(NSS_LOBBY_AUTHENTICATE, (uint)NMT_AUTHENTICATE, NSS_PREGAME, &OnAuthenticate, context);
691 session->AddTransition(NSS_AUTHENTICATE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
692 session->AddTransition(NSS_AUTHENTICATE, (uint)NMT_AUTHENTICATE, NSS_PREGAME, &OnAuthenticate, context);
694 session->AddTransition(NSS_PREGAME, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, &OnDisconnect, context);
695 session->AddTransition(NSS_PREGAME, (uint)NMT_CHAT, NSS_PREGAME, &OnChat, context);
696 session->AddTransition(NSS_PREGAME, (uint)NMT_READY, NSS_PREGAME, &OnReady, context);
697 session->AddTransition(NSS_PREGAME, (uint)NMT_CLEAR_ALL_READY, NSS_PREGAME, &OnClearAllReady, context);
698 session->AddTransition(NSS_PREGAME, (uint)NMT_GAME_SETUP, NSS_PREGAME, &OnGameSetup, context);
699 session->AddTransition(NSS_PREGAME, (uint)NMT_ASSIGN_PLAYER, NSS_PREGAME, &OnAssignPlayer, context);
700 session->AddTransition(NSS_PREGAME, (uint)NMT_KICKED, NSS_PREGAME, &OnKickPlayer, context);
701 session->AddTransition(NSS_PREGAME, (uint)NMT_GAME_START, NSS_PREGAME, &OnGameStart, context);
702 session->AddTransition(NSS_PREGAME, (uint)NMT_LOADED_GAME, NSS_INGAME, &OnLoadedGame, context);
704 session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_KICKED, NSS_JOIN_SYNCING, &OnKickPlayer, context);
705 session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, &OnDisconnect, context);
706 session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_LOADED_GAME, NSS_INGAME, &OnJoinSyncingLoadedGame, context);
708 session->AddTransition(NSS_INGAME, (uint)NMT_REJOINED, NSS_INGAME, &OnRejoined, context);
709 session->AddTransition(NSS_INGAME, (uint)NMT_KICKED, NSS_INGAME, &OnKickPlayer, context);
710 session->AddTransition(NSS_INGAME, (uint)NMT_CLIENT_PAUSED, NSS_INGAME, &OnClientPaused, context);
711 session->AddTransition(NSS_INGAME, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, &OnDisconnect, context);
712 session->AddTransition(NSS_INGAME, (uint)NMT_CHAT, NSS_INGAME, &OnChat, context);
713 session->AddTransition(NSS_INGAME, (uint)NMT_SIMULATION_COMMAND, NSS_INGAME, &OnSimulationCommand, context);
714 session->AddTransition(NSS_INGAME, (uint)NMT_SYNC_CHECK, NSS_INGAME, &OnSyncCheck, context);
715 session->AddTransition(NSS_INGAME, (uint)NMT_END_COMMAND_BATCH, NSS_INGAME, &OnEndCommandBatch, context);
717 // Set first state
718 session->SetFirstState(NSS_HANDSHAKE);
721 bool CNetServerWorker::HandleConnect(CNetServerSession* session)
723 if (std::find(m_BannedIPs.begin(), m_BannedIPs.end(), session->GetIPAddress()) != m_BannedIPs.end())
725 session->Disconnect(NDR_BANNED);
726 return false;
729 CSrvHandshakeMessage handshake;
730 handshake.m_Magic = PS_PROTOCOL_MAGIC;
731 handshake.m_ProtocolVersion = PS_PROTOCOL_VERSION;
732 handshake.m_SoftwareVersion = PS_PROTOCOL_VERSION;
733 return session->SendMessage(&handshake);
736 void CNetServerWorker::OnUserJoin(CNetServerSession* session)
738 AddPlayer(session->GetGUID(), session->GetUserName());
740 CPlayerAssignmentMessage assignMessage;
741 ConstructPlayerAssignmentMessage(assignMessage);
742 session->SendMessage(&assignMessage);
745 void CNetServerWorker::OnUserLeave(CNetServerSession* session)
747 std::vector<CStr>::iterator pausing = std::find(m_PausingPlayers.begin(), m_PausingPlayers.end(), session->GetGUID());
748 if (pausing != m_PausingPlayers.end())
749 m_PausingPlayers.erase(pausing);
751 RemovePlayer(session->GetGUID());
753 if (m_ServerTurnManager && session->GetCurrState() != NSS_JOIN_SYNCING)
754 m_ServerTurnManager->UninitialiseClient(session->GetHostID());
756 // TODO: ought to switch the player controlled by that client
757 // back to AI control, or something?
760 void CNetServerWorker::AddPlayer(const CStr& guid, const CStrW& name)
762 // Find all player IDs in active use; we mustn't give them to a second player (excluding the unassigned ID: -1)
763 std::set<i32> usedIDs;
764 for (const std::pair<const CStr, PlayerAssignment>& p : m_PlayerAssignments)
765 if (p.second.m_Enabled && p.second.m_PlayerID != -1)
766 usedIDs.insert(p.second.m_PlayerID);
768 // If the player is rejoining after disconnecting, try to give them
769 // back their old player ID. Don't do this in pregame however,
770 // as that ID might be invalid for various reasons.
772 i32 playerID = -1;
774 if (m_State != SERVER_STATE_UNCONNECTED && m_State != SERVER_STATE_PREGAME)
776 // Try to match GUID first
777 for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it)
779 if (!it->second.m_Enabled && it->first == guid && usedIDs.find(it->second.m_PlayerID) == usedIDs.end())
781 playerID = it->second.m_PlayerID;
782 m_PlayerAssignments.erase(it); // delete the old mapping, since we've got a new one now
783 goto found;
787 // Try to match username next
788 for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it)
790 if (!it->second.m_Enabled && it->second.m_Name == name && usedIDs.find(it->second.m_PlayerID) == usedIDs.end())
792 playerID = it->second.m_PlayerID;
793 m_PlayerAssignments.erase(it); // delete the old mapping, since we've got a new one now
794 goto found;
799 found:
800 PlayerAssignment assignment;
801 assignment.m_Enabled = true;
802 assignment.m_Name = name;
803 assignment.m_PlayerID = playerID;
804 assignment.m_Status = 0;
805 m_PlayerAssignments[guid] = assignment;
807 // Send the new assignments to all currently active players
808 // (which does not include the one that's just joining)
809 SendPlayerAssignments();
812 void CNetServerWorker::RemovePlayer(const CStr& guid)
814 m_PlayerAssignments[guid].m_Enabled = false;
816 SendPlayerAssignments();
819 void CNetServerWorker::ClearAllPlayerReady()
821 for (std::pair<const CStr, PlayerAssignment>& p : m_PlayerAssignments)
822 if (p.second.m_Status != 2)
823 p.second.m_Status = 0;
825 SendPlayerAssignments();
828 void CNetServerWorker::KickPlayer(const CStrW& playerName, const bool ban)
830 // Find the user with that name
831 std::vector<CNetServerSession*>::iterator it = std::find_if(m_Sessions.begin(), m_Sessions.end(),
832 [&](CNetServerSession* session) { return session->GetUserName() == playerName; });
834 // and return if no one or the host has that name
835 if (it == m_Sessions.end() || (*it)->GetGUID() == m_ControllerGUID)
836 return;
838 if (ban)
840 // Remember name
841 if (std::find(m_BannedPlayers.begin(), m_BannedPlayers.end(), playerName) == m_BannedPlayers.end())
842 m_BannedPlayers.push_back(m_LobbyAuth ? CStrW(playerName.substr(0, playerName.find(L" ("))) : playerName);
844 // Remember IP address
845 u32 ipAddress = (*it)->GetIPAddress();
846 if (std::find(m_BannedIPs.begin(), m_BannedIPs.end(), ipAddress) == m_BannedIPs.end())
847 m_BannedIPs.push_back(ipAddress);
850 // Disconnect that user
851 (*it)->Disconnect(ban ? NDR_BANNED : NDR_KICKED);
853 // Send message notifying other clients
854 CKickedMessage kickedMessage;
855 kickedMessage.m_Name = playerName;
856 kickedMessage.m_Ban = ban;
857 Broadcast(&kickedMessage, { NSS_PREGAME, NSS_JOIN_SYNCING, NSS_INGAME });
860 void CNetServerWorker::AssignPlayer(int playerID, const CStr& guid)
862 // Remove anyone who's already assigned to this player
863 for (std::pair<const CStr, PlayerAssignment>& p : m_PlayerAssignments)
865 if (p.second.m_PlayerID == playerID)
866 p.second.m_PlayerID = -1;
869 // Update this host's assignment if it exists
870 if (m_PlayerAssignments.find(guid) != m_PlayerAssignments.end())
871 m_PlayerAssignments[guid].m_PlayerID = playerID;
873 SendPlayerAssignments();
876 void CNetServerWorker::ConstructPlayerAssignmentMessage(CPlayerAssignmentMessage& message)
878 for (const std::pair<const CStr, PlayerAssignment>& p : m_PlayerAssignments)
880 if (!p.second.m_Enabled)
881 continue;
883 CPlayerAssignmentMessage::S_m_Hosts h;
884 h.m_GUID = p.first;
885 h.m_Name = p.second.m_Name;
886 h.m_PlayerID = p.second.m_PlayerID;
887 h.m_Status = p.second.m_Status;
888 message.m_Hosts.push_back(h);
892 void CNetServerWorker::SendPlayerAssignments()
894 CPlayerAssignmentMessage message;
895 ConstructPlayerAssignmentMessage(message);
896 Broadcast(&message, { NSS_PREGAME, NSS_JOIN_SYNCING, NSS_INGAME });
899 const ScriptInterface& CNetServerWorker::GetScriptInterface()
901 return *m_ScriptInterface;
904 void CNetServerWorker::SetTurnLength(u32 msecs)
906 if (m_ServerTurnManager)
907 m_ServerTurnManager->SetTurnLength(msecs);
910 void CNetServerWorker::ProcessLobbyAuth(const CStr& name, const CStr& token)
912 LOGMESSAGE("Net Server: Received lobby auth message from %s with %s", name, token);
913 // Find the user with that guid
914 std::vector<CNetServerSession*>::iterator it = std::find_if(m_Sessions.begin(), m_Sessions.end(),
915 [&](CNetServerSession* session)
916 { return session->GetGUID() == token; });
918 if (it == m_Sessions.end())
919 return;
921 (*it)->SetUserName(name.FromUTF8());
922 // Send an empty message to request the authentication message from the client
923 // after its identity has been confirmed via the lobby
924 CAuthenticateMessage emptyMessage;
925 (*it)->SendMessage(&emptyMessage);
928 bool CNetServerWorker::OnClientHandshake(void* context, CFsmEvent* event)
930 ENSURE(event->GetType() == (uint)NMT_CLIENT_HANDSHAKE);
932 CNetServerSession* session = (CNetServerSession*)context;
933 CNetServerWorker& server = session->GetServer();
935 CCliHandshakeMessage* message = (CCliHandshakeMessage*)event->GetParamRef();
936 if (message->m_ProtocolVersion != PS_PROTOCOL_VERSION)
938 session->Disconnect(NDR_INCORRECT_PROTOCOL_VERSION);
939 return false;
942 CStr guid = ps_generate_guid();
943 int count = 0;
944 // Ensure unique GUID
945 while(std::find_if(
946 server.m_Sessions.begin(), server.m_Sessions.end(),
947 [&guid] (const CNetServerSession* session)
948 { return session->GetGUID() == guid; }) != server.m_Sessions.end())
950 if (++count > 100)
952 session->Disconnect(NDR_GUID_FAILED);
953 return true;
955 guid = ps_generate_guid();
958 session->SetGUID(guid);
960 CSrvHandshakeResponseMessage handshakeResponse;
961 handshakeResponse.m_UseProtocolVersion = PS_PROTOCOL_VERSION;
962 handshakeResponse.m_GUID = guid;
963 handshakeResponse.m_Flags = 0;
965 if (server.m_LobbyAuth)
967 handshakeResponse.m_Flags |= PS_NETWORK_FLAG_REQUIRE_LOBBYAUTH;
968 session->SetNextState(NSS_LOBBY_AUTHENTICATE);
971 session->SendMessage(&handshakeResponse);
973 return true;
976 bool CNetServerWorker::OnAuthenticate(void* context, CFsmEvent* event)
978 ENSURE(event->GetType() == (uint)NMT_AUTHENTICATE);
980 CNetServerSession* session = (CNetServerSession*)context;
981 CNetServerWorker& server = session->GetServer();
983 // Prohibit joins while the game is loading
984 if (server.m_State == SERVER_STATE_LOADING)
986 LOGMESSAGE("Refused connection while the game is loading");
987 session->Disconnect(NDR_SERVER_LOADING);
988 return true;
991 CAuthenticateMessage* message = (CAuthenticateMessage*)event->GetParamRef();
992 CStrW username = SanitisePlayerName(message->m_Name);
993 CStrW usernameWithoutRating(username.substr(0, username.find(L" (")));
995 // Compare the lowercase names as specified by https://xmpp.org/extensions/xep-0029.html#sect-idm139493404168176
996 // "[...] comparisons will be made in case-normalized canonical form."
997 if (server.m_LobbyAuth && usernameWithoutRating.LowerCase() != session->GetUserName().LowerCase())
999 LOGERROR("Net server: lobby auth: %s tried joining as %s",
1000 session->GetUserName().ToUTF8(),
1001 usernameWithoutRating.ToUTF8());
1002 session->Disconnect(NDR_LOBBY_AUTH_FAILED);
1003 return true;
1006 // Check the password before anything else.
1007 // NB: m_Name must match the client's salt, @see CNetClient::SetGamePassword
1008 if (!server.CheckPassword(message->m_Password, message->m_Name.ToUTF8()))
1010 // Noisy logerror because players are not supposed to be able to get the IP,
1011 // so this might be someone targeting the host for some reason
1012 // (or TODO a dedicated server and we do want to log anyways)
1013 LOGERROR("Net server: user %s tried joining with the wrong password",
1014 session->GetUserName().ToUTF8());
1015 session->Disconnect(NDR_SERVER_REFUSED);
1016 return true;
1019 // Either deduplicate or prohibit join if name is in use
1020 bool duplicatePlayernames = false;
1021 CFG_GET_VAL("network.duplicateplayernames", duplicatePlayernames);
1022 // If lobby authentication is enabled, the clients playername has already been registered.
1023 // There also can't be any duplicated names.
1024 if (!server.m_LobbyAuth && duplicatePlayernames)
1025 username = server.DeduplicatePlayerName(username);
1026 else
1028 std::vector<CNetServerSession*>::iterator it = std::find_if(
1029 server.m_Sessions.begin(), server.m_Sessions.end(),
1030 [&username] (const CNetServerSession* session)
1031 { return session->GetUserName() == username; });
1033 if (it != server.m_Sessions.end() && (*it) != session)
1035 session->Disconnect(NDR_PLAYERNAME_IN_USE);
1036 return true;
1040 // Disconnect banned usernames
1041 if (std::find(server.m_BannedPlayers.begin(), server.m_BannedPlayers.end(), server.m_LobbyAuth ? usernameWithoutRating : username) != server.m_BannedPlayers.end())
1043 session->Disconnect(NDR_BANNED);
1044 return true;
1047 int maxObservers = 0;
1048 CFG_GET_VAL("network.observerlimit", maxObservers);
1050 bool isRejoining = false;
1051 bool serverFull = false;
1052 if (server.m_State == SERVER_STATE_PREGAME)
1054 // Don't check for maxObservers in the gamesetup, as we don't know yet who will be assigned
1055 serverFull = server.m_Sessions.size() >= MAX_CLIENTS;
1057 else
1059 bool isObserver = true;
1060 int disconnectedPlayers = 0;
1061 int connectedPlayers = 0;
1062 // (TODO: if GUIDs were stable, we should use them instead)
1063 for (const std::pair<const CStr, PlayerAssignment>& p : server.m_PlayerAssignments)
1065 const PlayerAssignment& assignment = p.second;
1067 if (!assignment.m_Enabled && assignment.m_Name == username)
1069 isObserver = assignment.m_PlayerID == -1;
1070 isRejoining = true;
1073 if (assignment.m_PlayerID == -1)
1074 continue;
1076 if (assignment.m_Enabled)
1077 ++connectedPlayers;
1078 else
1079 ++disconnectedPlayers;
1082 // Optionally allow everyone or only buddies to join after the game has started
1083 if (!isRejoining)
1085 CStr observerLateJoin;
1086 CFG_GET_VAL("network.lateobservers", observerLateJoin);
1088 if (observerLateJoin == "everyone")
1090 isRejoining = true;
1092 else if (observerLateJoin == "buddies")
1094 CStr buddies;
1095 CFG_GET_VAL("lobby.buddies", buddies);
1096 std::wstringstream buddiesStream(wstring_from_utf8(buddies));
1097 CStrW buddy;
1098 while (std::getline(buddiesStream, buddy, L','))
1100 if (buddy == usernameWithoutRating)
1102 isRejoining = true;
1103 break;
1109 if (!isRejoining)
1111 LOGMESSAGE("Refused connection after game start from not-previously-known user \"%s\"", utf8_from_wstring(username));
1112 session->Disconnect(NDR_SERVER_ALREADY_IN_GAME);
1113 return true;
1116 // Ensure all players will be able to rejoin
1117 serverFull = isObserver && (
1118 (int) server.m_Sessions.size() - connectedPlayers > maxObservers ||
1119 (int) server.m_Sessions.size() + disconnectedPlayers >= MAX_CLIENTS);
1122 if (serverFull)
1124 session->Disconnect(NDR_SERVER_FULL);
1125 return true;
1128 u32 newHostID = server.m_NextHostID++;
1130 session->SetUserName(username);
1131 session->SetHostID(newHostID);
1133 CAuthenticateResultMessage authenticateResult;
1134 authenticateResult.m_Code = isRejoining ? ARC_OK_REJOINING : ARC_OK;
1135 authenticateResult.m_HostID = newHostID;
1136 authenticateResult.m_Message = L"Logged in";
1137 authenticateResult.m_IsController = 0;
1139 if (message->m_ControllerSecret == server.m_ControllerSecret)
1141 if (server.m_ControllerGUID.empty())
1143 server.m_ControllerGUID = session->GetGUID();
1144 authenticateResult.m_IsController = 1;
1146 // TODO: we could probably handle having several controllers, or swapping?
1149 session->SendMessage(&authenticateResult);
1151 server.OnUserJoin(session);
1153 if (isRejoining)
1155 ENSURE(server.m_State != SERVER_STATE_UNCONNECTED && server.m_State != SERVER_STATE_PREGAME);
1157 // Request a copy of the current game state from an existing player,
1158 // so we can send it on to the new player
1160 // Assume session 0 is most likely the local player, so they're
1161 // the most efficient client to request a copy from
1162 CNetServerSession* sourceSession = server.m_Sessions.at(0);
1164 sourceSession->GetFileTransferer().StartTask(
1165 std::shared_ptr<CNetFileReceiveTask>(new CNetFileReceiveTask_ServerRejoin(server, newHostID))
1168 session->SetNextState(NSS_JOIN_SYNCING);
1171 return true;
1173 bool CNetServerWorker::OnSimulationCommand(void* context, CFsmEvent* event)
1175 ENSURE(event->GetType() == (uint)NMT_SIMULATION_COMMAND);
1177 CNetServerSession* session = (CNetServerSession*)context;
1178 CNetServerWorker& server = session->GetServer();
1180 CSimulationMessage* message = (CSimulationMessage*)event->GetParamRef();
1182 // Ignore messages sent by one player on behalf of another player
1183 // unless cheating is enabled
1184 bool cheatsEnabled = false;
1185 const ScriptInterface& scriptInterface = server.GetScriptInterface();
1186 ScriptRequest rq(scriptInterface);
1187 JS::RootedValue settings(rq.cx);
1188 Script::GetProperty(rq, server.m_InitAttributes, "settings", &settings);
1189 if (Script::HasProperty(rq, settings, "CheatsEnabled"))
1190 Script::GetProperty(rq, settings, "CheatsEnabled", cheatsEnabled);
1192 PlayerAssignmentMap::iterator it = server.m_PlayerAssignments.find(session->GetGUID());
1193 // When cheating is disabled, fail if the player the message claims to
1194 // represent does not exist or does not match the sender's player name
1195 if (!cheatsEnabled && (it == server.m_PlayerAssignments.end() || it->second.m_PlayerID != message->m_Player))
1196 return true;
1198 // Send it back to all clients that have finished
1199 // the loading screen (and the synchronization when rejoining)
1200 server.Broadcast(message, { NSS_INGAME });
1202 // Save all the received commands
1203 if (server.m_SavedCommands.size() < message->m_Turn + 1)
1204 server.m_SavedCommands.resize(message->m_Turn + 1);
1205 server.m_SavedCommands[message->m_Turn].push_back(*message);
1207 // TODO: we shouldn't send the message back to the client that first sent it
1208 return true;
1211 bool CNetServerWorker::OnSyncCheck(void* context, CFsmEvent* event)
1213 ENSURE(event->GetType() == (uint)NMT_SYNC_CHECK);
1215 CNetServerSession* session = (CNetServerSession*)context;
1216 CNetServerWorker& server = session->GetServer();
1218 CSyncCheckMessage* message = (CSyncCheckMessage*)event->GetParamRef();
1220 server.m_ServerTurnManager->NotifyFinishedClientUpdate(*session, message->m_Turn, message->m_Hash);
1221 return true;
1224 bool CNetServerWorker::OnEndCommandBatch(void* context, CFsmEvent* event)
1226 ENSURE(event->GetType() == (uint)NMT_END_COMMAND_BATCH);
1228 CNetServerSession* session = (CNetServerSession*)context;
1229 CNetServerWorker& server = session->GetServer();
1231 CEndCommandBatchMessage* message = (CEndCommandBatchMessage*)event->GetParamRef();
1233 // The turn-length field is ignored
1234 server.m_ServerTurnManager->NotifyFinishedClientCommands(*session, message->m_Turn);
1235 return true;
1238 bool CNetServerWorker::OnChat(void* context, CFsmEvent* event)
1240 ENSURE(event->GetType() == (uint)NMT_CHAT);
1242 CNetServerSession* session = (CNetServerSession*)context;
1243 CNetServerWorker& server = session->GetServer();
1245 CChatMessage* message = (CChatMessage*)event->GetParamRef();
1247 message->m_GUID = session->GetGUID();
1249 server.Broadcast(message, { NSS_PREGAME, NSS_INGAME });
1251 return true;
1254 bool CNetServerWorker::OnReady(void* context, CFsmEvent* event)
1256 ENSURE(event->GetType() == (uint)NMT_READY);
1258 CNetServerSession* session = (CNetServerSession*)context;
1259 CNetServerWorker& server = session->GetServer();
1261 // Occurs if a client presses not-ready
1262 // in the very last moment before the hosts starts the game
1263 if (server.m_State == SERVER_STATE_LOADING)
1264 return true;
1266 CReadyMessage* message = (CReadyMessage*)event->GetParamRef();
1267 message->m_GUID = session->GetGUID();
1268 server.Broadcast(message, { NSS_PREGAME });
1270 server.m_PlayerAssignments[message->m_GUID].m_Status = message->m_Status;
1272 return true;
1275 bool CNetServerWorker::OnClearAllReady(void* context, CFsmEvent* event)
1277 ENSURE(event->GetType() == (uint)NMT_CLEAR_ALL_READY);
1279 CNetServerSession* session = (CNetServerSession*)context;
1280 CNetServerWorker& server = session->GetServer();
1282 if (session->GetGUID() == server.m_ControllerGUID)
1283 server.ClearAllPlayerReady();
1285 return true;
1288 bool CNetServerWorker::OnGameSetup(void* context, CFsmEvent* event)
1290 ENSURE(event->GetType() == (uint)NMT_GAME_SETUP);
1292 CNetServerSession* session = (CNetServerSession*)context;
1293 CNetServerWorker& server = session->GetServer();
1295 // Changing the settings after gamestart is not implemented and would cause an Out-of-sync error.
1296 // This happened when doubleclicking on the startgame button.
1297 if (server.m_State != SERVER_STATE_PREGAME)
1298 return true;
1300 // Only the controller is allowed to send game setup updates.
1301 // TODO: it would be good to allow other players to request changes to some settings,
1302 // e.g. their civilisation.
1303 // Possibly this should use another message, to enforce a single source of truth.
1304 if (session->GetGUID() == server.m_ControllerGUID)
1306 CGameSetupMessage* message = (CGameSetupMessage*)event->GetParamRef();
1307 server.Broadcast(message, { NSS_PREGAME });
1309 return true;
1312 bool CNetServerWorker::OnAssignPlayer(void* context, CFsmEvent* event)
1314 ENSURE(event->GetType() == (uint)NMT_ASSIGN_PLAYER);
1315 CNetServerSession* session = (CNetServerSession*)context;
1316 CNetServerWorker& server = session->GetServer();
1318 if (session->GetGUID() == server.m_ControllerGUID)
1320 CAssignPlayerMessage* message = (CAssignPlayerMessage*)event->GetParamRef();
1321 server.AssignPlayer(message->m_PlayerID, message->m_GUID);
1323 return true;
1326 bool CNetServerWorker::OnGameStart(void* context, CFsmEvent* event)
1328 ENSURE(event->GetType() == (uint)NMT_GAME_START);
1329 CNetServerSession* session = (CNetServerSession*)context;
1330 CNetServerWorker& server = session->GetServer();
1332 if (session->GetGUID() != server.m_ControllerGUID)
1333 return true;
1335 CGameStartMessage* message = (CGameStartMessage*)event->GetParamRef();
1336 server.StartGame(message->m_InitAttributes);
1337 return true;
1340 bool CNetServerWorker::OnLoadedGame(void* context, CFsmEvent* event)
1342 ENSURE(event->GetType() == (uint)NMT_LOADED_GAME);
1344 CNetServerSession* loadedSession = (CNetServerSession*)context;
1345 CNetServerWorker& server = loadedSession->GetServer();
1347 // We're in the loading state, so wait until every client has loaded
1348 // before starting the game
1349 ENSURE(server.m_State == SERVER_STATE_LOADING);
1350 if (server.CheckGameLoadStatus(loadedSession))
1351 return true;
1353 CClientsLoadingMessage message;
1354 // We always send all GUIDs of clients in the loading state
1355 // so that we don't have to bother about switching GUI pages
1356 for (CNetServerSession* session : server.m_Sessions)
1357 if (session->GetCurrState() != NSS_INGAME && loadedSession->GetGUID() != session->GetGUID())
1359 CClientsLoadingMessage::S_m_Clients client;
1360 client.m_GUID = session->GetGUID();
1361 message.m_Clients.push_back(client);
1364 // Send to the client who has loaded the game but did not reach the NSS_INGAME state yet
1365 loadedSession->SendMessage(&message);
1366 server.Broadcast(&message, { NSS_INGAME });
1368 return true;
1371 bool CNetServerWorker::OnJoinSyncingLoadedGame(void* context, CFsmEvent* event)
1373 // A client rejoining an in-progress game has now finished loading the
1374 // map and deserialized the initial state.
1375 // The simulation may have progressed since then, so send any subsequent
1376 // commands to them and set them as an active player so they can participate
1377 // in all future turns.
1379 // (TODO: if it takes a long time for them to receive and execute all these
1380 // commands, the other players will get frozen for that time and may be unhappy;
1381 // we could try repeating this process a few times until the client converges
1382 // on the up-to-date state, before setting them as active.)
1384 ENSURE(event->GetType() == (uint)NMT_LOADED_GAME);
1386 CNetServerSession* session = (CNetServerSession*)context;
1387 CNetServerWorker& server = session->GetServer();
1389 CLoadedGameMessage* message = (CLoadedGameMessage*)event->GetParamRef();
1391 u32 turn = message->m_CurrentTurn;
1392 u32 readyTurn = server.m_ServerTurnManager->GetReadyTurn();
1394 // Send them all commands received since their saved state,
1395 // and turn-ended messages for any turns that have already been processed
1396 for (size_t i = turn + 1; i < std::max(readyTurn+1, (u32)server.m_SavedCommands.size()); ++i)
1398 if (i < server.m_SavedCommands.size())
1399 for (size_t j = 0; j < server.m_SavedCommands[i].size(); ++j)
1400 session->SendMessage(&server.m_SavedCommands[i][j]);
1402 if (i <= readyTurn)
1404 CEndCommandBatchMessage endMessage;
1405 endMessage.m_Turn = i;
1406 endMessage.m_TurnLength = server.m_ServerTurnManager->GetSavedTurnLength(i);
1407 session->SendMessage(&endMessage);
1411 // Tell the turn manager to expect commands from this new client
1412 // Special case: the controller shouldn't be treated as an observer in any case.
1413 bool isObserver = server.m_PlayerAssignments[session->GetGUID()].m_PlayerID == -1 && server.m_ControllerGUID != session->GetGUID();
1414 server.m_ServerTurnManager->InitialiseClient(session->GetHostID(), readyTurn, isObserver);
1416 // Tell the client that everything has finished loading and it should start now
1417 CLoadedGameMessage loaded;
1418 loaded.m_CurrentTurn = readyTurn;
1419 session->SendMessage(&loaded);
1421 return true;
1424 bool CNetServerWorker::OnRejoined(void* context, CFsmEvent* event)
1426 // A client has finished rejoining and the loading screen disappeared.
1427 ENSURE(event->GetType() == (uint)NMT_REJOINED);
1429 CNetServerSession* session = (CNetServerSession*)context;
1430 CNetServerWorker& server = session->GetServer();
1432 // Inform everyone of the client having rejoined
1433 CRejoinedMessage* message = (CRejoinedMessage*)event->GetParamRef();
1434 message->m_GUID = session->GetGUID();
1435 server.Broadcast(message, { NSS_INGAME });
1437 // Send all pausing players to the rejoined client.
1438 for (const CStr& guid : server.m_PausingPlayers)
1440 CClientPausedMessage pausedMessage;
1441 pausedMessage.m_GUID = guid;
1442 pausedMessage.m_Pause = true;
1443 session->SendMessage(&pausedMessage);
1446 return true;
1449 bool CNetServerWorker::OnKickPlayer(void* context, CFsmEvent* event)
1451 ENSURE(event->GetType() == (uint)NMT_KICKED);
1453 CNetServerSession* session = (CNetServerSession*)context;
1454 CNetServerWorker& server = session->GetServer();
1456 if (session->GetGUID() == server.m_ControllerGUID)
1458 CKickedMessage* message = (CKickedMessage*)event->GetParamRef();
1459 server.KickPlayer(message->m_Name, message->m_Ban);
1461 return true;
1464 bool CNetServerWorker::OnDisconnect(void* context, CFsmEvent* event)
1466 ENSURE(event->GetType() == (uint)NMT_CONNECTION_LOST);
1468 CNetServerSession* session = (CNetServerSession*)context;
1469 CNetServerWorker& server = session->GetServer();
1471 server.OnUserLeave(session);
1473 return true;
1476 bool CNetServerWorker::OnClientPaused(void* context, CFsmEvent* event)
1478 ENSURE(event->GetType() == (uint)NMT_CLIENT_PAUSED);
1480 CNetServerSession* session = (CNetServerSession*)context;
1481 CNetServerWorker& server = session->GetServer();
1483 CClientPausedMessage* message = (CClientPausedMessage*)event->GetParamRef();
1485 message->m_GUID = session->GetGUID();
1487 // Update the list of pausing players.
1488 std::vector<CStr>::iterator player = std::find(server.m_PausingPlayers.begin(), server.m_PausingPlayers.end(), session->GetGUID());
1490 if (message->m_Pause)
1492 if (player != server.m_PausingPlayers.end())
1493 return true;
1495 server.m_PausingPlayers.push_back(session->GetGUID());
1497 else
1499 if (player == server.m_PausingPlayers.end())
1500 return true;
1502 server.m_PausingPlayers.erase(player);
1505 // Send messages to clients that are in game, and are not the client who paused.
1506 for (CNetServerSession* netSession : server.m_Sessions)
1507 if (netSession->GetCurrState() == NSS_INGAME && message->m_GUID != netSession->GetGUID())
1508 netSession->SendMessage(message);
1510 return true;
1513 bool CNetServerWorker::CheckGameLoadStatus(CNetServerSession* changedSession)
1515 for (const CNetServerSession* session : m_Sessions)
1516 if (session != changedSession && session->GetCurrState() != NSS_INGAME)
1517 return false;
1519 // Inform clients that everyone has loaded the map and that the game can start
1520 CLoadedGameMessage loaded;
1521 loaded.m_CurrentTurn = 0;
1523 // Notice the changedSession is still in the NSS_PREGAME state
1524 Broadcast(&loaded, { NSS_PREGAME, NSS_INGAME });
1526 m_State = SERVER_STATE_INGAME;
1527 return true;
1530 void CNetServerWorker::StartGame(const CStr& initAttribs)
1532 for (std::pair<const CStr, PlayerAssignment>& player : m_PlayerAssignments)
1533 if (player.second.m_Enabled && player.second.m_PlayerID != -1 && player.second.m_Status == 0)
1535 LOGERROR("Tried to start the game without player \"%s\" being ready!", utf8_from_wstring(player.second.m_Name).c_str());
1536 return;
1539 m_ServerTurnManager = new CNetServerTurnManager(*this);
1541 for (CNetServerSession* session : m_Sessions)
1543 // Special case: the controller shouldn't be treated as an observer in any case.
1544 bool isObserver = m_PlayerAssignments[session->GetGUID()].m_PlayerID == -1 && m_ControllerGUID != session->GetGUID();
1545 m_ServerTurnManager->InitialiseClient(session->GetHostID(), 0, isObserver);
1548 m_State = SERVER_STATE_LOADING;
1550 // Remove players and observers that are not present when the game starts
1551 for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end();)
1552 if (it->second.m_Enabled)
1553 ++it;
1554 else
1555 it = m_PlayerAssignments.erase(it);
1557 SendPlayerAssignments();
1559 // Update init attributes. They should no longer change.
1560 Script::ParseJSON(ScriptRequest(m_ScriptInterface), initAttribs, &m_InitAttributes);
1562 CGameStartMessage gameStart;
1563 gameStart.m_InitAttributes = initAttribs;
1564 Broadcast(&gameStart, { NSS_PREGAME });
1567 CStrW CNetServerWorker::SanitisePlayerName(const CStrW& original)
1569 const size_t MAX_LENGTH = 32;
1571 CStrW name = original;
1572 name.Replace(L"[", L"{"); // remove GUI tags
1573 name.Replace(L"]", L"}"); // remove for symmetry
1575 // Restrict the length
1576 if (name.length() > MAX_LENGTH)
1577 name = name.Left(MAX_LENGTH);
1579 // Don't allow surrounding whitespace
1580 name.Trim(PS_TRIM_BOTH);
1582 // Don't allow empty name
1583 if (name.empty())
1584 name = L"Anonymous";
1586 return name;
1589 CStrW CNetServerWorker::DeduplicatePlayerName(const CStrW& original)
1591 CStrW name = original;
1593 // Try names "Foo", "Foo (2)", "Foo (3)", etc
1594 size_t id = 2;
1595 while (true)
1597 bool unique = true;
1598 for (const CNetServerSession* session : m_Sessions)
1600 if (session->GetUserName() == name)
1602 unique = false;
1603 break;
1607 if (unique)
1608 return name;
1610 name = original + L" (" + CStrW::FromUInt(id++) + L")";
1614 void CNetServerWorker::SendHolePunchingMessage(const CStr& ipStr, u16 port)
1616 if (m_Host)
1617 StunClient::SendHolePunchingMessages(*m_Host, ipStr, port);
1623 CNetServer::CNetServer(bool useLobbyAuth) :
1624 m_Worker(new CNetServerWorker(useLobbyAuth)),
1625 m_LobbyAuth(useLobbyAuth), m_UseSTUN(false), m_PublicIp(""), m_PublicPort(20595), m_Password()
1629 CNetServer::~CNetServer()
1631 delete m_Worker;
1634 bool CNetServer::GetUseSTUN() const
1636 return m_UseSTUN;
1639 bool CNetServer::UseLobbyAuth() const
1641 return m_LobbyAuth;
1644 bool CNetServer::SetupConnection(const u16 port)
1646 return m_Worker->SetupConnection(port);
1649 CStr CNetServer::GetPublicIp() const
1651 return m_PublicIp;
1654 u16 CNetServer::GetPublicPort() const
1656 return m_PublicPort;
1659 u16 CNetServer::GetLocalPort() const
1661 std::lock_guard<std::mutex> lock(m_Worker->m_WorkerMutex);
1662 if (!m_Worker->m_Host)
1663 return 0;
1664 return m_Worker->m_Host->address.port;
1667 void CNetServer::SetConnectionData(const CStr& ip, const u16 port)
1669 m_PublicIp = ip;
1670 m_PublicPort = port;
1671 m_UseSTUN = false;
1674 bool CNetServer::SetConnectionDataViaSTUN()
1676 m_UseSTUN = true;
1677 std::lock_guard<std::mutex> lock(m_Worker->m_WorkerMutex);
1678 if (!m_Worker->m_Host)
1679 return false;
1680 return StunClient::FindPublicIP(*m_Worker->m_Host, m_PublicIp, m_PublicPort);
1683 bool CNetServer::CheckPasswordAndIncrement(const std::string& username, const std::string& password, const std::string& salt)
1685 std::unordered_map<std::string, int>::iterator it = m_FailedAttempts.find(username);
1686 if (m_Worker->CheckPassword(password, salt))
1688 if (it != m_FailedAttempts.end())
1689 it->second = 0;
1690 return true;
1692 if (it == m_FailedAttempts.end())
1693 m_FailedAttempts.emplace(username, 1);
1694 else
1695 it->second++;
1696 return false;
1699 bool CNetServer::IsBanned(const std::string& username) const
1701 std::unordered_map<std::string, int>::const_iterator it = m_FailedAttempts.find(username);
1702 return it != m_FailedAttempts.end() && it->second >= FAILED_PASSWORD_TRIES_BEFORE_BAN;
1705 void CNetServer::SetPassword(const CStr& password)
1707 m_Password = password;
1708 std::lock_guard<std::mutex> lock(m_Worker->m_WorkerMutex);
1709 m_Worker->SetPassword(password);
1712 void CNetServer::SetControllerSecret(const std::string& secret)
1714 std::lock_guard<std::mutex> lock(m_Worker->m_WorkerMutex);
1715 m_Worker->SetControllerSecret(secret);
1718 void CNetServer::StartGame()
1720 std::lock_guard<std::mutex> lock(m_Worker->m_WorkerMutex);
1721 m_Worker->m_StartGameQueue.push_back(true);
1724 void CNetServer::UpdateInitAttributes(JS::MutableHandleValue attrs, const ScriptRequest& rq)
1726 // Pass the attributes as JSON, since that's the easiest safe
1727 // cross-thread way of passing script data
1728 std::string attrsJSON = Script::StringifyJSON(rq, attrs, false);
1730 std::lock_guard<std::mutex> lock(m_Worker->m_WorkerMutex);
1731 m_Worker->m_InitAttributesQueue.push_back(attrsJSON);
1734 void CNetServer::OnLobbyAuth(const CStr& name, const CStr& token)
1736 std::lock_guard<std::mutex> lock(m_Worker->m_WorkerMutex);
1737 m_Worker->m_LobbyAuthQueue.push_back(std::make_pair(name, token));
1740 void CNetServer::SetTurnLength(u32 msecs)
1742 std::lock_guard<std::mutex> lock(m_Worker->m_WorkerMutex);
1743 m_Worker->m_TurnLengthQueue.push_back(msecs);
1746 void CNetServer::SendHolePunchingMessage(const CStr& ip, u16 port)
1748 m_Worker->SendHolePunchingMessage(ip, port);