Always require lobby authentication for lobby matches, refs #3549 / rP21520 / D897.
[0ad.git] / source / network / NetServer.cpp
blob08d2fd8b072476fb822097ba2d0a190ab01dbb8c
1 /* Copyright (C) 2018 Wildfire Games.
2 * This file is part of 0 A.D.
4 * 0 A.D. is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 2 of the License, or
7 * (at your option) any later version.
9 * 0 A.D. is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
18 #include "precompiled.h"
20 #include "NetServer.h"
22 #include "NetClient.h"
23 #include "NetMessage.h"
24 #include "NetSession.h"
25 #include "NetServerTurnManager.h"
26 #include "NetStats.h"
28 #include "lib/external_libraries/enet.h"
29 #include "lib/types.h"
30 #include "network/StunClient.h"
31 #include "ps/CLogger.h"
32 #include "ps/ConfigDB.h"
33 #include "ps/GUID.h"
34 #include "ps/Profile.h"
35 #include "ps/ThreadUtil.h"
36 #include "scriptinterface/ScriptInterface.h"
37 #include "scriptinterface/ScriptRuntime.h"
38 #include "simulation2/Simulation2.h"
39 #include "simulation2/system/TurnManager.h"
41 #if CONFIG2_MINIUPNPC
42 #include <miniupnpc/miniwget.h>
43 #include <miniupnpc/miniupnpc.h>
44 #include <miniupnpc/upnpcommands.h>
45 #include <miniupnpc/upnperrors.h>
46 #endif
48 #include <string>
50 /**
51 * Number of peers to allocate for the enet host.
52 * Limited by ENET_PROTOCOL_MAXIMUM_PEER_ID (4096).
54 * At most 8 players, 32 observers and 1 temporary connection to send the "server full" disconnect-reason.
56 #define MAX_CLIENTS 41
58 #define DEFAULT_SERVER_NAME L"Unnamed Server"
60 static const int CHANNEL_COUNT = 1;
62 /**
63 * enet_host_service timeout (msecs).
64 * Smaller numbers may hurt performance; larger numbers will
65 * hurt latency responding to messages from game thread.
67 static const int HOST_SERVICE_TIMEOUT = 50;
69 CNetServer* g_NetServer = NULL;
71 static CStr DebugName(CNetServerSession* session)
73 if (session == NULL)
74 return "[unknown host]";
75 if (session->GetGUID().empty())
76 return "[unauthed host]";
77 return "[" + session->GetGUID().substr(0, 8) + "...]";
80 /**
81 * Async task for receiving the initial game state to be forwarded to another
82 * client that is rejoining an in-progress network game.
84 class CNetFileReceiveTask_ServerRejoin : public CNetFileReceiveTask
86 NONCOPYABLE(CNetFileReceiveTask_ServerRejoin);
87 public:
88 CNetFileReceiveTask_ServerRejoin(CNetServerWorker& server, u32 hostID)
89 : m_Server(server), m_RejoinerHostID(hostID)
93 virtual void OnComplete()
95 // We've received the game state from an existing player - now
96 // we need to send it onwards to the newly rejoining player
98 // Find the session corresponding to the rejoining host (if any)
99 CNetServerSession* session = NULL;
100 for (CNetServerSession* serverSession : m_Server.m_Sessions)
102 if (serverSession->GetHostID() == m_RejoinerHostID)
104 session = serverSession;
105 break;
109 if (!session)
111 LOGMESSAGE("Net server: rejoining client disconnected before we sent to it");
112 return;
115 // Store the received state file, and tell the client to start downloading it from us
116 // TODO: this will get kind of confused if there's multiple clients downloading in parallel;
117 // they'll race and get whichever happens to be the latest received by the server,
118 // which should still work but isn't great
119 m_Server.m_JoinSyncFile = m_Buffer;
120 CJoinSyncStartMessage message;
121 session->SendMessage(&message);
124 private:
125 CNetServerWorker& m_Server;
126 u32 m_RejoinerHostID;
130 * XXX: We use some non-threadsafe functions from the worker thread.
131 * See http://trac.wildfiregames.com/ticket/654
134 CNetServerWorker::CNetServerWorker(bool useLobbyAuth, int autostartPlayers) :
135 m_AutostartPlayers(autostartPlayers),
136 m_LobbyAuth(useLobbyAuth),
137 m_Shutdown(false),
138 m_ScriptInterface(NULL),
139 m_NextHostID(1), m_Host(NULL), m_HostGUID(), m_Stats(NULL),
140 m_LastConnectionCheck(0)
142 m_State = SERVER_STATE_UNCONNECTED;
144 m_ServerTurnManager = NULL;
146 m_ServerName = DEFAULT_SERVER_NAME;
149 CNetServerWorker::~CNetServerWorker()
151 if (m_State != SERVER_STATE_UNCONNECTED)
153 // Tell the thread to shut down
155 CScopeLock lock(m_WorkerMutex);
156 m_Shutdown = true;
159 // Wait for it to shut down cleanly
160 pthread_join(m_WorkerThread, NULL);
163 // Clean up resources
165 delete m_Stats;
167 for (CNetServerSession* session : m_Sessions)
169 session->DisconnectNow(NDR_SERVER_SHUTDOWN);
170 delete session;
173 if (m_Host)
174 enet_host_destroy(m_Host);
176 delete m_ServerTurnManager;
179 bool CNetServerWorker::SetupConnection(const u16 port)
181 ENSURE(m_State == SERVER_STATE_UNCONNECTED);
182 ENSURE(!m_Host);
184 // Bind to default host
185 ENetAddress addr;
186 addr.host = ENET_HOST_ANY;
187 addr.port = port;
189 // Create ENet server
190 m_Host = enet_host_create(&addr, MAX_CLIENTS, CHANNEL_COUNT, 0, 0);
191 if (!m_Host)
193 LOGERROR("Net server: enet_host_create failed");
194 return false;
197 m_Stats = new CNetStatsTable();
198 if (CProfileViewer::IsInitialised())
199 g_ProfileViewer.AddRootTable(m_Stats);
201 m_State = SERVER_STATE_PREGAME;
203 // Launch the worker thread
204 int ret = pthread_create(&m_WorkerThread, NULL, &RunThread, this);
205 ENSURE(ret == 0);
207 #if CONFIG2_MINIUPNPC
208 // Launch the UPnP thread
209 ret = pthread_create(&m_UPnPThread, NULL, &SetupUPnP, NULL);
210 ENSURE(ret == 0);
211 #endif
213 return true;
216 #if CONFIG2_MINIUPNPC
217 void* CNetServerWorker::SetupUPnP(void*)
219 // Values we want to set.
220 char psPort[6];
221 sprintf_s(psPort, ARRAY_SIZE(psPort), "%d", PS_DEFAULT_PORT);
222 const char* leaseDuration = "0"; // Indefinite/permanent lease duration.
223 const char* description = "0AD Multiplayer";
224 const char* protocall = "UDP";
225 char internalIPAddress[64];
226 char externalIPAddress[40];
227 // Variables to hold the values that actually get set.
228 char intClient[40];
229 char intPort[6];
230 char duration[16];
231 // Intermediate variables.
232 struct UPNPUrls urls;
233 struct IGDdatas data;
234 struct UPNPDev* devlist = NULL;
236 // Cached root descriptor URL.
237 std::string rootDescURL;
238 CFG_GET_VAL("network.upnprootdescurl", rootDescURL);
239 if (!rootDescURL.empty())
240 LOGMESSAGE("Net server: attempting to use cached root descriptor URL: %s", rootDescURL.c_str());
242 int ret = 0;
243 bool allocatedUrls = false;
245 // Try a cached URL first
246 if (!rootDescURL.empty() && UPNP_GetIGDFromUrl(rootDescURL.c_str(), &urls, &data, internalIPAddress, sizeof(internalIPAddress)))
248 LOGMESSAGE("Net server: using cached IGD = %s", urls.controlURL);
249 ret = 1;
251 // No cached URL, or it did not respond. Try getting a valid UPnP device for 10 seconds.
252 #if defined(MINIUPNPC_API_VERSION) && MINIUPNPC_API_VERSION >= 14
253 else if ((devlist = upnpDiscover(10000, 0, 0, 0, 0, 2, 0)) != NULL)
254 #else
255 else if ((devlist = upnpDiscover(10000, 0, 0, 0, 0, 0)) != NULL)
256 #endif
258 ret = UPNP_GetValidIGD(devlist, &urls, &data, internalIPAddress, sizeof(internalIPAddress));
259 allocatedUrls = ret != 0; // urls is allocated on non-zero return values
261 else
263 LOGMESSAGE("Net server: upnpDiscover failed and no working cached URL.");
264 return NULL;
267 switch (ret)
269 case 0:
270 LOGMESSAGE("Net server: No IGD found");
271 break;
272 case 1:
273 LOGMESSAGE("Net server: found valid IGD = %s", urls.controlURL);
274 break;
275 case 2:
276 LOGMESSAGE("Net server: found a valid, not connected IGD = %s, will try to continue anyway", urls.controlURL);
277 break;
278 case 3:
279 LOGMESSAGE("Net server: found a UPnP device unrecognized as IGD = %s, will try to continue anyway", urls.controlURL);
280 break;
281 default:
282 debug_warn(L"Unrecognized return value from UPNP_GetValidIGD");
285 // Try getting our external/internet facing IP. TODO: Display this on the game-setup page for conviniance.
286 ret = UPNP_GetExternalIPAddress(urls.controlURL, data.first.servicetype, externalIPAddress);
287 if (ret != UPNPCOMMAND_SUCCESS)
289 LOGMESSAGE("Net server: GetExternalIPAddress failed with code %d (%s)", ret, strupnperror(ret));
290 return NULL;
292 LOGMESSAGE("Net server: ExternalIPAddress = %s", externalIPAddress);
294 // Try to setup port forwarding.
295 ret = UPNP_AddPortMapping(urls.controlURL, data.first.servicetype, psPort, psPort,
296 internalIPAddress, description, protocall, 0, leaseDuration);
297 if (ret != UPNPCOMMAND_SUCCESS)
299 LOGMESSAGE("Net server: AddPortMapping(%s, %s, %s) failed with code %d (%s)",
300 psPort, psPort, internalIPAddress, ret, strupnperror(ret));
301 return NULL;
304 // Check that the port was actually forwarded.
305 ret = UPNP_GetSpecificPortMappingEntry(urls.controlURL,
306 data.first.servicetype,
307 psPort, protocall,
308 #if defined(MINIUPNPC_API_VERSION) && MINIUPNPC_API_VERSION >= 10
309 NULL/*remoteHost*/,
310 #endif
311 intClient, intPort, NULL/*desc*/,
312 NULL/*enabled*/, duration);
314 if (ret != UPNPCOMMAND_SUCCESS)
316 LOGMESSAGE("Net server: GetSpecificPortMappingEntry() failed with code %d (%s)", ret, strupnperror(ret));
317 return NULL;
320 LOGMESSAGE("Net server: External %s:%s %s is redirected to internal %s:%s (duration=%s)",
321 externalIPAddress, psPort, protocall, intClient, intPort, duration);
323 // Cache root descriptor URL to try to avoid discovery next time.
324 g_ConfigDB.SetValueString(CFG_USER, "network.upnprootdescurl", urls.controlURL);
325 g_ConfigDB.WriteValueToFile(CFG_USER, "network.upnprootdescurl", urls.controlURL);
326 LOGMESSAGE("Net server: cached UPnP root descriptor URL as %s", urls.controlURL);
328 // Make sure everything is properly freed.
329 if (allocatedUrls)
330 FreeUPNPUrls(&urls);
332 freeUPNPDevlist(devlist);
334 return NULL;
336 #endif // CONFIG2_MINIUPNPC
338 bool CNetServerWorker::SendMessage(ENetPeer* peer, const CNetMessage* message)
340 ENSURE(m_Host);
342 CNetServerSession* session = static_cast<CNetServerSession*>(peer->data);
344 return CNetHost::SendMessage(message, peer, DebugName(session).c_str());
347 bool CNetServerWorker::Broadcast(const CNetMessage* message, const std::vector<NetServerSessionState>& targetStates)
349 ENSURE(m_Host);
351 bool ok = true;
353 // TODO: this does lots of repeated message serialisation if we have lots
354 // of remote peers; could do it more efficiently if that's a real problem
356 for (CNetServerSession* session : m_Sessions)
357 if (std::find(targetStates.begin(), targetStates.end(), session->GetCurrState()) != targetStates.end() &&
358 !session->SendMessage(message))
359 ok = false;
361 return ok;
364 void* CNetServerWorker::RunThread(void* data)
366 debug_SetThreadName("NetServer");
368 static_cast<CNetServerWorker*>(data)->Run();
370 return NULL;
373 void CNetServerWorker::Run()
375 // The script runtime uses the profiler and therefore the thread must be registered before the runtime is created
376 g_Profiler2.RegisterCurrentThread("Net server");
378 // To avoid the need for JS_SetContextThread, we create and use and destroy
379 // the script interface entirely within this network thread
380 m_ScriptInterface = new ScriptInterface("Engine", "Net server", ScriptInterface::CreateRuntime(g_ScriptRuntime));
381 m_GameAttributes.init(m_ScriptInterface->GetJSRuntime(), JS::UndefinedValue());
383 while (true)
385 if (!RunStep())
386 break;
388 // Implement autostart mode
389 if (m_State == SERVER_STATE_PREGAME && (int)m_PlayerAssignments.size() == m_AutostartPlayers)
390 StartGame();
392 // Update profiler stats
393 m_Stats->LatchHostState(m_Host);
396 // Clear roots before deleting their context
397 m_SavedCommands.clear();
399 SAFE_DELETE(m_ScriptInterface);
402 bool CNetServerWorker::RunStep()
404 // Check for messages from the game thread.
405 // (Do as little work as possible while the mutex is held open,
406 // to avoid performance problems and deadlocks.)
408 m_ScriptInterface->GetRuntime()->MaybeIncrementalGC(0.5f);
410 JSContext* cx = m_ScriptInterface->GetContext();
411 JSAutoRequest rq(cx);
413 std::vector<bool> newStartGame;
414 std::vector<std::string> newGameAttributes;
415 std::vector<std::pair<CStr, CStr>> newLobbyAuths;
416 std::vector<u32> newTurnLength;
419 CScopeLock lock(m_WorkerMutex);
421 if (m_Shutdown)
422 return false;
424 newStartGame.swap(m_StartGameQueue);
425 newGameAttributes.swap(m_GameAttributesQueue);
426 newLobbyAuths.swap(m_LobbyAuthQueue);
427 newTurnLength.swap(m_TurnLengthQueue);
430 if (!newGameAttributes.empty())
432 JS::RootedValue gameAttributesVal(cx);
433 GetScriptInterface().ParseJSON(newGameAttributes.back(), &gameAttributesVal);
434 UpdateGameAttributes(&gameAttributesVal);
437 if (!newTurnLength.empty())
438 SetTurnLength(newTurnLength.back());
440 // Do StartGame last, so we have the most up-to-date game attributes when we start
441 if (!newStartGame.empty())
442 StartGame();
444 while (!newLobbyAuths.empty())
446 const std::pair<CStr, CStr>& auth = newLobbyAuths.back();
447 ProcessLobbyAuth(auth.first, auth.second);
448 newLobbyAuths.pop_back();
451 // Perform file transfers
452 for (CNetServerSession* session : m_Sessions)
453 session->GetFileTransferer().Poll();
455 CheckClientConnections();
457 // Process network events:
459 ENetEvent event;
460 int status = enet_host_service(m_Host, &event, HOST_SERVICE_TIMEOUT);
461 if (status < 0)
463 LOGERROR("CNetServerWorker: enet_host_service failed (%d)", status);
464 // TODO: notify game that the server has shut down
465 return false;
468 if (status == 0)
470 // Reached timeout with no events - try again
471 return true;
474 // Process the event:
476 switch (event.type)
478 case ENET_EVENT_TYPE_CONNECT:
480 // Report the client address
481 char hostname[256] = "(error)";
482 enet_address_get_host_ip(&event.peer->address, hostname, ARRAY_SIZE(hostname));
483 LOGMESSAGE("Net server: Received connection from %s:%u", hostname, (unsigned int)event.peer->address.port);
485 // Set up a session object for this peer
487 CNetServerSession* session = new CNetServerSession(*this, event.peer);
489 m_Sessions.push_back(session);
491 SetupSession(session);
493 ENSURE(event.peer->data == NULL);
494 event.peer->data = session;
496 HandleConnect(session);
498 break;
501 case ENET_EVENT_TYPE_DISCONNECT:
503 // If there is an active session with this peer, then reset and delete it
505 CNetServerSession* session = static_cast<CNetServerSession*>(event.peer->data);
506 if (session)
508 LOGMESSAGE("Net server: Disconnected %s", DebugName(session).c_str());
510 // Remove the session first, so we won't send player-update messages to it
511 // when updating the FSM
512 m_Sessions.erase(remove(m_Sessions.begin(), m_Sessions.end(), session), m_Sessions.end());
514 session->Update((uint)NMT_CONNECTION_LOST, NULL);
516 delete session;
517 event.peer->data = NULL;
520 if (m_State == SERVER_STATE_LOADING)
521 CheckGameLoadStatus(NULL);
523 break;
526 case ENET_EVENT_TYPE_RECEIVE:
528 // If there is an active session with this peer, then process the message
530 CNetServerSession* session = static_cast<CNetServerSession*>(event.peer->data);
531 if (session)
533 // Create message from raw data
534 CNetMessage* msg = CNetMessageFactory::CreateMessage(event.packet->data, event.packet->dataLength, GetScriptInterface());
535 if (msg)
537 LOGMESSAGE("Net server: Received message %s of size %lu from %s", msg->ToString().c_str(), (unsigned long)msg->GetSerializedLength(), DebugName(session).c_str());
539 HandleMessageReceive(msg, session);
541 delete msg;
545 // Done using the packet
546 enet_packet_destroy(event.packet);
548 break;
551 case ENET_EVENT_TYPE_NONE:
552 break;
555 return true;
558 void CNetServerWorker::CheckClientConnections()
560 // Send messages at most once per second
561 std::time_t now = std::time(nullptr);
562 if (now <= m_LastConnectionCheck)
563 return;
565 m_LastConnectionCheck = now;
567 for (size_t i = 0; i < m_Sessions.size(); ++i)
569 u32 lastReceived = m_Sessions[i]->GetLastReceivedTime();
570 u32 meanRTT = m_Sessions[i]->GetMeanRTT();
572 CNetMessage* message = nullptr;
574 // Report if we didn't hear from the client since few seconds
575 if (lastReceived > NETWORK_WARNING_TIMEOUT)
577 CClientTimeoutMessage* msg = new CClientTimeoutMessage();
578 msg->m_GUID = m_Sessions[i]->GetGUID();
579 msg->m_LastReceivedTime = lastReceived;
580 message = msg;
582 // Report if the client has bad ping
583 else if (meanRTT > DEFAULT_TURN_LENGTH_MP)
585 CClientPerformanceMessage* msg = new CClientPerformanceMessage();
586 CClientPerformanceMessage::S_m_Clients client;
587 client.m_GUID = m_Sessions[i]->GetGUID();
588 client.m_MeanRTT = meanRTT;
589 msg->m_Clients.push_back(client);
590 message = msg;
593 // Send to all clients except the affected one
594 // (since that will show the locally triggered warning instead).
595 // Also send it to clients that finished the loading screen while
596 // the game is still waiting for other clients to finish the loading screen.
597 if (message)
598 for (size_t j = 0; j < m_Sessions.size(); ++j)
600 if (i != j && (
601 (m_Sessions[j]->GetCurrState() == NSS_PREGAME && m_State == SERVER_STATE_PREGAME) ||
602 m_Sessions[j]->GetCurrState() == NSS_INGAME))
604 m_Sessions[j]->SendMessage(message);
608 SAFE_DELETE(message);
612 void CNetServerWorker::HandleMessageReceive(const CNetMessage* message, CNetServerSession* session)
614 // Handle non-FSM messages first
615 Status status = session->GetFileTransferer().HandleMessageReceive(message);
616 if (status != INFO::SKIPPED)
617 return;
619 if (message->GetType() == NMT_FILE_TRANSFER_REQUEST)
621 CFileTransferRequestMessage* reqMessage = (CFileTransferRequestMessage*)message;
623 // Rejoining client got our JoinSyncStart after we received the state from
624 // another client, and has now requested that we forward it to them
626 ENSURE(!m_JoinSyncFile.empty());
627 session->GetFileTransferer().StartResponse(reqMessage->m_RequestID, m_JoinSyncFile);
629 return;
632 // Update FSM
633 if (!session->Update(message->GetType(), (void*)message))
634 LOGERROR("Net server: Error running FSM update (type=%d state=%d)", (int)message->GetType(), (int)session->GetCurrState());
637 void CNetServerWorker::SetupSession(CNetServerSession* session)
639 void* context = session;
641 // Set up transitions for session
643 session->AddTransition(NSS_UNCONNECTED, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
645 session->AddTransition(NSS_HANDSHAKE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
646 session->AddTransition(NSS_HANDSHAKE, (uint)NMT_CLIENT_HANDSHAKE, NSS_AUTHENTICATE, (void*)&OnClientHandshake, context);
648 session->AddTransition(NSS_LOBBY_AUTHENTICATE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
649 session->AddTransition(NSS_LOBBY_AUTHENTICATE, (uint)NMT_AUTHENTICATE, NSS_PREGAME, (void*)&OnAuthenticate, context);
651 session->AddTransition(NSS_AUTHENTICATE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
652 session->AddTransition(NSS_AUTHENTICATE, (uint)NMT_AUTHENTICATE, NSS_PREGAME, (void*)&OnAuthenticate, context);
654 session->AddTransition(NSS_PREGAME, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, (void*)&OnDisconnect, context);
655 session->AddTransition(NSS_PREGAME, (uint)NMT_CHAT, NSS_PREGAME, (void*)&OnChat, context);
656 session->AddTransition(NSS_PREGAME, (uint)NMT_READY, NSS_PREGAME, (void*)&OnReady, context);
657 session->AddTransition(NSS_PREGAME, (uint)NMT_CLEAR_ALL_READY, NSS_PREGAME, (void*)&OnClearAllReady, context);
658 session->AddTransition(NSS_PREGAME, (uint)NMT_GAME_SETUP, NSS_PREGAME, (void*)&OnGameSetup, context);
659 session->AddTransition(NSS_PREGAME, (uint)NMT_ASSIGN_PLAYER, NSS_PREGAME, (void*)&OnAssignPlayer, context);
660 session->AddTransition(NSS_PREGAME, (uint)NMT_KICKED, NSS_PREGAME, (void*)&OnKickPlayer, context);
661 session->AddTransition(NSS_PREGAME, (uint)NMT_GAME_START, NSS_PREGAME, (void*)&OnStartGame, context);
662 session->AddTransition(NSS_PREGAME, (uint)NMT_LOADED_GAME, NSS_INGAME, (void*)&OnLoadedGame, context);
664 session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_KICKED, NSS_JOIN_SYNCING, (void*)&OnKickPlayer, context);
665 session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, (void*)&OnDisconnect, context);
666 session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_LOADED_GAME, NSS_INGAME, (void*)&OnJoinSyncingLoadedGame, context);
668 session->AddTransition(NSS_INGAME, (uint)NMT_REJOINED, NSS_INGAME, (void*)&OnRejoined, context);
669 session->AddTransition(NSS_INGAME, (uint)NMT_KICKED, NSS_INGAME, (void*)&OnKickPlayer, context);
670 session->AddTransition(NSS_INGAME, (uint)NMT_CLIENT_PAUSED, NSS_INGAME, (void*)&OnClientPaused, context);
671 session->AddTransition(NSS_INGAME, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, (void*)&OnDisconnect, context);
672 session->AddTransition(NSS_INGAME, (uint)NMT_CHAT, NSS_INGAME, (void*)&OnChat, context);
673 session->AddTransition(NSS_INGAME, (uint)NMT_SIMULATION_COMMAND, NSS_INGAME, (void*)&OnInGame, context);
674 session->AddTransition(NSS_INGAME, (uint)NMT_SYNC_CHECK, NSS_INGAME, (void*)&OnInGame, context);
675 session->AddTransition(NSS_INGAME, (uint)NMT_END_COMMAND_BATCH, NSS_INGAME, (void*)&OnInGame, context);
677 // Set first state
678 session->SetFirstState(NSS_HANDSHAKE);
681 bool CNetServerWorker::HandleConnect(CNetServerSession* session)
683 if (std::find(m_BannedIPs.begin(), m_BannedIPs.end(), session->GetIPAddress()) != m_BannedIPs.end())
685 session->Disconnect(NDR_BANNED);
686 return false;
689 CSrvHandshakeMessage handshake;
690 handshake.m_Magic = PS_PROTOCOL_MAGIC;
691 handshake.m_ProtocolVersion = PS_PROTOCOL_VERSION;
692 handshake.m_SoftwareVersion = PS_PROTOCOL_VERSION;
693 return session->SendMessage(&handshake);
696 void CNetServerWorker::OnUserJoin(CNetServerSession* session)
698 AddPlayer(session->GetGUID(), session->GetUserName());
700 if (m_HostGUID.empty() && session->IsLocalClient())
701 m_HostGUID = session->GetGUID();
703 CGameSetupMessage gameSetupMessage(GetScriptInterface());
704 gameSetupMessage.m_Data = m_GameAttributes;
705 session->SendMessage(&gameSetupMessage);
707 CPlayerAssignmentMessage assignMessage;
708 ConstructPlayerAssignmentMessage(assignMessage);
709 session->SendMessage(&assignMessage);
712 void CNetServerWorker::OnUserLeave(CNetServerSession* session)
714 std::vector<CStr>::iterator pausing = std::find(m_PausingPlayers.begin(), m_PausingPlayers.end(), session->GetGUID());
715 if (pausing != m_PausingPlayers.end())
716 m_PausingPlayers.erase(pausing);
718 RemovePlayer(session->GetGUID());
720 if (m_ServerTurnManager && session->GetCurrState() != NSS_JOIN_SYNCING)
721 m_ServerTurnManager->UninitialiseClient(session->GetHostID()); // TODO: only for non-observers
723 // TODO: ought to switch the player controlled by that client
724 // back to AI control, or something?
727 void CNetServerWorker::AddPlayer(const CStr& guid, const CStrW& name)
729 // Find all player IDs in active use; we mustn't give them to a second player (excluding the unassigned ID: -1)
730 std::set<i32> usedIDs;
731 for (const std::pair<CStr, PlayerAssignment>& p : m_PlayerAssignments)
732 if (p.second.m_Enabled && p.second.m_PlayerID != -1)
733 usedIDs.insert(p.second.m_PlayerID);
735 // If the player is rejoining after disconnecting, try to give them
736 // back their old player ID
738 i32 playerID = -1;
740 // Try to match GUID first
741 for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it)
743 if (!it->second.m_Enabled && it->first == guid && usedIDs.find(it->second.m_PlayerID) == usedIDs.end())
745 playerID = it->second.m_PlayerID;
746 m_PlayerAssignments.erase(it); // delete the old mapping, since we've got a new one now
747 goto found;
751 // Try to match username next
752 for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it)
754 if (!it->second.m_Enabled && it->second.m_Name == name && usedIDs.find(it->second.m_PlayerID) == usedIDs.end())
756 playerID = it->second.m_PlayerID;
757 m_PlayerAssignments.erase(it); // delete the old mapping, since we've got a new one now
758 goto found;
762 // Otherwise leave the player ID as -1 (observer) and let gamesetup change it as needed.
764 found:
765 PlayerAssignment assignment;
766 assignment.m_Enabled = true;
767 assignment.m_Name = name;
768 assignment.m_PlayerID = playerID;
769 assignment.m_Status = 0;
770 m_PlayerAssignments[guid] = assignment;
772 // Send the new assignments to all currently active players
773 // (which does not include the one that's just joining)
774 SendPlayerAssignments();
777 void CNetServerWorker::RemovePlayer(const CStr& guid)
779 m_PlayerAssignments[guid].m_Enabled = false;
781 SendPlayerAssignments();
784 void CNetServerWorker::ClearAllPlayerReady()
786 for (std::pair<const CStr, PlayerAssignment>& p : m_PlayerAssignments)
787 if (p.second.m_Status != 2)
788 p.second.m_Status = 0;
790 SendPlayerAssignments();
793 void CNetServerWorker::KickPlayer(const CStrW& playerName, const bool ban)
795 // Find the user with that name
796 std::vector<CNetServerSession*>::iterator it = std::find_if(m_Sessions.begin(), m_Sessions.end(),
797 [&](CNetServerSession* session) { return session->GetUserName() == playerName; });
799 // and return if no one or the host has that name
800 if (it == m_Sessions.end() || (*it)->GetGUID() == m_HostGUID)
801 return;
803 if (ban)
805 // Remember name
806 if (std::find(m_BannedPlayers.begin(), m_BannedPlayers.end(), playerName) == m_BannedPlayers.end())
807 m_BannedPlayers.push_back(playerName);
809 // Remember IP address
810 u32 ipAddress = (*it)->GetIPAddress();
811 if (std::find(m_BannedIPs.begin(), m_BannedIPs.end(), ipAddress) == m_BannedIPs.end())
812 m_BannedIPs.push_back(ipAddress);
815 // Disconnect that user
816 (*it)->Disconnect(ban ? NDR_BANNED : NDR_KICKED);
818 // Send message notifying other clients
819 CKickedMessage kickedMessage;
820 kickedMessage.m_Name = playerName;
821 kickedMessage.m_Ban = ban;
822 Broadcast(&kickedMessage, { NSS_PREGAME, NSS_JOIN_SYNCING, NSS_INGAME });
825 void CNetServerWorker::AssignPlayer(int playerID, const CStr& guid)
827 // Remove anyone who's already assigned to this player
828 for (std::pair<const CStr, PlayerAssignment>& p : m_PlayerAssignments)
830 if (p.second.m_PlayerID == playerID)
831 p.second.m_PlayerID = -1;
834 // Update this host's assignment if it exists
835 if (m_PlayerAssignments.find(guid) != m_PlayerAssignments.end())
836 m_PlayerAssignments[guid].m_PlayerID = playerID;
838 SendPlayerAssignments();
841 void CNetServerWorker::ConstructPlayerAssignmentMessage(CPlayerAssignmentMessage& message)
843 for (const std::pair<CStr, PlayerAssignment>& p : m_PlayerAssignments)
845 if (!p.second.m_Enabled)
846 continue;
848 CPlayerAssignmentMessage::S_m_Hosts h;
849 h.m_GUID = p.first;
850 h.m_Name = p.second.m_Name;
851 h.m_PlayerID = p.second.m_PlayerID;
852 h.m_Status = p.second.m_Status;
853 message.m_Hosts.push_back(h);
857 void CNetServerWorker::SendPlayerAssignments()
859 CPlayerAssignmentMessage message;
860 ConstructPlayerAssignmentMessage(message);
861 Broadcast(&message, { NSS_PREGAME, NSS_JOIN_SYNCING, NSS_INGAME });
864 const ScriptInterface& CNetServerWorker::GetScriptInterface()
866 return *m_ScriptInterface;
869 void CNetServerWorker::SetTurnLength(u32 msecs)
871 if (m_ServerTurnManager)
872 m_ServerTurnManager->SetTurnLength(msecs);
875 void CNetServerWorker::ProcessLobbyAuth(const CStr& name, const CStr& token)
877 LOGMESSAGE("Net Server: Received lobby auth message from %s with %s", name, token);
878 // Find the user with that guid
879 std::vector<CNetServerSession*>::iterator it = std::find_if(m_Sessions.begin(), m_Sessions.end(),
880 [&](CNetServerSession* session)
881 { return session->GetGUID() == token; });
883 if (it == m_Sessions.end())
884 return;
886 (*it)->SetUserName(name.FromUTF8());
887 // Send an empty message to request the authentication message from the client
888 // after its identity has been confirmed via the lobby
889 CAuthenticateMessage emptyMessage;
890 (*it)->SendMessage(&emptyMessage);
893 bool CNetServerWorker::OnClientHandshake(void* context, CFsmEvent* event)
895 ENSURE(event->GetType() == (uint)NMT_CLIENT_HANDSHAKE);
897 CNetServerSession* session = (CNetServerSession*)context;
898 CNetServerWorker& server = session->GetServer();
900 CCliHandshakeMessage* message = (CCliHandshakeMessage*)event->GetParamRef();
901 if (message->m_ProtocolVersion != PS_PROTOCOL_VERSION)
903 session->Disconnect(NDR_INCORRECT_PROTOCOL_VERSION);
904 return false;
907 CStr guid = ps_generate_guid();
908 int count = 0;
909 // Ensure unique GUID
910 while(std::find_if(
911 server.m_Sessions.begin(), server.m_Sessions.end(),
912 [&guid] (const CNetServerSession* session)
913 { return session->GetGUID() == guid; }) != server.m_Sessions.end())
915 if (++count > 100)
917 session->Disconnect(NDR_UNKNOWN);
918 return true;
920 guid = ps_generate_guid();
923 session->SetGUID(guid);
925 CSrvHandshakeResponseMessage handshakeResponse;
926 handshakeResponse.m_UseProtocolVersion = PS_PROTOCOL_VERSION;
927 handshakeResponse.m_GUID = guid;
928 handshakeResponse.m_Flags = 0;
930 if (server.m_LobbyAuth)
932 handshakeResponse.m_Flags |= PS_NETWORK_FLAG_REQUIRE_LOBBYAUTH;
933 session->SetNextState(NSS_LOBBY_AUTHENTICATE);
936 session->SendMessage(&handshakeResponse);
938 return true;
941 bool CNetServerWorker::OnAuthenticate(void* context, CFsmEvent* event)
943 ENSURE(event->GetType() == (uint)NMT_AUTHENTICATE);
945 CNetServerSession* session = (CNetServerSession*)context;
946 CNetServerWorker& server = session->GetServer();
948 // Prohibit joins while the game is loading
949 if (server.m_State == SERVER_STATE_LOADING)
951 LOGMESSAGE("Refused connection while the game is loading");
952 session->Disconnect(NDR_SERVER_LOADING);
953 return true;
956 CAuthenticateMessage* message = (CAuthenticateMessage*)event->GetParamRef();
957 CStrW username = SanitisePlayerName(message->m_Name);
958 CStrW usernameWithoutRating(username.substr(0, username.find(L" (")));
960 // Compare the lowercase names as specified by https://xmpp.org/extensions/xep-0029.html#sect-idm139493404168176
961 // "[...] comparisons will be made in case-normalized canonical form."
962 if (server.m_LobbyAuth && usernameWithoutRating.LowerCase() != session->GetUserName().LowerCase())
964 LOGERROR("Net server: lobby auth: %s tried joining as %s",
965 session->GetUserName().ToUTF8(),
966 usernameWithoutRating.ToUTF8());
967 session->Disconnect(NDR_LOBBY_AUTH_FAILED);
968 return true;
971 // Either deduplicate or prohibit join if name is in use
972 bool duplicatePlayernames = false;
973 CFG_GET_VAL("network.duplicateplayernames", duplicatePlayernames);
974 if (duplicatePlayernames)
975 username = server.DeduplicatePlayerName(username);
976 else
978 std::vector<CNetServerSession*>::iterator it = std::find_if(
979 server.m_Sessions.begin(), server.m_Sessions.end(),
980 [&username] (const CNetServerSession* session)
981 { return session->GetUserName() == username; });
983 if (it != server.m_Sessions.end() && (*it) != session)
985 session->Disconnect(NDR_PLAYERNAME_IN_USE);
986 return true;
990 // Disconnect banned usernames
991 if (std::find(server.m_BannedPlayers.begin(), server.m_BannedPlayers.end(), username) != server.m_BannedPlayers.end())
993 session->Disconnect(NDR_BANNED);
994 return true;
997 int maxObservers = 0;
998 CFG_GET_VAL("network.observerlimit", maxObservers);
1000 bool isRejoining = false;
1001 bool serverFull = false;
1002 if (server.m_State == SERVER_STATE_PREGAME)
1004 // Don't check for maxObservers in the gamesetup, as we don't know yet who will be assigned
1005 serverFull = server.m_Sessions.size() >= MAX_CLIENTS;
1007 else
1009 bool isObserver = true;
1010 int disconnectedPlayers = 0;
1011 int connectedPlayers = 0;
1012 // (TODO: if GUIDs were stable, we should use them instead)
1013 for (const std::pair<CStr, PlayerAssignment>& p : server.m_PlayerAssignments)
1015 const PlayerAssignment& assignment = p.second;
1017 if (!assignment.m_Enabled && assignment.m_Name == username)
1019 isObserver = assignment.m_PlayerID == -1;
1020 isRejoining = true;
1023 if (assignment.m_PlayerID == -1)
1024 continue;
1026 if (assignment.m_Enabled)
1027 ++connectedPlayers;
1028 else
1029 ++disconnectedPlayers;
1032 // Optionally allow everyone or only buddies to join after the game has started
1033 if (!isRejoining)
1035 CStr observerLateJoin;
1036 CFG_GET_VAL("network.lateobservers", observerLateJoin);
1038 if (observerLateJoin == "everyone")
1040 isRejoining = true;
1042 else if (observerLateJoin == "buddies")
1044 CStr buddies;
1045 CFG_GET_VAL("lobby.buddies", buddies);
1046 std::wstringstream buddiesStream(wstring_from_utf8(buddies));
1047 CStrW buddy;
1048 while (std::getline(buddiesStream, buddy, L','))
1050 if (buddy == usernameWithoutRating)
1052 isRejoining = true;
1053 break;
1059 if (!isRejoining)
1061 LOGMESSAGE("Refused connection after game start from not-previously-known user \"%s\"", utf8_from_wstring(username));
1062 session->Disconnect(NDR_SERVER_ALREADY_IN_GAME);
1063 return true;
1066 // Ensure all players will be able to rejoin
1067 serverFull = isObserver && (
1068 (int) server.m_Sessions.size() - connectedPlayers > maxObservers ||
1069 (int) server.m_Sessions.size() + disconnectedPlayers >= MAX_CLIENTS);
1072 if (serverFull)
1074 session->Disconnect(NDR_SERVER_FULL);
1075 return true;
1078 // TODO: check server password etc?
1080 u32 newHostID = server.m_NextHostID++;
1082 session->SetUserName(username);
1083 session->SetHostID(newHostID);
1084 session->SetLocalClient(message->m_IsLocalClient);
1086 CAuthenticateResultMessage authenticateResult;
1087 authenticateResult.m_Code = isRejoining ? ARC_OK_REJOINING : ARC_OK;
1088 authenticateResult.m_HostID = newHostID;
1089 authenticateResult.m_Message = L"Logged in";
1090 session->SendMessage(&authenticateResult);
1092 server.OnUserJoin(session);
1094 if (isRejoining)
1096 // Request a copy of the current game state from an existing player,
1097 // so we can send it on to the new player
1099 // Assume session 0 is most likely the local player, so they're
1100 // the most efficient client to request a copy from
1101 CNetServerSession* sourceSession = server.m_Sessions.at(0);
1103 session->SetLongTimeout(true);
1105 sourceSession->GetFileTransferer().StartTask(
1106 shared_ptr<CNetFileReceiveTask>(new CNetFileReceiveTask_ServerRejoin(server, newHostID))
1109 session->SetNextState(NSS_JOIN_SYNCING);
1112 return true;
1115 bool CNetServerWorker::OnInGame(void* context, CFsmEvent* event)
1117 // TODO: should split each of these cases into a separate method
1119 CNetServerSession* session = (CNetServerSession*)context;
1120 CNetServerWorker& server = session->GetServer();
1122 CNetMessage* message = (CNetMessage*)event->GetParamRef();
1123 if (message->GetType() == (uint)NMT_SIMULATION_COMMAND)
1125 CSimulationMessage* simMessage = static_cast<CSimulationMessage*> (message);
1127 // Ignore messages sent by one player on behalf of another player
1128 // unless cheating is enabled
1129 bool cheatsEnabled = false;
1130 const ScriptInterface& scriptInterface = server.GetScriptInterface();
1131 JSContext* cx = scriptInterface.GetContext();
1132 JSAutoRequest rq(cx);
1133 JS::RootedValue settings(cx);
1134 scriptInterface.GetProperty(server.m_GameAttributes, "settings", &settings);
1135 if (scriptInterface.HasProperty(settings, "CheatsEnabled"))
1136 scriptInterface.GetProperty(settings, "CheatsEnabled", cheatsEnabled);
1138 PlayerAssignmentMap::iterator it = server.m_PlayerAssignments.find(session->GetGUID());
1139 // When cheating is disabled, fail if the player the message claims to
1140 // represent does not exist or does not match the sender's player name
1141 if (!cheatsEnabled && (it == server.m_PlayerAssignments.end() || it->second.m_PlayerID != simMessage->m_Player))
1142 return true;
1144 // Send it back to all clients that have finished
1145 // the loading screen (and the synchronization when rejoining)
1146 server.Broadcast(simMessage, { NSS_INGAME });
1148 // Save all the received commands
1149 if (server.m_SavedCommands.size() < simMessage->m_Turn + 1)
1150 server.m_SavedCommands.resize(simMessage->m_Turn + 1);
1151 server.m_SavedCommands[simMessage->m_Turn].push_back(*simMessage);
1153 // TODO: we shouldn't send the message back to the client that first sent it
1155 else if (message->GetType() == (uint)NMT_SYNC_CHECK)
1157 CSyncCheckMessage* syncMessage = static_cast<CSyncCheckMessage*> (message);
1158 server.m_ServerTurnManager->NotifyFinishedClientUpdate(*session, syncMessage->m_Turn, syncMessage->m_Hash);
1160 else if (message->GetType() == (uint)NMT_END_COMMAND_BATCH)
1162 // The turn-length field is ignored
1163 CEndCommandBatchMessage* endMessage = static_cast<CEndCommandBatchMessage*> (message);
1164 server.m_ServerTurnManager->NotifyFinishedClientCommands(*session, endMessage->m_Turn);
1167 return true;
1170 bool CNetServerWorker::OnChat(void* context, CFsmEvent* event)
1172 ENSURE(event->GetType() == (uint)NMT_CHAT);
1174 CNetServerSession* session = (CNetServerSession*)context;
1175 CNetServerWorker& server = session->GetServer();
1177 CChatMessage* message = (CChatMessage*)event->GetParamRef();
1179 message->m_GUID = session->GetGUID();
1181 server.Broadcast(message, { NSS_PREGAME, NSS_INGAME });
1183 return true;
1186 bool CNetServerWorker::OnReady(void* context, CFsmEvent* event)
1188 ENSURE(event->GetType() == (uint)NMT_READY);
1190 CNetServerSession* session = (CNetServerSession*)context;
1191 CNetServerWorker& server = session->GetServer();
1193 // Occurs if a client presses not-ready
1194 // in the very last moment before the hosts starts the game
1195 if (server.m_State == SERVER_STATE_LOADING)
1196 return true;
1198 CReadyMessage* message = (CReadyMessage*)event->GetParamRef();
1199 message->m_GUID = session->GetGUID();
1200 server.Broadcast(message, { NSS_PREGAME });
1202 server.m_PlayerAssignments[message->m_GUID].m_Status = message->m_Status;
1204 return true;
1207 bool CNetServerWorker::OnClearAllReady(void* context, CFsmEvent* event)
1209 ENSURE(event->GetType() == (uint)NMT_CLEAR_ALL_READY);
1211 CNetServerSession* session = (CNetServerSession*)context;
1212 CNetServerWorker& server = session->GetServer();
1214 if (session->GetGUID() == server.m_HostGUID)
1215 server.ClearAllPlayerReady();
1217 return true;
1220 bool CNetServerWorker::OnGameSetup(void* context, CFsmEvent* event)
1222 ENSURE(event->GetType() == (uint)NMT_GAME_SETUP);
1224 CNetServerSession* session = (CNetServerSession*)context;
1225 CNetServerWorker& server = session->GetServer();
1227 // Changing the settings after gamestart is not implemented and would cause an Out-of-sync error.
1228 // This happened when doubleclicking on the startgame button.
1229 if (server.m_State != SERVER_STATE_PREGAME)
1230 return true;
1232 if (session->GetGUID() == server.m_HostGUID)
1234 CGameSetupMessage* message = (CGameSetupMessage*)event->GetParamRef();
1235 server.UpdateGameAttributes(&(message->m_Data));
1237 return true;
1240 bool CNetServerWorker::OnAssignPlayer(void* context, CFsmEvent* event)
1242 ENSURE(event->GetType() == (uint)NMT_ASSIGN_PLAYER);
1243 CNetServerSession* session = (CNetServerSession*)context;
1244 CNetServerWorker& server = session->GetServer();
1246 if (session->GetGUID() == server.m_HostGUID)
1248 CAssignPlayerMessage* message = (CAssignPlayerMessage*)event->GetParamRef();
1249 server.AssignPlayer(message->m_PlayerID, message->m_GUID);
1251 return true;
1254 bool CNetServerWorker::OnStartGame(void* context, CFsmEvent* event)
1256 ENSURE(event->GetType() == (uint)NMT_GAME_START);
1257 CNetServerSession* session = (CNetServerSession*)context;
1258 CNetServerWorker& server = session->GetServer();
1260 if (session->GetGUID() == server.m_HostGUID)
1261 server.StartGame();
1263 return true;
1266 bool CNetServerWorker::OnLoadedGame(void* context, CFsmEvent* event)
1268 ENSURE(event->GetType() == (uint)NMT_LOADED_GAME);
1270 CNetServerSession* loadedSession = (CNetServerSession*)context;
1271 CNetServerWorker& server = loadedSession->GetServer();
1273 loadedSession->SetLongTimeout(false);
1275 // We're in the loading state, so wait until every client has loaded
1276 // before starting the game
1277 ENSURE(server.m_State == SERVER_STATE_LOADING);
1278 if (server.CheckGameLoadStatus(loadedSession))
1279 return true;
1281 CClientsLoadingMessage message;
1282 // We always send all GUIDs of clients in the loading state
1283 // so that we don't have to bother about switching GUI pages
1284 for (CNetServerSession* session : server.m_Sessions)
1285 if (session->GetCurrState() != NSS_INGAME && loadedSession->GetGUID() != session->GetGUID())
1287 CClientsLoadingMessage::S_m_Clients client;
1288 client.m_GUID = session->GetGUID();
1289 message.m_Clients.push_back(client);
1292 // Send to the client who has loaded the game but did not reach the NSS_INGAME state yet
1293 loadedSession->SendMessage(&message);
1294 server.Broadcast(&message, { NSS_INGAME });
1296 return true;
1299 bool CNetServerWorker::OnJoinSyncingLoadedGame(void* context, CFsmEvent* event)
1301 // A client rejoining an in-progress game has now finished loading the
1302 // map and deserialized the initial state.
1303 // The simulation may have progressed since then, so send any subsequent
1304 // commands to them and set them as an active player so they can participate
1305 // in all future turns.
1307 // (TODO: if it takes a long time for them to receive and execute all these
1308 // commands, the other players will get frozen for that time and may be unhappy;
1309 // we could try repeating this process a few times until the client converges
1310 // on the up-to-date state, before setting them as active.)
1312 ENSURE(event->GetType() == (uint)NMT_LOADED_GAME);
1314 CNetServerSession* session = (CNetServerSession*)context;
1315 CNetServerWorker& server = session->GetServer();
1317 CLoadedGameMessage* message = (CLoadedGameMessage*)event->GetParamRef();
1319 u32 turn = message->m_CurrentTurn;
1320 u32 readyTurn = server.m_ServerTurnManager->GetReadyTurn();
1322 // Send them all commands received since their saved state,
1323 // and turn-ended messages for any turns that have already been processed
1324 for (size_t i = turn + 1; i < std::max(readyTurn+1, (u32)server.m_SavedCommands.size()); ++i)
1326 if (i < server.m_SavedCommands.size())
1327 for (size_t j = 0; j < server.m_SavedCommands[i].size(); ++j)
1328 session->SendMessage(&server.m_SavedCommands[i][j]);
1330 if (i <= readyTurn)
1332 CEndCommandBatchMessage endMessage;
1333 endMessage.m_Turn = i;
1334 endMessage.m_TurnLength = server.m_ServerTurnManager->GetSavedTurnLength(i);
1335 session->SendMessage(&endMessage);
1339 // Tell the turn manager to expect commands from this new client
1340 server.m_ServerTurnManager->InitialiseClient(session->GetHostID(), readyTurn);
1342 // Tell the client that everything has finished loading and it should start now
1343 CLoadedGameMessage loaded;
1344 loaded.m_CurrentTurn = readyTurn;
1345 session->SendMessage(&loaded);
1347 return true;
1350 bool CNetServerWorker::OnRejoined(void* context, CFsmEvent* event)
1352 // A client has finished rejoining and the loading screen disappeared.
1353 ENSURE(event->GetType() == (uint)NMT_REJOINED);
1355 CNetServerSession* session = (CNetServerSession*)context;
1356 CNetServerWorker& server = session->GetServer();
1358 // Inform everyone of the client having rejoined
1359 CRejoinedMessage* message = (CRejoinedMessage*)event->GetParamRef();
1360 message->m_GUID = session->GetGUID();
1361 server.Broadcast(message, { NSS_INGAME });
1363 // Send all pausing players to the rejoined client.
1364 for (const CStr& guid : server.m_PausingPlayers)
1366 CClientPausedMessage pausedMessage;
1367 pausedMessage.m_GUID = guid;
1368 pausedMessage.m_Pause = true;
1369 session->SendMessage(&pausedMessage);
1372 session->SetLongTimeout(false);
1374 return true;
1377 bool CNetServerWorker::OnKickPlayer(void* context, CFsmEvent* event)
1379 ENSURE(event->GetType() == (uint)NMT_KICKED);
1381 CNetServerSession* session = (CNetServerSession*)context;
1382 CNetServerWorker& server = session->GetServer();
1384 if (session->GetGUID() == server.m_HostGUID)
1386 CKickedMessage* message = (CKickedMessage*)event->GetParamRef();
1387 server.KickPlayer(message->m_Name, message->m_Ban);
1389 return true;
1392 bool CNetServerWorker::OnDisconnect(void* context, CFsmEvent* event)
1394 ENSURE(event->GetType() == (uint)NMT_CONNECTION_LOST);
1396 CNetServerSession* session = (CNetServerSession*)context;
1397 CNetServerWorker& server = session->GetServer();
1399 server.OnUserLeave(session);
1401 return true;
1404 bool CNetServerWorker::OnClientPaused(void* context, CFsmEvent* event)
1406 ENSURE(event->GetType() == (uint)NMT_CLIENT_PAUSED);
1408 CNetServerSession* session = (CNetServerSession*)context;
1409 CNetServerWorker& server = session->GetServer();
1411 CClientPausedMessage* message = (CClientPausedMessage*)event->GetParamRef();
1413 message->m_GUID = session->GetGUID();
1415 // Update the list of pausing players.
1416 std::vector<CStr>::iterator player = std::find(server.m_PausingPlayers.begin(), server.m_PausingPlayers.end(), session->GetGUID());
1418 if (message->m_Pause)
1420 if (player != server.m_PausingPlayers.end())
1421 return true;
1423 server.m_PausingPlayers.push_back(session->GetGUID());
1425 else
1427 if (player == server.m_PausingPlayers.end())
1428 return true;
1430 server.m_PausingPlayers.erase(player);
1433 // Send messages to clients that are in game, and are not the client who paused.
1434 for (CNetServerSession* session : server.m_Sessions)
1436 if (session->GetCurrState() == NSS_INGAME && message->m_GUID != session->GetGUID())
1437 session->SendMessage(message);
1440 return true;
1443 bool CNetServerWorker::CheckGameLoadStatus(CNetServerSession* changedSession)
1445 for (const CNetServerSession* session : m_Sessions)
1446 if (session != changedSession && session->GetCurrState() != NSS_INGAME)
1447 return false;
1449 // Inform clients that everyone has loaded the map and that the game can start
1450 CLoadedGameMessage loaded;
1451 loaded.m_CurrentTurn = 0;
1453 // Notice the changedSession is still in the NSS_PREGAME state
1454 Broadcast(&loaded, { NSS_PREGAME, NSS_INGAME });
1456 m_State = SERVER_STATE_INGAME;
1457 return true;
1460 void CNetServerWorker::StartGame()
1462 for (std::pair<const CStr, PlayerAssignment>& player : m_PlayerAssignments)
1463 if (player.second.m_Enabled && player.second.m_PlayerID != -1 && player.second.m_Status == 0)
1465 LOGERROR("Tried to start the game without player \"%s\" being ready!", utf8_from_wstring(player.second.m_Name).c_str());
1466 return;
1469 m_ServerTurnManager = new CNetServerTurnManager(*this);
1471 for (CNetServerSession* session : m_Sessions)
1473 m_ServerTurnManager->InitialiseClient(session->GetHostID(), 0); // TODO: only for non-observers
1474 session->SetLongTimeout(true);
1477 m_State = SERVER_STATE_LOADING;
1479 // Send the final setup state to all clients
1480 UpdateGameAttributes(&m_GameAttributes);
1482 // Remove players and observers that are not present when the game starts
1483 for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end();)
1484 if (it->second.m_Enabled)
1485 ++it;
1486 else
1487 it = m_PlayerAssignments.erase(it);
1489 SendPlayerAssignments();
1491 CGameStartMessage gameStart;
1492 Broadcast(&gameStart, { NSS_PREGAME });
1495 void CNetServerWorker::UpdateGameAttributes(JS::MutableHandleValue attrs)
1497 m_GameAttributes = attrs;
1499 if (!m_Host)
1500 return;
1502 CGameSetupMessage gameSetupMessage(GetScriptInterface());
1503 gameSetupMessage.m_Data = m_GameAttributes;
1504 Broadcast(&gameSetupMessage, { NSS_PREGAME });
1507 CStrW CNetServerWorker::SanitisePlayerName(const CStrW& original)
1509 const size_t MAX_LENGTH = 32;
1511 CStrW name = original;
1512 name.Replace(L"[", L"{"); // remove GUI tags
1513 name.Replace(L"]", L"}"); // remove for symmetry
1515 // Restrict the length
1516 if (name.length() > MAX_LENGTH)
1517 name = name.Left(MAX_LENGTH);
1519 // Don't allow surrounding whitespace
1520 name.Trim(PS_TRIM_BOTH);
1522 // Don't allow empty name
1523 if (name.empty())
1524 name = L"Anonymous";
1526 return name;
1529 CStrW CNetServerWorker::DeduplicatePlayerName(const CStrW& original)
1531 CStrW name = original;
1533 // Try names "Foo", "Foo (2)", "Foo (3)", etc
1534 size_t id = 2;
1535 while (true)
1537 bool unique = true;
1538 for (const CNetServerSession* session : m_Sessions)
1540 if (session->GetUserName() == name)
1542 unique = false;
1543 break;
1547 if (unique)
1548 return name;
1550 name = original + L" (" + CStrW::FromUInt(id++) + L")";
1554 void CNetServerWorker::SendHolePunchingMessage(const CStr& ipStr, u16 port)
1556 StunClient::SendHolePunchingMessages(m_Host, ipStr.c_str(), port);
1562 CNetServer::CNetServer(bool useLobbyAuth, int autostartPlayers) :
1563 m_Worker(new CNetServerWorker(useLobbyAuth, autostartPlayers)),
1564 m_LobbyAuth(useLobbyAuth)
1568 CNetServer::~CNetServer()
1570 delete m_Worker;
1573 bool CNetServer::UseLobbyAuth() const
1575 return m_LobbyAuth;
1578 bool CNetServer::SetupConnection(const u16 port)
1580 return m_Worker->SetupConnection(port);
1583 void CNetServer::StartGame()
1585 CScopeLock lock(m_Worker->m_WorkerMutex);
1586 m_Worker->m_StartGameQueue.push_back(true);
1589 void CNetServer::UpdateGameAttributes(JS::MutableHandleValue attrs, const ScriptInterface& scriptInterface)
1591 // Pass the attributes as JSON, since that's the easiest safe
1592 // cross-thread way of passing script data
1593 std::string attrsJSON = scriptInterface.StringifyJSON(attrs, false);
1595 CScopeLock lock(m_Worker->m_WorkerMutex);
1596 m_Worker->m_GameAttributesQueue.push_back(attrsJSON);
1599 void CNetServer::OnLobbyAuth(const CStr& name, const CStr& token)
1601 CScopeLock lock(m_Worker->m_WorkerMutex);
1602 m_Worker->m_LobbyAuthQueue.push_back(std::make_pair(name, token));
1605 void CNetServer::SetTurnLength(u32 msecs)
1607 CScopeLock lock(m_Worker->m_WorkerMutex);
1608 m_Worker->m_TurnLengthQueue.push_back(msecs);
1611 void CNetServer::SendHolePunchingMessage(const CStr& ip, u16 port)
1613 m_Worker->SendHolePunchingMessage(ip, port);