Merge 'remotes/trunk'
[0ad.git] / source / network / NetServer.cpp
blob7f22be1a1bbc5cb9b481d311525875e976410706
1 /* Copyright (C) 2024 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 <set>
52 #include <string>
54 /**
55 * Number of peers to allocate for the enet host.
56 * Limited by ENET_PROTOCOL_MAXIMUM_PEER_ID (4096).
58 * At most 8 players, 32 observers and 1 temporary connection to send the "server full" disconnect-reason.
60 #define MAX_CLIENTS 41
62 #define DEFAULT_SERVER_NAME L"Unnamed Server"
64 constexpr int CHANNEL_COUNT = 1;
65 constexpr int FAILED_PASSWORD_TRIES_BEFORE_BAN = 3;
67 /**
68 * enet_host_service timeout (msecs).
69 * Smaller numbers may hurt performance; larger numbers will
70 * hurt latency responding to messages from game thread.
72 static const int HOST_SERVICE_TIMEOUT = 50;
74 /**
75 * Once ping goes above turn length * command delay,
76 * the game will start 'freezing' for other clients while we catch up.
77 * Since commands are sent client -> server -> client, divide by 2.
78 * (duplicated in NetServer.cpp to avoid having to fetch the constants in a header file)
80 constexpr u32 NETWORK_BAD_PING = DEFAULT_TURN_LENGTH * COMMAND_DELAY_MP / 2;
82 CNetServer* g_NetServer = NULL;
84 static CStr DebugName(CNetServerSession* session)
86 if (session == NULL)
87 return "[unknown host]";
88 if (session->GetGUID().empty())
89 return "[unauthed host]";
90 return "[" + session->GetGUID().substr(0, 8) + "...]";
94 * XXX: We use some non-threadsafe functions from the worker thread.
95 * See http://trac.wildfiregames.com/ticket/654
98 CNetServerWorker::CNetServerWorker(bool useLobbyAuth) :
99 m_LobbyAuth(useLobbyAuth),
100 m_Shutdown(false),
101 m_ScriptInterface(NULL),
102 m_NextHostID(1), m_Host(NULL), m_ControllerGUID(), m_Stats(NULL),
103 m_LastConnectionCheck(0)
105 m_State = SERVER_STATE_UNCONNECTED;
107 m_ServerTurnManager = NULL;
109 m_ServerName = DEFAULT_SERVER_NAME;
112 CNetServerWorker::~CNetServerWorker()
114 if (m_State != SERVER_STATE_UNCONNECTED)
116 // Tell the thread to shut down
118 std::lock_guard<std::mutex> lock(m_WorkerMutex);
119 m_Shutdown = true;
122 // Wait for it to shut down cleanly
123 m_WorkerThread.join();
126 #if CONFIG2_MINIUPNPC
127 if (m_UPnPThread.joinable())
128 m_UPnPThread.detach();
129 #endif
131 // Clean up resources
133 delete m_Stats;
135 for (CNetServerSession* session : m_Sessions)
137 session->DisconnectNow(NDR_SERVER_SHUTDOWN);
138 delete session;
141 if (m_Host)
142 enet_host_destroy(m_Host);
144 delete m_ServerTurnManager;
147 void CNetServerWorker::SetPassword(const CStr& hashedPassword)
149 m_Password = hashedPassword;
153 void CNetServerWorker::SetControllerSecret(const std::string& secret)
155 m_ControllerSecret = secret;
159 bool CNetServerWorker::CheckPassword(const std::string& password, const std::string& salt) const
161 return HashCryptographically(m_Password, salt) == password;
165 bool CNetServerWorker::SetupConnection(const u16 port)
167 ENSURE(m_State == SERVER_STATE_UNCONNECTED);
168 ENSURE(!m_Host);
170 // Bind to default host
171 ENetAddress addr;
172 addr.host = ENET_HOST_ANY;
173 addr.port = port;
175 // Create ENet server
176 m_Host = PS::Enet::CreateHost(&addr, MAX_CLIENTS, CHANNEL_COUNT);
177 if (!m_Host)
179 LOGERROR("Net server: enet_host_create failed");
180 return false;
183 m_Stats = new CNetStatsTable();
184 if (CProfileViewer::IsInitialised())
185 g_ProfileViewer.AddRootTable(m_Stats);
187 m_State = SERVER_STATE_PREGAME;
189 // Launch the worker thread
190 m_WorkerThread = std::thread(Threading::HandleExceptions<RunThread>::Wrapper, this);
192 #if CONFIG2_MINIUPNPC
193 // Launch the UPnP thread
194 m_UPnPThread = std::thread(Threading::HandleExceptions<SetupUPnP>::Wrapper);
195 #endif
197 return true;
200 #if CONFIG2_MINIUPNPC
201 void CNetServerWorker::SetupUPnP()
203 debug_SetThreadName("UPnP");
205 // Values we want to set.
206 char psPort[6];
207 sprintf_s(psPort, ARRAY_SIZE(psPort), "%d", PS_DEFAULT_PORT);
208 const char* leaseDuration = "0"; // Indefinite/permanent lease duration.
209 const char* description = "0AD Multiplayer";
210 const char* protocall = "UDP";
211 char internalIPAddress[64];
212 char externalIPAddress[40];
214 // Variables to hold the values that actually get set.
215 char intClient[40];
216 char intPort[6];
217 char duration[16];
219 // Intermediate variables.
220 bool allocatedUrls = false;
221 struct UPNPUrls urls;
222 struct IGDdatas data;
223 struct UPNPDev* devlist = NULL;
225 // Make sure everything is properly freed.
226 std::function<void()> freeUPnP = [&allocatedUrls, &urls, &devlist]()
228 if (allocatedUrls)
229 FreeUPNPUrls(&urls);
230 freeUPNPDevlist(devlist);
231 // IGDdatas does not need to be freed according to UPNP_GetIGDFromUrl
234 // Cached root descriptor URL.
235 std::string rootDescURL;
236 CFG_GET_VAL("network.upnprootdescurl", rootDescURL);
237 if (!rootDescURL.empty())
238 LOGMESSAGE("Net server: attempting to use cached root descriptor URL: %s", rootDescURL.c_str());
240 int ret = 0;
242 // Try a cached URL first
243 if (!rootDescURL.empty() && UPNP_GetIGDFromUrl(rootDescURL.c_str(), &urls, &data, internalIPAddress, sizeof(internalIPAddress)))
245 LOGMESSAGE("Net server: using cached IGD = %s", urls.controlURL);
246 ret = 1;
248 // No cached URL, or it did not respond. Try getting a valid UPnP device for 10 seconds.
249 #if defined(MINIUPNPC_API_VERSION) && MINIUPNPC_API_VERSION >= 14
250 else if ((devlist = upnpDiscover(10000, 0, 0, 0, 0, 2, 0)) != NULL)
251 #else
252 else if ((devlist = upnpDiscover(10000, 0, 0, 0, 0, 0)) != NULL)
253 #endif
255 #if defined(MINIUPNPC_API_VERSION) && MINIUPNPC_API_VERSION >= 18
256 ret = UPNP_GetValidIGD(devlist, &urls, &data, internalIPAddress, sizeof(internalIPAddress), nullptr, 0);
257 #else
258 ret = UPNP_GetValidIGD(devlist, &urls, &data, internalIPAddress, sizeof(internalIPAddress));
259 #endif
260 allocatedUrls = ret != 0; // urls is allocated on non-zero return values
262 else
264 LOGMESSAGE("Net server: upnpDiscover failed and no working cached URL.");
265 freeUPnP();
266 return;
269 switch (ret)
271 case 0:
272 LOGMESSAGE("Net server: No IGD found");
273 break;
274 case 1:
275 LOGMESSAGE("Net server: found valid IGD = %s", urls.controlURL);
276 break;
277 #if defined(MINIUPNPC_API_VERSION) && MINIUPNPC_API_VERSION >= 18
278 case 2:
279 LOGMESSAGE("Net server: found IGD with reserved IP = %s, will try to continue anyway", urls.controlURL);
280 break;
281 case 3:
282 #else
283 case 2:
284 #endif
285 LOGMESSAGE("Net server: found a valid, not connected IGD = %s, will try to continue anyway", urls.controlURL);
286 break;
287 #if defined(MINIUPNPC_API_VERSION) && MINIUPNPC_API_VERSION >= 18
288 case 4:
289 #else
290 case 3:
291 #endif
292 LOGMESSAGE("Net server: found a UPnP device unrecognized as IGD = %s, will try to continue anyway", urls.controlURL);
293 break;
294 default:
295 debug_warn(L"Unrecognized return value from UPNP_GetValidIGD");
298 // Try getting our external/internet facing IP. TODO: Display this on the game-setup page for convenience.
299 ret = UPNP_GetExternalIPAddress(urls.controlURL, data.first.servicetype, externalIPAddress);
300 if (ret != UPNPCOMMAND_SUCCESS)
302 LOGMESSAGE("Net server: GetExternalIPAddress failed with code %d (%s)", ret, strupnperror(ret));
303 freeUPnP();
304 return;
306 LOGMESSAGE("Net server: ExternalIPAddress = %s", externalIPAddress);
308 // Try to setup port forwarding.
309 ret = UPNP_AddPortMapping(urls.controlURL, data.first.servicetype, psPort, psPort,
310 internalIPAddress, description, protocall, 0, leaseDuration);
311 if (ret != UPNPCOMMAND_SUCCESS)
313 LOGMESSAGE("Net server: AddPortMapping(%s, %s, %s) failed with code %d (%s)",
314 psPort, psPort, internalIPAddress, ret, strupnperror(ret));
315 freeUPnP();
316 return;
319 // Check that the port was actually forwarded.
320 ret = UPNP_GetSpecificPortMappingEntry(urls.controlURL,
321 data.first.servicetype,
322 psPort, protocall,
323 #if defined(MINIUPNPC_API_VERSION) && MINIUPNPC_API_VERSION >= 10
324 NULL/*remoteHost*/,
325 #endif
326 intClient, intPort, NULL/*desc*/,
327 NULL/*enabled*/, duration);
329 if (ret != UPNPCOMMAND_SUCCESS)
331 LOGMESSAGE("Net server: GetSpecificPortMappingEntry() failed with code %d (%s)", ret, strupnperror(ret));
332 freeUPnP();
333 return;
336 LOGMESSAGE("Net server: External %s:%s %s is redirected to internal %s:%s (duration=%s)",
337 externalIPAddress, psPort, protocall, intClient, intPort, duration);
339 // Cache root descriptor URL to try to avoid discovery next time.
340 g_ConfigDB.SetValueString(CFG_USER, "network.upnprootdescurl", urls.controlURL);
341 g_ConfigDB.WriteValueToFile(CFG_USER, "network.upnprootdescurl", urls.controlURL);
342 LOGMESSAGE("Net server: cached UPnP root descriptor URL as %s", urls.controlURL);
344 freeUPnP();
346 #endif // CONFIG2_MINIUPNPC
348 bool CNetServerWorker::SendMessage(ENetPeer* peer, const CNetMessage* message)
350 ENSURE(m_Host);
352 CNetServerSession* session = static_cast<CNetServerSession*>(peer->data);
354 return CNetHost::SendMessage(message, peer, DebugName(session).c_str());
357 bool CNetServerWorker::Broadcast(const CNetMessage* message, const std::vector<NetServerSessionState>& targetStates)
359 ENSURE(m_Host);
361 bool ok = true;
363 // TODO: this does lots of repeated message serialisation if we have lots
364 // of remote peers; could do it more efficiently if that's a real problem
366 for (CNetServerSession* session : m_Sessions)
367 if (std::find(targetStates.begin(), targetStates.end(), static_cast<NetServerSessionState>(session->GetCurrState())) != targetStates.end() &&
368 !session->SendMessage(message))
369 ok = false;
371 return ok;
374 void CNetServerWorker::RunThread(CNetServerWorker* data)
376 debug_SetThreadName("NetServer");
378 data->Run();
381 void CNetServerWorker::Run()
383 // The script context uses the profiler and therefore the thread must be registered before the context is created
384 g_Profiler2.RegisterCurrentThread("Net server");
386 // We create a new ScriptContext for this network thread, with a single ScriptInterface.
387 std::shared_ptr<ScriptContext> netServerContext = ScriptContext::CreateContext();
388 m_ScriptInterface = new ScriptInterface("Engine", "Net server", netServerContext);
389 m_InitAttributes.init(m_ScriptInterface->GetGeneralJSContext(), JS::UndefinedValue());
391 while (true)
393 if (!RunStep())
394 break;
396 // Update profiler stats
397 m_Stats->LatchHostState(m_Host);
400 // Clear roots before deleting their context
401 m_SavedCommands.clear();
403 SAFE_DELETE(m_ScriptInterface);
406 bool CNetServerWorker::RunStep()
408 // Check for messages from the game thread.
409 // (Do as little work as possible while the mutex is held open,
410 // to avoid performance problems and deadlocks.)
412 m_ScriptInterface->GetContext().MaybeIncrementalGC(0.5f);
414 ScriptRequest rq(m_ScriptInterface);
416 std::vector<bool> newStartGame;
417 std::vector<std::string> newGameAttributes;
418 std::vector<std::pair<CStr, CStr>> newLobbyAuths;
419 std::vector<u32> newTurnLength;
422 std::lock_guard<std::mutex> lock(m_WorkerMutex);
424 if (m_Shutdown)
425 return false;
427 newStartGame.swap(m_StartGameQueue);
428 newGameAttributes.swap(m_InitAttributesQueue);
429 newLobbyAuths.swap(m_LobbyAuthQueue);
430 newTurnLength.swap(m_TurnLengthQueue);
433 if (!newGameAttributes.empty())
435 if (m_State != SERVER_STATE_UNCONNECTED && m_State != SERVER_STATE_PREGAME)
436 LOGERROR("NetServer: Init Attributes cannot be changed after the server starts loading.");
437 else
439 JS::RootedValue gameAttributesVal(rq.cx);
440 Script::ParseJSON(rq, newGameAttributes.back(), &gameAttributesVal);
441 m_InitAttributes = gameAttributesVal;
445 if (!newTurnLength.empty())
446 SetTurnLength(newTurnLength.back());
448 while (!newLobbyAuths.empty())
450 const std::pair<CStr, CStr>& auth = newLobbyAuths.back();
451 ProcessLobbyAuth(auth.first, auth.second);
452 newLobbyAuths.pop_back();
455 // Perform file transfers
456 for (CNetServerSession* session : m_Sessions)
457 session->GetFileTransferer().Poll();
459 CheckClientConnections();
461 // Process network events:
463 ENetEvent event;
464 int status = enet_host_service(m_Host, &event, HOST_SERVICE_TIMEOUT);
465 if (status < 0)
467 LOGERROR("CNetServerWorker: enet_host_service failed (%d)", status);
468 // TODO: notify game that the server has shut down
469 return false;
472 if (status == 0)
474 // Reached timeout with no events - try again
475 return true;
478 // Process the event:
480 switch (event.type)
482 case ENET_EVENT_TYPE_CONNECT:
484 // Report the client address
485 char hostname[256] = "(error)";
486 enet_address_get_host_ip(&event.peer->address, hostname, ARRAY_SIZE(hostname));
487 LOGMESSAGE("Net server: Received connection from %s:%u", hostname, (unsigned int)event.peer->address.port);
489 // Set up a session object for this peer
491 CNetServerSession* session = new CNetServerSession(*this, event.peer);
493 m_Sessions.push_back(session);
495 SetupSession(session);
497 ENSURE(event.peer->data == NULL);
498 event.peer->data = session;
500 HandleConnect(session);
502 break;
505 case ENET_EVENT_TYPE_DISCONNECT:
507 // If there is an active session with this peer, then reset and delete it
509 CNetServerSession* session = static_cast<CNetServerSession*>(event.peer->data);
510 if (session)
512 LOGMESSAGE("Net server: Disconnected %s", DebugName(session).c_str());
514 // Remove the session first, so we won't send player-update messages to it
515 // when updating the FSM
516 m_Sessions.erase(remove(m_Sessions.begin(), m_Sessions.end(), session), m_Sessions.end());
518 session->Update((uint)NMT_CONNECTION_LOST, NULL);
520 delete session;
521 event.peer->data = NULL;
524 if (m_State == SERVER_STATE_LOADING)
525 CheckGameLoadStatus(NULL);
527 break;
530 case ENET_EVENT_TYPE_RECEIVE:
532 // If there is an active session with this peer, then process the message
534 CNetServerSession* session = static_cast<CNetServerSession*>(event.peer->data);
535 if (session)
537 // Create message from raw data
538 CNetMessage* msg = CNetMessageFactory::CreateMessage(event.packet->data, event.packet->dataLength, GetScriptInterface());
539 if (msg)
541 LOGMESSAGE("Net server: Received message %s of size %lu from %s", msg->ToString().c_str(), (unsigned long)msg->GetSerializedLength(), DebugName(session).c_str());
543 HandleMessageReceive(msg, session);
545 delete msg;
549 // Done using the packet
550 enet_packet_destroy(event.packet);
552 break;
555 case ENET_EVENT_TYPE_NONE:
556 break;
559 return true;
562 void CNetServerWorker::CheckClientConnections()
564 // Send messages at most once per second
565 std::time_t now = std::time(nullptr);
566 if (now <= m_LastConnectionCheck)
567 return;
569 m_LastConnectionCheck = now;
571 for (size_t i = 0; i < m_Sessions.size(); ++i)
573 u32 lastReceived = m_Sessions[i]->GetLastReceivedTime();
574 u32 meanRTT = m_Sessions[i]->GetMeanRTT();
576 CNetMessage* message = nullptr;
578 // Report if we didn't hear from the client since few seconds
579 if (lastReceived > NETWORK_WARNING_TIMEOUT)
581 CClientTimeoutMessage* msg = new CClientTimeoutMessage();
582 msg->m_GUID = m_Sessions[i]->GetGUID();
583 msg->m_LastReceivedTime = lastReceived;
584 message = msg;
586 // Report if the client has bad ping
587 else if (meanRTT > NETWORK_BAD_PING)
589 CClientPerformanceMessage* msg = new CClientPerformanceMessage();
590 CClientPerformanceMessage::S_m_Clients client;
591 client.m_GUID = m_Sessions[i]->GetGUID();
592 client.m_MeanRTT = meanRTT;
593 msg->m_Clients.push_back(client);
594 message = msg;
597 // Send to all clients except the affected one
598 // (since that will show the locally triggered warning instead).
599 // Also send it to clients that finished the loading screen while
600 // the game is still waiting for other clients to finish the loading screen.
601 if (message)
602 for (size_t j = 0; j < m_Sessions.size(); ++j)
604 if (i != j && (
605 (m_Sessions[j]->GetCurrState() == NSS_PREGAME && m_State == SERVER_STATE_PREGAME) ||
606 m_Sessions[j]->GetCurrState() == NSS_INGAME))
608 m_Sessions[j]->SendMessage(message);
612 SAFE_DELETE(message);
616 void CNetServerWorker::HandleMessageReceive(const CNetMessage* message, CNetServerSession* session)
618 // Handle non-FSM messages first
619 Status status = session->GetFileTransferer().HandleMessageReceive(*message);
620 if (status != INFO::SKIPPED)
621 return;
623 if (message->GetType() == NMT_FILE_TRANSFER_REQUEST)
625 CFileTransferRequestMessage* reqMessage = (CFileTransferRequestMessage*)message;
627 // Rejoining client got our JoinSyncStart after we received the state from
628 // another client, and has now requested that we forward it to them
630 ENSURE(!m_JoinSyncFile.empty());
631 session->GetFileTransferer().StartResponse(reqMessage->m_RequestID, m_JoinSyncFile);
633 return;
636 // Update FSM
637 if (!session->Update(message->GetType(), (void*)message))
638 LOGERROR("Net server: Error running FSM update (type=%d state=%d)", (int)message->GetType(), (int)session->GetCurrState());
641 void CNetServerWorker::SetupSession(CNetServerSession* session)
643 // Set up transitions for session
645 session->AddTransition(NSS_UNCONNECTED, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
647 session->AddTransition(NSS_HANDSHAKE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
648 session->AddTransition(NSS_HANDSHAKE, (uint)NMT_CLIENT_HANDSHAKE, NSS_AUTHENTICATE, &OnClientHandshake, session);
650 session->AddTransition(NSS_LOBBY_AUTHENTICATE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
651 session->AddTransition(NSS_LOBBY_AUTHENTICATE, (uint)NMT_AUTHENTICATE, NSS_PREGAME, &OnAuthenticate, session);
653 session->AddTransition(NSS_AUTHENTICATE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
654 session->AddTransition(NSS_AUTHENTICATE, (uint)NMT_AUTHENTICATE, NSS_PREGAME, &OnAuthenticate, session);
656 session->AddTransition(NSS_PREGAME, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, &OnDisconnect, session);
657 session->AddTransition(NSS_PREGAME, (uint)NMT_CHAT, NSS_PREGAME, &OnChat, session);
658 session->AddTransition(NSS_PREGAME, (uint)NMT_READY, NSS_PREGAME, &OnReady, session);
659 session->AddTransition(NSS_PREGAME, (uint)NMT_CLEAR_ALL_READY, NSS_PREGAME, &OnClearAllReady, session);
660 session->AddTransition(NSS_PREGAME, (uint)NMT_GAME_SETUP, NSS_PREGAME, &OnGameSetup, session);
661 session->AddTransition(NSS_PREGAME, (uint)NMT_ASSIGN_PLAYER, NSS_PREGAME, &OnAssignPlayer, session);
662 session->AddTransition(NSS_PREGAME, (uint)NMT_KICKED, NSS_PREGAME, &OnKickPlayer, session);
663 session->AddTransition(NSS_PREGAME, (uint)NMT_GAME_START, NSS_PREGAME, &OnGameStart, session);
664 session->AddTransition(NSS_PREGAME, (uint)NMT_LOADED_GAME, NSS_INGAME, &OnLoadedGame, session);
666 session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_KICKED, NSS_JOIN_SYNCING, &OnKickPlayer, session);
667 session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, &OnDisconnect, session);
668 session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_LOADED_GAME, NSS_INGAME, &OnJoinSyncingLoadedGame, session);
670 session->AddTransition(NSS_INGAME, (uint)NMT_REJOINED, NSS_INGAME, &OnRejoined, session);
671 session->AddTransition(NSS_INGAME, (uint)NMT_KICKED, NSS_INGAME, &OnKickPlayer, session);
672 session->AddTransition(NSS_INGAME, (uint)NMT_CLIENT_PAUSED, NSS_INGAME, &OnClientPaused, session);
673 session->AddTransition(NSS_INGAME, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, &OnDisconnect, session);
674 session->AddTransition(NSS_INGAME, (uint)NMT_CHAT, NSS_INGAME, &OnChat, session);
675 session->AddTransition(NSS_INGAME, (uint)NMT_SIMULATION_COMMAND, NSS_INGAME, &OnSimulationCommand, session);
676 session->AddTransition(NSS_INGAME, (uint)NMT_SYNC_CHECK, NSS_INGAME, &OnSyncCheck, session);
677 session->AddTransition(NSS_INGAME, (uint)NMT_END_COMMAND_BATCH, NSS_INGAME, &OnEndCommandBatch, session);
679 // Set first state
680 session->SetFirstState(NSS_HANDSHAKE);
683 bool CNetServerWorker::HandleConnect(CNetServerSession* session)
685 if (std::find(m_BannedIPs.begin(), m_BannedIPs.end(), session->GetIPAddress()) != m_BannedIPs.end())
687 session->Disconnect(NDR_BANNED);
688 return false;
691 CSrvHandshakeMessage handshake;
692 handshake.m_Magic = PS_PROTOCOL_MAGIC;
693 handshake.m_ProtocolVersion = PS_PROTOCOL_VERSION;
694 handshake.m_SoftwareVersion = PS_PROTOCOL_VERSION;
695 return session->SendMessage(&handshake);
698 void CNetServerWorker::OnUserJoin(CNetServerSession* session)
700 AddPlayer(session->GetGUID(), session->GetUserName());
702 CPlayerAssignmentMessage assignMessage;
703 ConstructPlayerAssignmentMessage(assignMessage);
704 session->SendMessage(&assignMessage);
707 void CNetServerWorker::OnUserLeave(CNetServerSession* session)
709 std::vector<CStr>::iterator pausing = std::find(m_PausingPlayers.begin(), m_PausingPlayers.end(), session->GetGUID());
710 if (pausing != m_PausingPlayers.end())
711 m_PausingPlayers.erase(pausing);
713 RemovePlayer(session->GetGUID());
715 if (m_ServerTurnManager && session->GetCurrState() != NSS_JOIN_SYNCING)
716 m_ServerTurnManager->UninitialiseClient(session->GetHostID());
718 // TODO: ought to switch the player controlled by that client
719 // back to AI control, or something?
722 void CNetServerWorker::AddPlayer(const CStr& guid, const CStrW& name)
724 // Find all player IDs in active use; we mustn't give them to a second player (excluding the unassigned ID: -1)
725 std::set<i32> usedIDs;
726 for (const std::pair<const CStr, PlayerAssignment>& p : m_PlayerAssignments)
727 if (p.second.m_Enabled && p.second.m_PlayerID != -1)
728 usedIDs.insert(p.second.m_PlayerID);
730 // If the player is rejoining after disconnecting, try to give them
731 // back their old player ID. Don't do this in pregame however,
732 // as that ID might be invalid for various reasons.
734 i32 playerID = -1;
736 if (m_State != SERVER_STATE_UNCONNECTED && m_State != SERVER_STATE_PREGAME)
738 // Try to match GUID first
739 for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it)
741 if (!it->second.m_Enabled && it->first == guid && usedIDs.find(it->second.m_PlayerID) == usedIDs.end())
743 playerID = it->second.m_PlayerID;
744 m_PlayerAssignments.erase(it); // delete the old mapping, since we've got a new one now
745 goto found;
749 // Try to match username next
750 for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it)
752 if (!it->second.m_Enabled && it->second.m_Name == name && usedIDs.find(it->second.m_PlayerID) == usedIDs.end())
754 playerID = it->second.m_PlayerID;
755 m_PlayerAssignments.erase(it); // delete the old mapping, since we've got a new one now
756 goto found;
761 found:
762 PlayerAssignment assignment;
763 assignment.m_Enabled = true;
764 assignment.m_Name = name;
765 assignment.m_PlayerID = playerID;
766 assignment.m_Status = 0;
767 m_PlayerAssignments[guid] = assignment;
769 // Send the new assignments to all currently active players
770 // (which does not include the one that's just joining)
771 SendPlayerAssignments();
774 void CNetServerWorker::RemovePlayer(const CStr& guid)
776 m_PlayerAssignments[guid].m_Enabled = false;
778 SendPlayerAssignments();
781 void CNetServerWorker::ClearAllPlayerReady()
783 for (std::pair<const CStr, PlayerAssignment>& p : m_PlayerAssignments)
784 if (p.second.m_Status != 2)
785 p.second.m_Status = 0;
787 SendPlayerAssignments();
790 void CNetServerWorker::KickPlayer(const CStrW& playerName, const bool ban)
792 // Find the user with that name
793 std::vector<CNetServerSession*>::iterator it = std::find_if(m_Sessions.begin(), m_Sessions.end(),
794 [&](CNetServerSession* session) { return session->GetUserName() == playerName; });
796 // and return if no one or the host has that name
797 if (it == m_Sessions.end() || (*it)->GetGUID() == m_ControllerGUID)
798 return;
800 if (ban)
802 // Remember name
803 if (std::find(m_BannedPlayers.begin(), m_BannedPlayers.end(), playerName) == m_BannedPlayers.end())
804 m_BannedPlayers.push_back(m_LobbyAuth ? CStrW(playerName.substr(0, playerName.find(L" ("))) : playerName);
806 // Remember IP address
807 u32 ipAddress = (*it)->GetIPAddress();
808 if (std::find(m_BannedIPs.begin(), m_BannedIPs.end(), ipAddress) == m_BannedIPs.end())
809 m_BannedIPs.push_back(ipAddress);
812 // Disconnect that user
813 (*it)->Disconnect(ban ? NDR_BANNED : NDR_KICKED);
815 // Send message notifying other clients
816 CKickedMessage kickedMessage;
817 kickedMessage.m_Name = playerName;
818 kickedMessage.m_Ban = ban;
819 Broadcast(&kickedMessage, { NSS_PREGAME, NSS_JOIN_SYNCING, NSS_INGAME });
822 void CNetServerWorker::AssignPlayer(int playerID, const CStr& guid)
824 // Remove anyone who's already assigned to this player
825 for (std::pair<const CStr, PlayerAssignment>& p : m_PlayerAssignments)
827 if (p.second.m_PlayerID == playerID)
828 p.second.m_PlayerID = -1;
831 // Update this host's assignment if it exists
832 if (m_PlayerAssignments.find(guid) != m_PlayerAssignments.end())
833 m_PlayerAssignments[guid].m_PlayerID = playerID;
835 SendPlayerAssignments();
838 void CNetServerWorker::ConstructPlayerAssignmentMessage(CPlayerAssignmentMessage& message)
840 for (const std::pair<const CStr, PlayerAssignment>& p : m_PlayerAssignments)
842 if (!p.second.m_Enabled)
843 continue;
845 CPlayerAssignmentMessage::S_m_Hosts h;
846 h.m_GUID = p.first;
847 h.m_Name = p.second.m_Name;
848 h.m_PlayerID = p.second.m_PlayerID;
849 h.m_Status = p.second.m_Status;
850 message.m_Hosts.push_back(h);
854 void CNetServerWorker::SendPlayerAssignments()
856 CPlayerAssignmentMessage message;
857 ConstructPlayerAssignmentMessage(message);
858 Broadcast(&message, { NSS_PREGAME, NSS_JOIN_SYNCING, NSS_INGAME });
861 const ScriptInterface& CNetServerWorker::GetScriptInterface()
863 return *m_ScriptInterface;
866 void CNetServerWorker::SetTurnLength(u32 msecs)
868 if (m_ServerTurnManager)
869 m_ServerTurnManager->SetTurnLength(msecs);
872 void CNetServerWorker::ProcessLobbyAuth(const CStr& name, const CStr& token)
874 LOGMESSAGE("Net Server: Received lobby auth message from %s with %s", name, token);
875 // Find the user with that guid
876 std::vector<CNetServerSession*>::iterator it = std::find_if(m_Sessions.begin(), m_Sessions.end(),
877 [&](CNetServerSession* session)
878 { return session->GetGUID() == token; });
880 if (it == m_Sessions.end())
881 return;
883 (*it)->SetUserName(name.FromUTF8());
884 // Send an empty message to request the authentication message from the client
885 // after its identity has been confirmed via the lobby
886 CAuthenticateMessage emptyMessage;
887 (*it)->SendMessage(&emptyMessage);
890 bool CNetServerWorker::OnClientHandshake(CNetServerSession* session, CFsmEvent* event)
892 ENSURE(event->GetType() == (uint)NMT_CLIENT_HANDSHAKE);
894 CNetServerWorker& server = session->GetServer();
896 CCliHandshakeMessage* message = (CCliHandshakeMessage*)event->GetParamRef();
897 if (message->m_ProtocolVersion != PS_PROTOCOL_VERSION)
899 session->Disconnect(NDR_INCORRECT_PROTOCOL_VERSION);
900 return false;
903 CStr guid = ps_generate_guid();
904 int count = 0;
905 // Ensure unique GUID
906 while(std::find_if(
907 server.m_Sessions.begin(), server.m_Sessions.end(),
908 [&guid] (const CNetServerSession* session)
909 { return session->GetGUID() == guid; }) != server.m_Sessions.end())
911 if (++count > 100)
913 session->Disconnect(NDR_GUID_FAILED);
914 return true;
916 guid = ps_generate_guid();
919 session->SetGUID(guid);
921 CSrvHandshakeResponseMessage handshakeResponse;
922 handshakeResponse.m_UseProtocolVersion = PS_PROTOCOL_VERSION;
923 handshakeResponse.m_GUID = guid;
924 handshakeResponse.m_Flags = 0;
926 if (server.m_LobbyAuth)
928 handshakeResponse.m_Flags |= PS_NETWORK_FLAG_REQUIRE_LOBBYAUTH;
929 session->SetNextState(NSS_LOBBY_AUTHENTICATE);
932 session->SendMessage(&handshakeResponse);
934 return true;
937 bool CNetServerWorker::OnAuthenticate(CNetServerSession* session, CFsmEvent* event)
939 ENSURE(event->GetType() == (uint)NMT_AUTHENTICATE);
941 CNetServerWorker& server = session->GetServer();
943 // Prohibit joins while the game is loading
944 if (server.m_State == SERVER_STATE_LOADING)
946 LOGMESSAGE("Refused connection while the game is loading");
947 session->Disconnect(NDR_SERVER_LOADING);
948 return true;
951 CAuthenticateMessage* message = (CAuthenticateMessage*)event->GetParamRef();
952 CStrW username = SanitisePlayerName(message->m_Name);
953 CStrW usernameWithoutRating(username.substr(0, username.find(L" (")));
955 // Compare the lowercase names as specified by https://xmpp.org/extensions/xep-0029.html#sect-idm139493404168176
956 // "[...] comparisons will be made in case-normalized canonical form."
957 if (server.m_LobbyAuth && usernameWithoutRating.LowerCase() != session->GetUserName().LowerCase())
959 LOGERROR("Net server: lobby auth: %s tried joining as %s",
960 session->GetUserName().ToUTF8(),
961 usernameWithoutRating.ToUTF8());
962 session->Disconnect(NDR_LOBBY_AUTH_FAILED);
963 return true;
966 // Check the password before anything else.
967 // NB: m_Name must match the client's salt, @see CNetClient::SetGamePassword
968 if (!server.CheckPassword(message->m_Password, message->m_Name.ToUTF8()))
970 // Noisy logerror because players are not supposed to be able to get the IP,
971 // so this might be someone targeting the host for some reason
972 // (or TODO a dedicated server and we do want to log anyways)
973 LOGERROR("Net server: user %s tried joining with the wrong password",
974 session->GetUserName().ToUTF8());
975 session->Disconnect(NDR_SERVER_REFUSED);
976 return true;
979 // Either deduplicate or prohibit join if name is in use
980 bool duplicatePlayernames = false;
981 CFG_GET_VAL("network.duplicateplayernames", duplicatePlayernames);
982 // If lobby authentication is enabled, the clients playername has already been registered.
983 // There also can't be any duplicated names.
984 if (!server.m_LobbyAuth && duplicatePlayernames)
985 username = server.DeduplicatePlayerName(username);
986 else
988 std::vector<CNetServerSession*>::iterator it = std::find_if(
989 server.m_Sessions.begin(), server.m_Sessions.end(),
990 [&username] (const CNetServerSession* session)
991 { return session->GetUserName() == username; });
993 if (it != server.m_Sessions.end() && (*it) != session)
995 session->Disconnect(NDR_PLAYERNAME_IN_USE);
996 return true;
1000 // Disconnect banned usernames
1001 if (std::find(server.m_BannedPlayers.begin(), server.m_BannedPlayers.end(), server.m_LobbyAuth ? usernameWithoutRating : username) != server.m_BannedPlayers.end())
1003 session->Disconnect(NDR_BANNED);
1004 return true;
1007 int maxObservers = 0;
1008 CFG_GET_VAL("network.observerlimit", maxObservers);
1010 bool isRejoining = false;
1011 bool serverFull = false;
1012 if (server.m_State == SERVER_STATE_PREGAME)
1014 // Don't check for maxObservers in the gamesetup, as we don't know yet who will be assigned
1015 serverFull = server.m_Sessions.size() >= MAX_CLIENTS;
1017 else
1019 bool isObserver = true;
1020 int disconnectedPlayers = 0;
1021 int connectedPlayers = 0;
1022 // (TODO: if GUIDs were stable, we should use them instead)
1023 for (const std::pair<const CStr, PlayerAssignment>& p : server.m_PlayerAssignments)
1025 const PlayerAssignment& assignment = p.second;
1027 if (!assignment.m_Enabled && assignment.m_Name == username)
1029 isObserver = assignment.m_PlayerID == -1;
1030 isRejoining = true;
1033 if (assignment.m_PlayerID == -1)
1034 continue;
1036 if (assignment.m_Enabled)
1037 ++connectedPlayers;
1038 else
1039 ++disconnectedPlayers;
1042 // Optionally allow everyone or only buddies to join after the game has started
1043 if (!isRejoining)
1045 CStr observerLateJoin;
1046 CFG_GET_VAL("network.lateobservers", observerLateJoin);
1048 if (observerLateJoin == "everyone")
1050 isRejoining = true;
1052 else if (observerLateJoin == "buddies")
1054 CStr buddies;
1055 CFG_GET_VAL("lobby.buddies", buddies);
1056 std::wstringstream buddiesStream(wstring_from_utf8(buddies));
1057 CStrW buddy;
1058 while (std::getline(buddiesStream, buddy, L','))
1060 if (buddy == usernameWithoutRating)
1062 isRejoining = true;
1063 break;
1069 if (!isRejoining)
1071 LOGMESSAGE("Refused connection after game start from not-previously-known user \"%s\"", utf8_from_wstring(username));
1072 session->Disconnect(NDR_SERVER_ALREADY_IN_GAME);
1073 return true;
1076 // Ensure all players will be able to rejoin
1077 serverFull = isObserver && (
1078 (int) server.m_Sessions.size() - connectedPlayers > maxObservers ||
1079 (int) server.m_Sessions.size() + disconnectedPlayers >= MAX_CLIENTS);
1082 if (serverFull)
1084 session->Disconnect(NDR_SERVER_FULL);
1085 return true;
1088 u32 newHostID = server.m_NextHostID++;
1090 session->SetUserName(username);
1091 session->SetHostID(newHostID);
1093 CAuthenticateResultMessage authenticateResult;
1094 authenticateResult.m_Code = isRejoining ? ARC_OK_REJOINING : ARC_OK;
1095 authenticateResult.m_HostID = newHostID;
1096 authenticateResult.m_Message = L"Logged in";
1097 authenticateResult.m_IsController = 0;
1099 if (message->m_ControllerSecret == server.m_ControllerSecret)
1101 if (server.m_ControllerGUID.empty())
1103 server.m_ControllerGUID = session->GetGUID();
1104 authenticateResult.m_IsController = 1;
1106 // TODO: we could probably handle having several controllers, or swapping?
1109 session->SendMessage(&authenticateResult);
1111 server.OnUserJoin(session);
1113 if (!isRejoining)
1114 return true;
1116 ENSURE(server.m_State != SERVER_STATE_UNCONNECTED && server.m_State != SERVER_STATE_PREGAME);
1118 // Request a copy of the current game state from an existing player, so we can send it on to the new
1119 // player.
1121 // Assume session 0 is most likely the local player, so they're the most efficient client to request a
1122 // copy from.
1123 CNetServerSession* sourceSession = server.m_Sessions.at(0);
1125 sourceSession->GetFileTransferer().StartTask([&server, newHostID](std::string buffer)
1127 // We've received the game state from an existing player - now we need to send it onwards
1128 // to the newly rejoining player.
1130 const auto sessionIt = std::find_if(server.m_Sessions.begin(), server.m_Sessions.end(),
1131 [newHostID](const CNetServerSession* serverSession)
1133 return serverSession->GetHostID() == newHostID;
1136 if (sessionIt == server.m_Sessions.end())
1138 LOGMESSAGE("Net server: rejoining client disconnected before we sent to it");
1139 return;
1142 // Store the received state file, and tell the client to stant downloading it from us.
1143 // TODO: The server will get kind of confused if there's multiple clients downloading in
1144 // parallel; they'll race and get whichever happens to be the latest received by the
1145 // server, which should still work but isn't great.
1146 server.m_JoinSyncFile = std::move(buffer);
1148 // Send the init attributes alongside - these should be correct since the game should be
1149 // started.
1150 CJoinSyncStartMessage message;
1151 message.m_InitAttributes = Script::StringifyJSON(
1152 ScriptRequest{server.GetScriptInterface()}, &server.m_InitAttributes);
1153 (*sessionIt)->SendMessage(&message);
1156 session->SetNextState(NSS_JOIN_SYNCING);
1157 return true;
1160 bool CNetServerWorker::OnSimulationCommand(CNetServerSession* session, CFsmEvent* event)
1162 ENSURE(event->GetType() == (uint)NMT_SIMULATION_COMMAND);
1164 CNetServerWorker& server = session->GetServer();
1166 CSimulationMessage* message = (CSimulationMessage*)event->GetParamRef();
1168 // Ignore messages sent by one player on behalf of another player
1169 // unless cheating is enabled
1170 bool cheatsEnabled = false;
1171 const ScriptInterface& scriptInterface = server.GetScriptInterface();
1172 ScriptRequest rq(scriptInterface);
1173 JS::RootedValue settings(rq.cx);
1174 Script::GetProperty(rq, server.m_InitAttributes, "settings", &settings);
1175 if (Script::HasProperty(rq, settings, "CheatsEnabled"))
1176 Script::GetProperty(rq, settings, "CheatsEnabled", cheatsEnabled);
1178 PlayerAssignmentMap::iterator it = server.m_PlayerAssignments.find(session->GetGUID());
1179 // When cheating is disabled, fail if the player the message claims to
1180 // represent does not exist or does not match the sender's player name
1181 if (!cheatsEnabled && (it == server.m_PlayerAssignments.end() || it->second.m_PlayerID != message->m_Player))
1182 return true;
1184 // Send it back to all clients that have finished
1185 // the loading screen (and the synchronization when rejoining)
1186 server.Broadcast(message, { NSS_INGAME });
1188 // Save all the received commands
1189 if (server.m_SavedCommands.size() < message->m_Turn + 1)
1190 server.m_SavedCommands.resize(message->m_Turn + 1);
1191 server.m_SavedCommands[message->m_Turn].push_back(*message);
1193 // TODO: we shouldn't send the message back to the client that first sent it
1194 return true;
1197 bool CNetServerWorker::OnSyncCheck(CNetServerSession* session, CFsmEvent* event)
1199 ENSURE(event->GetType() == (uint)NMT_SYNC_CHECK);
1201 CNetServerWorker& server = session->GetServer();
1203 CSyncCheckMessage* message = (CSyncCheckMessage*)event->GetParamRef();
1205 server.m_ServerTurnManager->NotifyFinishedClientUpdate(*session, message->m_Turn, message->m_Hash);
1206 return true;
1209 bool CNetServerWorker::OnEndCommandBatch(CNetServerSession* session, CFsmEvent* event)
1211 ENSURE(event->GetType() == (uint)NMT_END_COMMAND_BATCH);
1213 CNetServerWorker& server = session->GetServer();
1215 CEndCommandBatchMessage* message = (CEndCommandBatchMessage*)event->GetParamRef();
1217 // The turn-length field is ignored
1218 server.m_ServerTurnManager->NotifyFinishedClientCommands(*session, message->m_Turn);
1219 return true;
1222 bool CNetServerWorker::OnChat(CNetServerSession* session, CFsmEvent* event)
1224 ENSURE(event->GetType() == (uint)NMT_CHAT);
1226 CNetServerWorker& server = session->GetServer();
1228 CChatMessage* message = (CChatMessage*)event->GetParamRef();
1230 message->m_GUID = session->GetGUID();
1232 server.Broadcast(message, { NSS_PREGAME, NSS_INGAME });
1234 return true;
1237 bool CNetServerWorker::OnReady(CNetServerSession* session, CFsmEvent* event)
1239 ENSURE(event->GetType() == (uint)NMT_READY);
1241 CNetServerWorker& server = session->GetServer();
1243 // Occurs if a client presses not-ready
1244 // in the very last moment before the hosts starts the game
1245 if (server.m_State == SERVER_STATE_LOADING)
1246 return true;
1248 CReadyMessage* message = (CReadyMessage*)event->GetParamRef();
1249 message->m_GUID = session->GetGUID();
1250 server.Broadcast(message, { NSS_PREGAME });
1252 server.m_PlayerAssignments[message->m_GUID].m_Status = message->m_Status;
1254 return true;
1257 bool CNetServerWorker::OnClearAllReady(CNetServerSession* session, CFsmEvent* event)
1259 ENSURE(event->GetType() == (uint)NMT_CLEAR_ALL_READY);
1261 CNetServerWorker& server = session->GetServer();
1263 if (session->GetGUID() == server.m_ControllerGUID)
1264 server.ClearAllPlayerReady();
1266 return true;
1269 bool CNetServerWorker::OnGameSetup(CNetServerSession* session, CFsmEvent* event)
1271 ENSURE(event->GetType() == (uint)NMT_GAME_SETUP);
1273 CNetServerWorker& server = session->GetServer();
1275 // Changing the settings after gamestart is not implemented and would cause an Out-of-sync error.
1276 // This happened when doubleclicking on the startgame button.
1277 if (server.m_State != SERVER_STATE_PREGAME)
1278 return true;
1280 // Only the controller is allowed to send game setup updates.
1281 // TODO: it would be good to allow other players to request changes to some settings,
1282 // e.g. their civilisation.
1283 // Possibly this should use another message, to enforce a single source of truth.
1284 if (session->GetGUID() == server.m_ControllerGUID)
1286 CGameSetupMessage* message = (CGameSetupMessage*)event->GetParamRef();
1287 server.Broadcast(message, { NSS_PREGAME });
1289 return true;
1292 bool CNetServerWorker::OnAssignPlayer(CNetServerSession* session, CFsmEvent* event)
1294 ENSURE(event->GetType() == (uint)NMT_ASSIGN_PLAYER);
1295 CNetServerWorker& server = session->GetServer();
1297 if (session->GetGUID() == server.m_ControllerGUID)
1299 CAssignPlayerMessage* message = (CAssignPlayerMessage*)event->GetParamRef();
1300 server.AssignPlayer(message->m_PlayerID, message->m_GUID);
1302 return true;
1305 bool CNetServerWorker::OnGameStart(CNetServerSession* session, CFsmEvent* event)
1307 ENSURE(event->GetType() == (uint)NMT_GAME_START);
1308 CNetServerWorker& server = session->GetServer();
1310 if (session->GetGUID() != server.m_ControllerGUID)
1311 return true;
1313 CGameStartMessage* message = (CGameStartMessage*)event->GetParamRef();
1314 server.StartGame(message->m_InitAttributes);
1315 return true;
1318 bool CNetServerWorker::OnLoadedGame(CNetServerSession* loadedSession, CFsmEvent* event)
1320 ENSURE(event->GetType() == (uint)NMT_LOADED_GAME);
1322 CNetServerWorker& server = loadedSession->GetServer();
1324 // We're in the loading state, so wait until every client has loaded
1325 // before starting the game
1326 ENSURE(server.m_State == SERVER_STATE_LOADING);
1327 if (server.CheckGameLoadStatus(loadedSession))
1328 return true;
1330 CClientsLoadingMessage message;
1331 // We always send all GUIDs of clients in the loading state
1332 // so that we don't have to bother about switching GUI pages
1333 for (CNetServerSession* session : server.m_Sessions)
1334 if (session->GetCurrState() != NSS_INGAME && loadedSession->GetGUID() != session->GetGUID())
1336 CClientsLoadingMessage::S_m_Clients client;
1337 client.m_GUID = session->GetGUID();
1338 message.m_Clients.push_back(client);
1341 // Send to the client who has loaded the game but did not reach the NSS_INGAME state yet
1342 loadedSession->SendMessage(&message);
1343 server.Broadcast(&message, { NSS_INGAME });
1345 return true;
1348 bool CNetServerWorker::OnJoinSyncingLoadedGame(CNetServerSession* session, CFsmEvent* event)
1350 // A client rejoining an in-progress game has now finished loading the
1351 // map and deserialized the initial state.
1352 // The simulation may have progressed since then, so send any subsequent
1353 // commands to them and set them as an active player so they can participate
1354 // in all future turns.
1356 // (TODO: if it takes a long time for them to receive and execute all these
1357 // commands, the other players will get frozen for that time and may be unhappy;
1358 // we could try repeating this process a few times until the client converges
1359 // on the up-to-date state, before setting them as active.)
1361 ENSURE(event->GetType() == (uint)NMT_LOADED_GAME);
1363 CNetServerWorker& server = session->GetServer();
1365 CLoadedGameMessage* message = (CLoadedGameMessage*)event->GetParamRef();
1367 u32 turn = message->m_CurrentTurn;
1368 u32 readyTurn = server.m_ServerTurnManager->GetReadyTurn();
1370 // Send them all commands received since their saved state,
1371 // and turn-ended messages for any turns that have already been processed
1372 for (size_t i = turn + 1; i < std::max(readyTurn+1, (u32)server.m_SavedCommands.size()); ++i)
1374 if (i < server.m_SavedCommands.size())
1375 for (size_t j = 0; j < server.m_SavedCommands[i].size(); ++j)
1376 session->SendMessage(&server.m_SavedCommands[i][j]);
1378 if (i <= readyTurn)
1380 CEndCommandBatchMessage endMessage;
1381 endMessage.m_Turn = i;
1382 endMessage.m_TurnLength = server.m_ServerTurnManager->GetSavedTurnLength(i);
1383 session->SendMessage(&endMessage);
1387 // Tell the turn manager to expect commands from this new client
1388 // Special case: the controller shouldn't be treated as an observer in any case.
1389 bool isObserver = server.m_PlayerAssignments[session->GetGUID()].m_PlayerID == -1 && server.m_ControllerGUID != session->GetGUID();
1390 server.m_ServerTurnManager->InitialiseClient(session->GetHostID(), readyTurn, isObserver);
1392 // Tell the client that everything has finished loading and it should start now
1393 CLoadedGameMessage loaded;
1394 loaded.m_CurrentTurn = readyTurn;
1395 session->SendMessage(&loaded);
1397 return true;
1400 bool CNetServerWorker::OnRejoined(CNetServerSession* session, CFsmEvent* event)
1402 // A client has finished rejoining and the loading screen disappeared.
1403 ENSURE(event->GetType() == (uint)NMT_REJOINED);
1405 CNetServerWorker& server = session->GetServer();
1407 // Inform everyone of the client having rejoined
1408 CRejoinedMessage* message = (CRejoinedMessage*)event->GetParamRef();
1409 message->m_GUID = session->GetGUID();
1410 server.Broadcast(message, { NSS_INGAME });
1412 // Send all pausing players to the rejoined client.
1413 for (const CStr& guid : server.m_PausingPlayers)
1415 CClientPausedMessage pausedMessage;
1416 pausedMessage.m_GUID = guid;
1417 pausedMessage.m_Pause = true;
1418 session->SendMessage(&pausedMessage);
1421 return true;
1424 bool CNetServerWorker::OnKickPlayer(CNetServerSession* session, CFsmEvent* event)
1426 ENSURE(event->GetType() == (uint)NMT_KICKED);
1428 CNetServerWorker& server = session->GetServer();
1430 if (session->GetGUID() == server.m_ControllerGUID)
1432 CKickedMessage* message = (CKickedMessage*)event->GetParamRef();
1433 server.KickPlayer(message->m_Name, message->m_Ban);
1435 return true;
1438 bool CNetServerWorker::OnDisconnect(CNetServerSession* session, CFsmEvent* event)
1440 ENSURE(event->GetType() == (uint)NMT_CONNECTION_LOST);
1442 CNetServerWorker& server = session->GetServer();
1444 server.OnUserLeave(session);
1446 return true;
1449 bool CNetServerWorker::OnClientPaused(CNetServerSession* session, CFsmEvent* event)
1451 ENSURE(event->GetType() == (uint)NMT_CLIENT_PAUSED);
1453 CNetServerWorker& server = session->GetServer();
1455 CClientPausedMessage* message = (CClientPausedMessage*)event->GetParamRef();
1457 message->m_GUID = session->GetGUID();
1459 // Update the list of pausing players.
1460 std::vector<CStr>::iterator player = std::find(server.m_PausingPlayers.begin(), server.m_PausingPlayers.end(), session->GetGUID());
1462 if (message->m_Pause)
1464 if (player != server.m_PausingPlayers.end())
1465 return true;
1467 server.m_PausingPlayers.push_back(session->GetGUID());
1469 else
1471 if (player == server.m_PausingPlayers.end())
1472 return true;
1474 server.m_PausingPlayers.erase(player);
1477 // Send messages to clients that are in game, and are not the client who paused.
1478 for (CNetServerSession* netSession : server.m_Sessions)
1479 if (netSession->GetCurrState() == NSS_INGAME && message->m_GUID != netSession->GetGUID())
1480 netSession->SendMessage(message);
1482 return true;
1485 bool CNetServerWorker::CheckGameLoadStatus(CNetServerSession* changedSession)
1487 for (const CNetServerSession* session : m_Sessions)
1488 if (session != changedSession && session->GetCurrState() != NSS_INGAME)
1489 return false;
1491 // Inform clients that everyone has loaded the map and that the game can start
1492 CLoadedGameMessage loaded;
1493 loaded.m_CurrentTurn = 0;
1495 // Notice the changedSession is still in the NSS_PREGAME state
1496 Broadcast(&loaded, { NSS_PREGAME, NSS_INGAME });
1498 m_State = SERVER_STATE_INGAME;
1499 return true;
1502 void CNetServerWorker::StartGame(const CStr& initAttribs)
1504 for (std::pair<const CStr, PlayerAssignment>& player : m_PlayerAssignments)
1505 if (player.second.m_Enabled && player.second.m_PlayerID != -1 && player.second.m_Status == 0)
1507 LOGERROR("Tried to start the game without player \"%s\" being ready!", utf8_from_wstring(player.second.m_Name).c_str());
1508 return;
1511 m_ServerTurnManager = new CNetServerTurnManager(*this);
1513 for (CNetServerSession* session : m_Sessions)
1515 // Special case: the controller shouldn't be treated as an observer in any case.
1516 bool isObserver = m_PlayerAssignments[session->GetGUID()].m_PlayerID == -1 && m_ControllerGUID != session->GetGUID();
1517 m_ServerTurnManager->InitialiseClient(session->GetHostID(), 0, isObserver);
1520 m_State = SERVER_STATE_LOADING;
1522 // Remove players and observers that are not present when the game starts
1523 for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end();)
1524 if (it->second.m_Enabled)
1525 ++it;
1526 else
1527 it = m_PlayerAssignments.erase(it);
1529 SendPlayerAssignments();
1531 // Update init attributes. They should no longer change.
1532 Script::ParseJSON(ScriptRequest(m_ScriptInterface), initAttribs, &m_InitAttributes);
1534 CGameStartMessage gameStart;
1535 gameStart.m_InitAttributes = initAttribs;
1536 Broadcast(&gameStart, { NSS_PREGAME });
1539 CStrW CNetServerWorker::SanitisePlayerName(const CStrW& original)
1541 const size_t MAX_LENGTH = 32;
1543 CStrW name = original;
1544 name.Replace(L"[", L"{"); // remove GUI tags
1545 name.Replace(L"]", L"}"); // remove for symmetry
1547 // Restrict the length
1548 if (name.length() > MAX_LENGTH)
1549 name = name.Left(MAX_LENGTH);
1551 // Don't allow surrounding whitespace
1552 name.Trim(PS_TRIM_BOTH);
1554 // Don't allow empty name
1555 if (name.empty())
1556 name = L"Anonymous";
1558 return name;
1561 CStrW CNetServerWorker::DeduplicatePlayerName(const CStrW& original)
1563 CStrW name = original;
1565 // Try names "Foo", "Foo (2)", "Foo (3)", etc
1566 size_t id = 2;
1567 while (true)
1569 bool unique = true;
1570 for (const CNetServerSession* session : m_Sessions)
1572 if (session->GetUserName() == name)
1574 unique = false;
1575 break;
1579 if (unique)
1580 return name;
1582 name = original + L" (" + CStrW::FromUInt(id++) + L")";
1586 void CNetServerWorker::SendHolePunchingMessage(const CStr& ipStr, u16 port)
1588 if (m_Host)
1589 StunClient::SendHolePunchingMessages(*m_Host, ipStr, port);
1595 CNetServer::CNetServer(bool useLobbyAuth) :
1596 m_Worker(new CNetServerWorker(useLobbyAuth)),
1597 m_LobbyAuth(useLobbyAuth), m_UseSTUN(false), m_PublicIp(""), m_PublicPort(20595), m_Password()
1601 CNetServer::~CNetServer()
1603 delete m_Worker;
1606 bool CNetServer::GetUseSTUN() const
1608 return m_UseSTUN;
1611 bool CNetServer::UseLobbyAuth() const
1613 return m_LobbyAuth;
1616 bool CNetServer::SetupConnection(const u16 port)
1618 return m_Worker->SetupConnection(port);
1621 CStr CNetServer::GetPublicIp() const
1623 return m_PublicIp;
1626 u16 CNetServer::GetPublicPort() const
1628 return m_PublicPort;
1631 u16 CNetServer::GetLocalPort() const
1633 std::lock_guard<std::mutex> lock(m_Worker->m_WorkerMutex);
1634 if (!m_Worker->m_Host)
1635 return 0;
1636 return m_Worker->m_Host->address.port;
1639 void CNetServer::SetConnectionData(const CStr& ip, const u16 port)
1641 m_PublicIp = ip;
1642 m_PublicPort = port;
1643 m_UseSTUN = false;
1646 bool CNetServer::SetConnectionDataViaSTUN()
1648 m_UseSTUN = true;
1649 std::lock_guard<std::mutex> lock(m_Worker->m_WorkerMutex);
1650 if (!m_Worker->m_Host)
1651 return false;
1652 return StunClient::FindPublicIP(*m_Worker->m_Host, m_PublicIp, m_PublicPort);
1655 bool CNetServer::CheckPasswordAndIncrement(const std::string& username, const std::string& password, const std::string& salt)
1657 std::unordered_map<std::string, int>::iterator it = m_FailedAttempts.find(username);
1658 if (m_Worker->CheckPassword(password, salt))
1660 if (it != m_FailedAttempts.end())
1661 it->second = 0;
1662 return true;
1664 if (it == m_FailedAttempts.end())
1665 m_FailedAttempts.emplace(username, 1);
1666 else
1667 it->second++;
1668 return false;
1671 bool CNetServer::IsBanned(const std::string& username) const
1673 std::unordered_map<std::string, int>::const_iterator it = m_FailedAttempts.find(username);
1674 return it != m_FailedAttempts.end() && it->second >= FAILED_PASSWORD_TRIES_BEFORE_BAN;
1677 void CNetServer::SetPassword(const CStr& password)
1679 m_Password = password;
1680 std::lock_guard<std::mutex> lock(m_Worker->m_WorkerMutex);
1681 m_Worker->SetPassword(password);
1684 void CNetServer::SetControllerSecret(const std::string& secret)
1686 std::lock_guard<std::mutex> lock(m_Worker->m_WorkerMutex);
1687 m_Worker->SetControllerSecret(secret);
1690 void CNetServer::StartGame()
1692 std::lock_guard<std::mutex> lock(m_Worker->m_WorkerMutex);
1693 m_Worker->m_StartGameQueue.push_back(true);
1696 void CNetServer::UpdateInitAttributes(JS::MutableHandleValue attrs, const ScriptRequest& rq)
1698 // Pass the attributes as JSON, since that's the easiest safe
1699 // cross-thread way of passing script data
1700 std::string attrsJSON = Script::StringifyJSON(rq, attrs, false);
1702 std::lock_guard<std::mutex> lock(m_Worker->m_WorkerMutex);
1703 m_Worker->m_InitAttributesQueue.push_back(attrsJSON);
1706 void CNetServer::OnLobbyAuth(const CStr& name, const CStr& token)
1708 std::lock_guard<std::mutex> lock(m_Worker->m_WorkerMutex);
1709 m_Worker->m_LobbyAuthQueue.push_back(std::make_pair(name, token));
1712 void CNetServer::SetTurnLength(u32 msecs)
1714 std::lock_guard<std::mutex> lock(m_Worker->m_WorkerMutex);
1715 m_Worker->m_TurnLengthQueue.push_back(msecs);
1718 void CNetServer::SendHolePunchingMessage(const CStr& ip, u16 port)
1720 m_Worker->SendHolePunchingMessage(ip, port);