1 /* Copyright (C) 2018 Wildfire Games.
2 * This file is part of 0 A.D.
3 *
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.
8 *
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.
13 *
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/>.
16 */
17
18 #include "precompiled.h"
19
20 #include "NetServer.h"
21
22 #include "NetClient.h"
23 #include "NetMessage.h"
24 #include "NetSession.h"
25 #include "NetServerTurnManager.h"
26 #include "NetStats.h"
27
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"
40
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
47
48 #include <string>
49
50 /**
51 * Number of peers to allocate for the enet host.
52 * Limited by ENET_PROTOCOL_MAXIMUM_PEER_ID (4096).
53 *
54 * At most 8 players, 32 observers and 1 temporary connection to send the "server full" disconnect-reason.
55 */
56 #define MAX_CLIENTS 41
57
58 #define DEFAULT_SERVER_NAME L"Unnamed Server"
59
60 static const int CHANNEL_COUNT = 1;
61
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.
66 */
67 static const int HOST_SERVICE_TIMEOUT = 50;
68
69 CNetServer* g_NetServer = NULL;
70
DebugName(CNetServerSession * session)71 static CStr DebugName(CNetServerSession* session)
72 {
73 if (session == NULL)
74 return "[unknown host]";
75 if (session->GetGUID().empty())
76 return "[unauthed host]";
77 return "[" + session->GetGUID().substr(0, 8) + "...]";
78 }
79
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.
83 */
84 class CNetFileReceiveTask_ServerRejoin : public CNetFileReceiveTask
85 {
86 NONCOPYABLE(CNetFileReceiveTask_ServerRejoin);
87 public:
CNetFileReceiveTask_ServerRejoin(CNetServerWorker & server,u32 hostID)88 CNetFileReceiveTask_ServerRejoin(CNetServerWorker& server, u32 hostID)
89 : m_Server(server), m_RejoinerHostID(hostID)
90 {
91 }
92
OnComplete()93 virtual void OnComplete()
94 {
95 // We've received the game state from an existing player - now
96 // we need to send it onwards to the newly rejoining player
97
98 // Find the session corresponding to the rejoining host (if any)
99 CNetServerSession* session = NULL;
100 for (CNetServerSession* serverSession : m_Server.m_Sessions)
101 {
102 if (serverSession->GetHostID() == m_RejoinerHostID)
103 {
104 session = serverSession;
105 break;
106 }
107 }
108
109 if (!session)
110 {
111 LOGMESSAGE("Net server: rejoining client disconnected before we sent to it");
112 return;
113 }
114
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);
122 }
123
124 private:
125 CNetServerWorker& m_Server;
126 u32 m_RejoinerHostID;
127 };
128
129 /*
130 * XXX: We use some non-threadsafe functions from the worker thread.
131 * See http://trac.wildfiregames.com/ticket/654
132 */
133
CNetServerWorker(bool useLobbyAuth,int autostartPlayers)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)
141 {
142 m_State = SERVER_STATE_UNCONNECTED;
143
144 m_ServerTurnManager = NULL;
145
146 m_ServerName = DEFAULT_SERVER_NAME;
147 }
148
~CNetServerWorker()149 CNetServerWorker::~CNetServerWorker()
150 {
151 if (m_State != SERVER_STATE_UNCONNECTED)
152 {
153 // Tell the thread to shut down
154 {
155 CScopeLock lock(m_WorkerMutex);
156 m_Shutdown = true;
157 }
158
159 // Wait for it to shut down cleanly
160 pthread_join(m_WorkerThread, NULL);
161 }
162
163 // Clean up resources
164
165 delete m_Stats;
166
167 for (CNetServerSession* session : m_Sessions)
168 {
169 session->DisconnectNow(NDR_SERVER_SHUTDOWN);
170 delete session;
171 }
172
173 if (m_Host)
174 enet_host_destroy(m_Host);
175
176 delete m_ServerTurnManager;
177 }
178
SetupConnection(const u16 port)179 bool CNetServerWorker::SetupConnection(const u16 port)
180 {
181 ENSURE(m_State == SERVER_STATE_UNCONNECTED);
182 ENSURE(!m_Host);
183
184 // Bind to default host
185 ENetAddress addr;
186 addr.host = ENET_HOST_ANY;
187 addr.port = port;
188
189 // Create ENet server
190 m_Host = enet_host_create(&addr, MAX_CLIENTS, CHANNEL_COUNT, 0, 0);
191 if (!m_Host)
192 {
193 LOGERROR("Net server: enet_host_create failed");
194 return false;
195 }
196
197 m_Stats = new CNetStatsTable();
198 if (CProfileViewer::IsInitialised())
199 g_ProfileViewer.AddRootTable(m_Stats);
200
201 m_State = SERVER_STATE_PREGAME;
202
203 // Launch the worker thread
204 int ret = pthread_create(&m_WorkerThread, NULL, &RunThread, this);
205 ENSURE(ret == 0);
206
207 #if CONFIG2_MINIUPNPC
208 // Launch the UPnP thread
209 ret = pthread_create(&m_UPnPThread, NULL, &SetupUPnP, NULL);
210 ENSURE(ret == 0);
211 #endif
212
213 return true;
214 }
215
216 #if CONFIG2_MINIUPNPC
SetupUPnP(void *)217 void* CNetServerWorker::SetupUPnP(void*)
218 {
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;
235
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());
241
242 int ret = 0;
243 bool allocatedUrls = false;
244
245 // Try a cached URL first
246 if (!rootDescURL.empty() && UPNP_GetIGDFromUrl(rootDescURL.c_str(), &urls, &data, internalIPAddress, sizeof(internalIPAddress)))
247 {
248 LOGMESSAGE("Net server: using cached IGD = %s", urls.controlURL);
249 ret = 1;
250 }
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
257 {
258 ret = UPNP_GetValidIGD(devlist, &urls, &data, internalIPAddress, sizeof(internalIPAddress));
259 allocatedUrls = ret != 0; // urls is allocated on non-zero return values
260 }
261 else
262 {
263 LOGMESSAGE("Net server: upnpDiscover failed and no working cached URL.");
264 return NULL;
265 }
266
267 switch (ret)
268 {
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");
283 }
284
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)
288 {
289 LOGMESSAGE("Net server: GetExternalIPAddress failed with code %d (%s)", ret, strupnperror(ret));
290 return NULL;
291 }
292 LOGMESSAGE("Net server: ExternalIPAddress = %s", externalIPAddress);
293
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)
298 {
299 LOGMESSAGE("Net server: AddPortMapping(%s, %s, %s) failed with code %d (%s)",
300 psPort, psPort, internalIPAddress, ret, strupnperror(ret));
301 return NULL;
302 }
303
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);
313
314 if (ret != UPNPCOMMAND_SUCCESS)
315 {
316 LOGMESSAGE("Net server: GetSpecificPortMappingEntry() failed with code %d (%s)", ret, strupnperror(ret));
317 return NULL;
318 }
319
320 LOGMESSAGE("Net server: External %s:%s %s is redirected to internal %s:%s (duration=%s)",
321 externalIPAddress, psPort, protocall, intClient, intPort, duration);
322
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);
327
328 // Make sure everything is properly freed.
329 if (allocatedUrls)
330 FreeUPNPUrls(&urls);
331
332 freeUPNPDevlist(devlist);
333
334 return NULL;
335 }
336 #endif // CONFIG2_MINIUPNPC
337
SendMessage(ENetPeer * peer,const CNetMessage * message)338 bool CNetServerWorker::SendMessage(ENetPeer* peer, const CNetMessage* message)
339 {
340 ENSURE(m_Host);
341
342 CNetServerSession* session = static_cast<CNetServerSession*>(peer->data);
343
344 return CNetHost::SendMessage(message, peer, DebugName(session).c_str());
345 }
346
Broadcast(const CNetMessage * message,const std::vector<NetServerSessionState> & targetStates)347 bool CNetServerWorker::Broadcast(const CNetMessage* message, const std::vector<NetServerSessionState>& targetStates)
348 {
349 ENSURE(m_Host);
350
351 bool ok = true;
352
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
355
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;
360
361 return ok;
362 }
363
RunThread(void * data)364 void* CNetServerWorker::RunThread(void* data)
365 {
366 debug_SetThreadName("NetServer");
367
368 static_cast<CNetServerWorker*>(data)->Run();
369
370 return NULL;
371 }
372
Run()373 void CNetServerWorker::Run()
374 {
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");
377
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());
382
383 while (true)
384 {
385 if (!RunStep())
386 break;
387
388 // Implement autostart mode
389 if (m_State == SERVER_STATE_PREGAME && (int)m_PlayerAssignments.size() == m_AutostartPlayers)
390 StartGame();
391
392 // Update profiler stats
393 m_Stats->LatchHostState(m_Host);
394 }
395
396 // Clear roots before deleting their context
397 m_SavedCommands.clear();
398
399 SAFE_DELETE(m_ScriptInterface);
400 }
401
RunStep()402 bool CNetServerWorker::RunStep()
403 {
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.)
407
408 m_ScriptInterface->GetRuntime()->MaybeIncrementalGC(0.5f);
409
410 JSContext* cx = m_ScriptInterface->GetContext();
411 JSAutoRequest rq(cx);
412
413 std::vector<bool> newStartGame;
414 std::vector<std::string> newGameAttributes;
415 std::vector<std::pair<CStr, CStr>> newLobbyAuths;
416 std::vector<u32> newTurnLength;
417
418 {
419 CScopeLock lock(m_WorkerMutex);
420
421 if (m_Shutdown)
422 return false;
423
424 newStartGame.swap(m_StartGameQueue);
425 newGameAttributes.swap(m_GameAttributesQueue);
426 newLobbyAuths.swap(m_LobbyAuthQueue);
427 newTurnLength.swap(m_TurnLengthQueue);
428 }
429
430 if (!newGameAttributes.empty())
431 {
432 JS::RootedValue gameAttributesVal(cx);
433 GetScriptInterface().ParseJSON(newGameAttributes.back(), &gameAttributesVal);
434 UpdateGameAttributes(&gameAttributesVal);
435 }
436
437 if (!newTurnLength.empty())
438 SetTurnLength(newTurnLength.back());
439
440 // Do StartGame last, so we have the most up-to-date game attributes when we start
441 if (!newStartGame.empty())
442 StartGame();
443
444 while (!newLobbyAuths.empty())
445 {
446 const std::pair<CStr, CStr>& auth = newLobbyAuths.back();
447 ProcessLobbyAuth(auth.first, auth.second);
448 newLobbyAuths.pop_back();
449 }
450
451 // Perform file transfers
452 for (CNetServerSession* session : m_Sessions)
453 session->GetFileTransferer().Poll();
454
455 CheckClientConnections();
456
457 // Process network events:
458
459 ENetEvent event;
460 int status = enet_host_service(m_Host, &event, HOST_SERVICE_TIMEOUT);
461 if (status < 0)
462 {
463 LOGERROR("CNetServerWorker: enet_host_service failed (%d)", status);
464 // TODO: notify game that the server has shut down
465 return false;
466 }
467
468 if (status == 0)
469 {
470 // Reached timeout with no events - try again
471 return true;
472 }
473
474 // Process the event:
475
476 switch (event.type)
477 {
478 case ENET_EVENT_TYPE_CONNECT:
479 {
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);
484
485 // Set up a session object for this peer
486
487 CNetServerSession* session = new CNetServerSession(*this, event.peer);
488
489 m_Sessions.push_back(session);
490
491 SetupSession(session);
492
493 ENSURE(event.peer->data == NULL);
494 event.peer->data = session;
495
496 HandleConnect(session);
497
498 break;
499 }
500
501 case ENET_EVENT_TYPE_DISCONNECT:
502 {
503 // If there is an active session with this peer, then reset and delete it
504
505 CNetServerSession* session = static_cast<CNetServerSession*>(event.peer->data);
506 if (session)
507 {
508 LOGMESSAGE("Net server: Disconnected %s", DebugName(session).c_str());
509
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());
513
514 session->Update((uint)NMT_CONNECTION_LOST, NULL);
515
516 delete session;
517 event.peer->data = NULL;
518 }
519
520 if (m_State == SERVER_STATE_LOADING)
521 CheckGameLoadStatus(NULL);
522
523 break;
524 }
525
526 case ENET_EVENT_TYPE_RECEIVE:
527 {
528 // If there is an active session with this peer, then process the message
529
530 CNetServerSession* session = static_cast<CNetServerSession*>(event.peer->data);
531 if (session)
532 {
533 // Create message from raw data
534 CNetMessage* msg = CNetMessageFactory::CreateMessage(event.packet->data, event.packet->dataLength, GetScriptInterface());
535 if (msg)
536 {
537 LOGMESSAGE("Net server: Received message %s of size %lu from %s", msg->ToString().c_str(), (unsigned long)msg->GetSerializedLength(), DebugName(session).c_str());
538
539 HandleMessageReceive(msg, session);
540
541 delete msg;
542 }
543 }
544
545 // Done using the packet
546 enet_packet_destroy(event.packet);
547
548 break;
549 }
550
551 case ENET_EVENT_TYPE_NONE:
552 break;
553 }
554
555 return true;
556 }
557
CheckClientConnections()558 void CNetServerWorker::CheckClientConnections()
559 {
560 // Send messages at most once per second
561 std::time_t now = std::time(nullptr);
562 if (now <= m_LastConnectionCheck)
563 return;
564
565 m_LastConnectionCheck = now;
566
567 for (size_t i = 0; i < m_Sessions.size(); ++i)
568 {
569 u32 lastReceived = m_Sessions[i]->GetLastReceivedTime();
570 u32 meanRTT = m_Sessions[i]->GetMeanRTT();
571
572 CNetMessage* message = nullptr;
573
574 // Report if we didn't hear from the client since few seconds
575 if (lastReceived > NETWORK_WARNING_TIMEOUT)
576 {
577 CClientTimeoutMessage* msg = new CClientTimeoutMessage();
578 msg->m_GUID = m_Sessions[i]->GetGUID();
579 msg->m_LastReceivedTime = lastReceived;
580 message = msg;
581 }
582 // Report if the client has bad ping
583 else if (meanRTT > DEFAULT_TURN_LENGTH_MP)
584 {
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;
591 }
592
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)
599 {
600 if (i != j && (
601 (m_Sessions[j]->GetCurrState() == NSS_PREGAME && m_State == SERVER_STATE_PREGAME) ||
602 m_Sessions[j]->GetCurrState() == NSS_INGAME))
603 {
604 m_Sessions[j]->SendMessage(message);
605 }
606 }
607
608 SAFE_DELETE(message);
609 }
610 }
611
HandleMessageReceive(const CNetMessage * message,CNetServerSession * session)612 void CNetServerWorker::HandleMessageReceive(const CNetMessage* message, CNetServerSession* session)
613 {
614 // Handle non-FSM messages first
615 Status status = session->GetFileTransferer().HandleMessageReceive(message);
616 if (status != INFO::SKIPPED)
617 return;
618
619 if (message->GetType() == NMT_FILE_TRANSFER_REQUEST)
620 {
621 CFileTransferRequestMessage* reqMessage = (CFileTransferRequestMessage*)message;
622
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
625
626 ENSURE(!m_JoinSyncFile.empty());
627 session->GetFileTransferer().StartResponse(reqMessage->m_RequestID, m_JoinSyncFile);
628
629 return;
630 }
631
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());
635 }
636
SetupSession(CNetServerSession * session)637 void CNetServerWorker::SetupSession(CNetServerSession* session)
638 {
639 void* context = session;
640
641 // Set up transitions for session
642
643 session->AddTransition(NSS_UNCONNECTED, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
644
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);
647
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);
650
651 session->AddTransition(NSS_AUTHENTICATE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
652 session->AddTransition(NSS_AUTHENTICATE, (uint)NMT_AUTHENTICATE, NSS_PREGAME, (void*)&OnAuthenticate, context);
653
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);
663
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);
667
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);
676
677 // Set first state
678 session->SetFirstState(NSS_HANDSHAKE);
679 }
680
HandleConnect(CNetServerSession * session)681 bool CNetServerWorker::HandleConnect(CNetServerSession* session)
682 {
683 if (std::find(m_BannedIPs.begin(), m_BannedIPs.end(), session->GetIPAddress()) != m_BannedIPs.end())
684 {
685 session->Disconnect(NDR_BANNED);
686 return false;
687 }
688
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);
694 }
695
OnUserJoin(CNetServerSession * session)696 void CNetServerWorker::OnUserJoin(CNetServerSession* session)
697 {
698 AddPlayer(session->GetGUID(), session->GetUserName());
699
700 if (m_HostGUID.empty() && session->IsLocalClient())
701 m_HostGUID = session->GetGUID();
702
703 CGameSetupMessage gameSetupMessage(GetScriptInterface());
704 gameSetupMessage.m_Data = m_GameAttributes;
705 session->SendMessage(&gameSetupMessage);
706
707 CPlayerAssignmentMessage assignMessage;
708 ConstructPlayerAssignmentMessage(assignMessage);
709 session->SendMessage(&assignMessage);
710 }
711
OnUserLeave(CNetServerSession * session)712 void CNetServerWorker::OnUserLeave(CNetServerSession* session)
713 {
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);
717
718 RemovePlayer(session->GetGUID());
719
720 if (m_ServerTurnManager && session->GetCurrState() != NSS_JOIN_SYNCING)
721 m_ServerTurnManager->UninitialiseClient(session->GetHostID()); // TODO: only for non-observers
722
723 // TODO: ought to switch the player controlled by that client
724 // back to AI control, or something?
725 }
726
AddPlayer(const CStr & guid,const CStrW & name)727 void CNetServerWorker::AddPlayer(const CStr& guid, const CStrW& name)
728 {
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);
734
735 // If the player is rejoining after disconnecting, try to give them
736 // back their old player ID
737
738 i32 playerID = -1;
739
740 // Try to match GUID first
741 for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it)
742 {
743 if (!it->second.m_Enabled && it->first == guid && usedIDs.find(it->second.m_PlayerID) == usedIDs.end())
744 {
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;
748 }
749 }
750
751 // Try to match username next
752 for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it)
753 {
754 if (!it->second.m_Enabled && it->second.m_Name == name && usedIDs.find(it->second.m_PlayerID) == usedIDs.end())
755 {
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;
759 }
760 }
761
762 // Otherwise leave the player ID as -1 (observer) and let gamesetup change it as needed.
763
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;
771
772 // Send the new assignments to all currently active players
773 // (which does not include the one that's just joining)
774 SendPlayerAssignments();
775 }
776
RemovePlayer(const CStr & guid)777 void CNetServerWorker::RemovePlayer(const CStr& guid)
778 {
779 m_PlayerAssignments[guid].m_Enabled = false;
780
781 SendPlayerAssignments();
782 }
783
ClearAllPlayerReady()784 void CNetServerWorker::ClearAllPlayerReady()
785 {
786 for (std::pair<const CStr, PlayerAssignment>& p : m_PlayerAssignments)
787 if (p.second.m_Status != 2)
788 p.second.m_Status = 0;
789
790 SendPlayerAssignments();
791 }
792
KickPlayer(const CStrW & playerName,const bool ban)793 void CNetServerWorker::KickPlayer(const CStrW& playerName, const bool ban)
794 {
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; });
798
799 // and return if no one or the host has that name
800 if (it == m_Sessions.end() || (*it)->GetGUID() == m_HostGUID)
801 return;
802
803 if (ban)
804 {
805 // Remember name
806 if (std::find(m_BannedPlayers.begin(), m_BannedPlayers.end(), playerName) == m_BannedPlayers.end())
807 m_BannedPlayers.push_back(m_LobbyAuth ? CStrW(playerName.substr(0, playerName.find(L" ("))) : playerName);
808
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);
813 }
814
815 // Disconnect that user
816 (*it)->Disconnect(ban ? NDR_BANNED : NDR_KICKED);
817
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 });
823 }
824
AssignPlayer(int playerID,const CStr & guid)825 void CNetServerWorker::AssignPlayer(int playerID, const CStr& guid)
826 {
827 // Remove anyone who's already assigned to this player
828 for (std::pair<const CStr, PlayerAssignment>& p : m_PlayerAssignments)
829 {
830 if (p.second.m_PlayerID == playerID)
831 p.second.m_PlayerID = -1;
832 }
833
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;
837
838 SendPlayerAssignments();
839 }
840
ConstructPlayerAssignmentMessage(CPlayerAssignmentMessage & message)841 void CNetServerWorker::ConstructPlayerAssignmentMessage(CPlayerAssignmentMessage& message)
842 {
843 for (const std::pair<CStr, PlayerAssignment>& p : m_PlayerAssignments)
844 {
845 if (!p.second.m_Enabled)
846 continue;
847
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);
854 }
855 }
856
SendPlayerAssignments()857 void CNetServerWorker::SendPlayerAssignments()
858 {
859 CPlayerAssignmentMessage message;
860 ConstructPlayerAssignmentMessage(message);
861 Broadcast(&message, { NSS_PREGAME, NSS_JOIN_SYNCING, NSS_INGAME });
862 }
863
GetScriptInterface()864 const ScriptInterface& CNetServerWorker::GetScriptInterface()
865 {
866 return *m_ScriptInterface;
867 }
868
SetTurnLength(u32 msecs)869 void CNetServerWorker::SetTurnLength(u32 msecs)
870 {
871 if (m_ServerTurnManager)
872 m_ServerTurnManager->SetTurnLength(msecs);
873 }
874
ProcessLobbyAuth(const CStr & name,const CStr & token)875 void CNetServerWorker::ProcessLobbyAuth(const CStr& name, const CStr& token)
876 {
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; });
882
883 if (it == m_Sessions.end())
884 return;
885
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);
891 }
892
OnClientHandshake(void * context,CFsmEvent * event)893 bool CNetServerWorker::OnClientHandshake(void* context, CFsmEvent* event)
894 {
895 ENSURE(event->GetType() == (uint)NMT_CLIENT_HANDSHAKE);
896
897 CNetServerSession* session = (CNetServerSession*)context;
898 CNetServerWorker& server = session->GetServer();
899
900 CCliHandshakeMessage* message = (CCliHandshakeMessage*)event->GetParamRef();
901 if (message->m_ProtocolVersion != PS_PROTOCOL_VERSION)
902 {
903 session->Disconnect(NDR_INCORRECT_PROTOCOL_VERSION);
904 return false;
905 }
906
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())
914 {
915 if (++count > 100)
916 {
917 session->Disconnect(NDR_UNKNOWN);
918 return true;
919 }
920 guid = ps_generate_guid();
921 }
922
923 session->SetGUID(guid);
924
925 CSrvHandshakeResponseMessage handshakeResponse;
926 handshakeResponse.m_UseProtocolVersion = PS_PROTOCOL_VERSION;
927 handshakeResponse.m_GUID = guid;
928 handshakeResponse.m_Flags = 0;
929
930 if (server.m_LobbyAuth)
931 {
932 handshakeResponse.m_Flags |= PS_NETWORK_FLAG_REQUIRE_LOBBYAUTH;
933 session->SetNextState(NSS_LOBBY_AUTHENTICATE);
934 }
935
936 session->SendMessage(&handshakeResponse);
937
938 return true;
939 }
940
OnAuthenticate(void * context,CFsmEvent * event)941 bool CNetServerWorker::OnAuthenticate(void* context, CFsmEvent* event)
942 {
943 ENSURE(event->GetType() == (uint)NMT_AUTHENTICATE);
944
945 CNetServerSession* session = (CNetServerSession*)context;
946 CNetServerWorker& server = session->GetServer();
947
948 // Prohibit joins while the game is loading
949 if (server.m_State == SERVER_STATE_LOADING)
950 {
951 LOGMESSAGE("Refused connection while the game is loading");
952 session->Disconnect(NDR_SERVER_LOADING);
953 return true;
954 }
955
956 CAuthenticateMessage* message = (CAuthenticateMessage*)event->GetParamRef();
957 CStrW username = SanitisePlayerName(message->m_Name);
958 CStrW usernameWithoutRating(username.substr(0, username.find(L" (")));
959
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())
963 {
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;
969 }
970
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
977 {
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; });
982
983 if (it != server.m_Sessions.end() && (*it) != session)
984 {
985 session->Disconnect(NDR_PLAYERNAME_IN_USE);
986 return true;
987 }
988 }
989
990 // Disconnect banned usernames
991 if (std::find(server.m_BannedPlayers.begin(), server.m_BannedPlayers.end(), server.m_LobbyAuth ? usernameWithoutRating : username) != server.m_BannedPlayers.end())
992 {
993 session->Disconnect(NDR_BANNED);
994 return true;
995 }
996
997 int maxObservers = 0;
998 CFG_GET_VAL("network.observerlimit", maxObservers);
999
1000 bool isRejoining = false;
1001 bool serverFull = false;
1002 if (server.m_State == SERVER_STATE_PREGAME)
1003 {
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;
1006 }
1007 else
1008 {
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)
1014 {
1015 const PlayerAssignment& assignment = p.second;
1016
1017 if (!assignment.m_Enabled && assignment.m_Name == username)
1018 {
1019 isObserver = assignment.m_PlayerID == -1;
1020 isRejoining = true;
1021 }
1022
1023 if (assignment.m_PlayerID == -1)
1024 continue;
1025
1026 if (assignment.m_Enabled)
1027 ++connectedPlayers;
1028 else
1029 ++disconnectedPlayers;
1030 }
1031
1032 // Optionally allow everyone or only buddies to join after the game has started
1033 if (!isRejoining)
1034 {
1035 CStr observerLateJoin;
1036 CFG_GET_VAL("network.lateobservers", observerLateJoin);
1037
1038 if (observerLateJoin == "everyone")
1039 {
1040 isRejoining = true;
1041 }
1042 else if (observerLateJoin == "buddies")
1043 {
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','))
1049 {
1050 if (buddy == usernameWithoutRating)
1051 {
1052 isRejoining = true;
1053 break;
1054 }
1055 }
1056 }
1057 }
1058
1059 if (!isRejoining)
1060 {
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;
1064 }
1065
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);
1070 }
1071
1072 if (serverFull)
1073 {
1074 session->Disconnect(NDR_SERVER_FULL);
1075 return true;
1076 }
1077
1078 // TODO: check server password etc?
1079
1080 u32 newHostID = server.m_NextHostID++;
1081
1082 session->SetUserName(username);
1083 session->SetHostID(newHostID);
1084 session->SetLocalClient(message->m_IsLocalClient);
1085
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);
1091
1092 server.OnUserJoin(session);
1093
1094 if (isRejoining)
1095 {
1096 // Request a copy of the current game state from an existing player,
1097 // so we can send it on to the new player
1098
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);
1102
1103 session->SetLongTimeout(true);
1104
1105 sourceSession->GetFileTransferer().StartTask(
1106 shared_ptr<CNetFileReceiveTask>(new CNetFileReceiveTask_ServerRejoin(server, newHostID))
1107 );
1108
1109 session->SetNextState(NSS_JOIN_SYNCING);
1110 }
1111
1112 return true;
1113 }
1114
OnInGame(void * context,CFsmEvent * event)1115 bool CNetServerWorker::OnInGame(void* context, CFsmEvent* event)
1116 {
1117 // TODO: should split each of these cases into a separate method
1118
1119 CNetServerSession* session = (CNetServerSession*)context;
1120 CNetServerWorker& server = session->GetServer();
1121
1122 CNetMessage* message = (CNetMessage*)event->GetParamRef();
1123 if (message->GetType() == (uint)NMT_SIMULATION_COMMAND)
1124 {
1125 CSimulationMessage* simMessage = static_cast<CSimulationMessage*> (message);
1126
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);
1137
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;
1143
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 });
1147
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);
1152
1153 // TODO: we shouldn't send the message back to the client that first sent it
1154 }
1155 else if (message->GetType() == (uint)NMT_SYNC_CHECK)
1156 {
1157 CSyncCheckMessage* syncMessage = static_cast<CSyncCheckMessage*> (message);
1158 server.m_ServerTurnManager->NotifyFinishedClientUpdate(*session, syncMessage->m_Turn, syncMessage->m_Hash);
1159 }
1160 else if (message->GetType() == (uint)NMT_END_COMMAND_BATCH)
1161 {
1162 // The turn-length field is ignored
1163 CEndCommandBatchMessage* endMessage = static_cast<CEndCommandBatchMessage*> (message);
1164 server.m_ServerTurnManager->NotifyFinishedClientCommands(*session, endMessage->m_Turn);
1165 }
1166
1167 return true;
1168 }
1169
OnChat(void * context,CFsmEvent * event)1170 bool CNetServerWorker::OnChat(void* context, CFsmEvent* event)
1171 {
1172 ENSURE(event->GetType() == (uint)NMT_CHAT);
1173
1174 CNetServerSession* session = (CNetServerSession*)context;
1175 CNetServerWorker& server = session->GetServer();
1176
1177 CChatMessage* message = (CChatMessage*)event->GetParamRef();
1178
1179 message->m_GUID = session->GetGUID();
1180
1181 server.Broadcast(message, { NSS_PREGAME, NSS_INGAME });
1182
1183 return true;
1184 }
1185
OnReady(void * context,CFsmEvent * event)1186 bool CNetServerWorker::OnReady(void* context, CFsmEvent* event)
1187 {
1188 ENSURE(event->GetType() == (uint)NMT_READY);
1189
1190 CNetServerSession* session = (CNetServerSession*)context;
1191 CNetServerWorker& server = session->GetServer();
1192
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;
1197
1198 CReadyMessage* message = (CReadyMessage*)event->GetParamRef();
1199 message->m_GUID = session->GetGUID();
1200 server.Broadcast(message, { NSS_PREGAME });
1201
1202 server.m_PlayerAssignments[message->m_GUID].m_Status = message->m_Status;
1203
1204 return true;
1205 }
1206
OnClearAllReady(void * context,CFsmEvent * event)1207 bool CNetServerWorker::OnClearAllReady(void* context, CFsmEvent* event)
1208 {
1209 ENSURE(event->GetType() == (uint)NMT_CLEAR_ALL_READY);
1210
1211 CNetServerSession* session = (CNetServerSession*)context;
1212 CNetServerWorker& server = session->GetServer();
1213
1214 if (session->GetGUID() == server.m_HostGUID)
1215 server.ClearAllPlayerReady();
1216
1217 return true;
1218 }
1219
OnGameSetup(void * context,CFsmEvent * event)1220 bool CNetServerWorker::OnGameSetup(void* context, CFsmEvent* event)
1221 {
1222 ENSURE(event->GetType() == (uint)NMT_GAME_SETUP);
1223
1224 CNetServerSession* session = (CNetServerSession*)context;
1225 CNetServerWorker& server = session->GetServer();
1226
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;
1231
1232 if (session->GetGUID() == server.m_HostGUID)
1233 {
1234 CGameSetupMessage* message = (CGameSetupMessage*)event->GetParamRef();
1235 server.UpdateGameAttributes(&(message->m_Data));
1236 }
1237 return true;
1238 }
1239
OnAssignPlayer(void * context,CFsmEvent * event)1240 bool CNetServerWorker::OnAssignPlayer(void* context, CFsmEvent* event)
1241 {
1242 ENSURE(event->GetType() == (uint)NMT_ASSIGN_PLAYER);
1243 CNetServerSession* session = (CNetServerSession*)context;
1244 CNetServerWorker& server = session->GetServer();
1245
1246 if (session->GetGUID() == server.m_HostGUID)
1247 {
1248 CAssignPlayerMessage* message = (CAssignPlayerMessage*)event->GetParamRef();
1249 server.AssignPlayer(message->m_PlayerID, message->m_GUID);
1250 }
1251 return true;
1252 }
1253
OnStartGame(void * context,CFsmEvent * event)1254 bool CNetServerWorker::OnStartGame(void* context, CFsmEvent* event)
1255 {
1256 ENSURE(event->GetType() == (uint)NMT_GAME_START);
1257 CNetServerSession* session = (CNetServerSession*)context;
1258 CNetServerWorker& server = session->GetServer();
1259
1260 if (session->GetGUID() == server.m_HostGUID)
1261 server.StartGame();
1262
1263 return true;
1264 }
1265
OnLoadedGame(void * context,CFsmEvent * event)1266 bool CNetServerWorker::OnLoadedGame(void* context, CFsmEvent* event)
1267 {
1268 ENSURE(event->GetType() == (uint)NMT_LOADED_GAME);
1269
1270 CNetServerSession* loadedSession = (CNetServerSession*)context;
1271 CNetServerWorker& server = loadedSession->GetServer();
1272
1273 loadedSession->SetLongTimeout(false);
1274
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;
1280
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())
1286 {
1287 CClientsLoadingMessage::S_m_Clients client;
1288 client.m_GUID = session->GetGUID();
1289 message.m_Clients.push_back(client);
1290 }
1291
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 });
1295
1296 return true;
1297 }
1298
OnJoinSyncingLoadedGame(void * context,CFsmEvent * event)1299 bool CNetServerWorker::OnJoinSyncingLoadedGame(void* context, CFsmEvent* event)
1300 {
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.
1306 //
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.)
1311
1312 ENSURE(event->GetType() == (uint)NMT_LOADED_GAME);
1313
1314 CNetServerSession* session = (CNetServerSession*)context;
1315 CNetServerWorker& server = session->GetServer();
1316
1317 CLoadedGameMessage* message = (CLoadedGameMessage*)event->GetParamRef();
1318
1319 u32 turn = message->m_CurrentTurn;
1320 u32 readyTurn = server.m_ServerTurnManager->GetReadyTurn();
1321
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)
1325 {
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]);
1329
1330 if (i <= readyTurn)
1331 {
1332 CEndCommandBatchMessage endMessage;
1333 endMessage.m_Turn = i;
1334 endMessage.m_TurnLength = server.m_ServerTurnManager->GetSavedTurnLength(i);
1335 session->SendMessage(&endMessage);
1336 }
1337 }
1338
1339 // Tell the turn manager to expect commands from this new client
1340 server.m_ServerTurnManager->InitialiseClient(session->GetHostID(), readyTurn);
1341
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);
1346
1347 return true;
1348 }
1349
OnRejoined(void * context,CFsmEvent * event)1350 bool CNetServerWorker::OnRejoined(void* context, CFsmEvent* event)
1351 {
1352 // A client has finished rejoining and the loading screen disappeared.
1353 ENSURE(event->GetType() == (uint)NMT_REJOINED);
1354
1355 CNetServerSession* session = (CNetServerSession*)context;
1356 CNetServerWorker& server = session->GetServer();
1357
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 });
1362
1363 // Send all pausing players to the rejoined client.
1364 for (const CStr& guid : server.m_PausingPlayers)
1365 {
1366 CClientPausedMessage pausedMessage;
1367 pausedMessage.m_GUID = guid;
1368 pausedMessage.m_Pause = true;
1369 session->SendMessage(&pausedMessage);
1370 }
1371
1372 session->SetLongTimeout(false);
1373
1374 return true;
1375 }
1376
OnKickPlayer(void * context,CFsmEvent * event)1377 bool CNetServerWorker::OnKickPlayer(void* context, CFsmEvent* event)
1378 {
1379 ENSURE(event->GetType() == (uint)NMT_KICKED);
1380
1381 CNetServerSession* session = (CNetServerSession*)context;
1382 CNetServerWorker& server = session->GetServer();
1383
1384 if (session->GetGUID() == server.m_HostGUID)
1385 {
1386 CKickedMessage* message = (CKickedMessage*)event->GetParamRef();
1387 server.KickPlayer(message->m_Name, message->m_Ban);
1388 }
1389 return true;
1390 }
1391
OnDisconnect(void * context,CFsmEvent * event)1392 bool CNetServerWorker::OnDisconnect(void* context, CFsmEvent* event)
1393 {
1394 ENSURE(event->GetType() == (uint)NMT_CONNECTION_LOST);
1395
1396 CNetServerSession* session = (CNetServerSession*)context;
1397 CNetServerWorker& server = session->GetServer();
1398
1399 server.OnUserLeave(session);
1400
1401 return true;
1402 }
1403
OnClientPaused(void * context,CFsmEvent * event)1404 bool CNetServerWorker::OnClientPaused(void* context, CFsmEvent* event)
1405 {
1406 ENSURE(event->GetType() == (uint)NMT_CLIENT_PAUSED);
1407
1408 CNetServerSession* session = (CNetServerSession*)context;
1409 CNetServerWorker& server = session->GetServer();
1410
1411 CClientPausedMessage* message = (CClientPausedMessage*)event->GetParamRef();
1412
1413 message->m_GUID = session->GetGUID();
1414
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());
1417
1418 if (message->m_Pause)
1419 {
1420 if (player != server.m_PausingPlayers.end())
1421 return true;
1422
1423 server.m_PausingPlayers.push_back(session->GetGUID());
1424 }
1425 else
1426 {
1427 if (player == server.m_PausingPlayers.end())
1428 return true;
1429
1430 server.m_PausingPlayers.erase(player);
1431 }
1432
1433 // Send messages to clients that are in game, and are not the client who paused.
1434 for (CNetServerSession* session : server.m_Sessions)
1435 {
1436 if (session->GetCurrState() == NSS_INGAME && message->m_GUID != session->GetGUID())
1437 session->SendMessage(message);
1438 }
1439
1440 return true;
1441 }
1442
CheckGameLoadStatus(CNetServerSession * changedSession)1443 bool CNetServerWorker::CheckGameLoadStatus(CNetServerSession* changedSession)
1444 {
1445 for (const CNetServerSession* session : m_Sessions)
1446 if (session != changedSession && session->GetCurrState() != NSS_INGAME)
1447 return false;
1448
1449 // Inform clients that everyone has loaded the map and that the game can start
1450 CLoadedGameMessage loaded;
1451 loaded.m_CurrentTurn = 0;
1452
1453 // Notice the changedSession is still in the NSS_PREGAME state
1454 Broadcast(&loaded, { NSS_PREGAME, NSS_INGAME });
1455
1456 m_State = SERVER_STATE_INGAME;
1457 return true;
1458 }
1459
StartGame()1460 void CNetServerWorker::StartGame()
1461 {
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)
1464 {
1465 LOGERROR("Tried to start the game without player \"%s\" being ready!", utf8_from_wstring(player.second.m_Name).c_str());
1466 return;
1467 }
1468
1469 m_ServerTurnManager = new CNetServerTurnManager(*this);
1470
1471 for (CNetServerSession* session : m_Sessions)
1472 {
1473 m_ServerTurnManager->InitialiseClient(session->GetHostID(), 0); // TODO: only for non-observers
1474 session->SetLongTimeout(true);
1475 }
1476
1477 m_State = SERVER_STATE_LOADING;
1478
1479 // Send the final setup state to all clients
1480 UpdateGameAttributes(&m_GameAttributes);
1481
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);
1488
1489 SendPlayerAssignments();
1490
1491 CGameStartMessage gameStart;
1492 Broadcast(&gameStart, { NSS_PREGAME });
1493 }
1494
UpdateGameAttributes(JS::MutableHandleValue attrs)1495 void CNetServerWorker::UpdateGameAttributes(JS::MutableHandleValue attrs)
1496 {
1497 m_GameAttributes = attrs;
1498
1499 if (!m_Host)
1500 return;
1501
1502 CGameSetupMessage gameSetupMessage(GetScriptInterface());
1503 gameSetupMessage.m_Data = m_GameAttributes;
1504 Broadcast(&gameSetupMessage, { NSS_PREGAME });
1505 }
1506
SanitisePlayerName(const CStrW & original)1507 CStrW CNetServerWorker::SanitisePlayerName(const CStrW& original)
1508 {
1509 const size_t MAX_LENGTH = 32;
1510
1511 CStrW name = original;
1512 name.Replace(L"[", L"{"); // remove GUI tags
1513 name.Replace(L"]", L"}"); // remove for symmetry
1514
1515 // Restrict the length
1516 if (name.length() > MAX_LENGTH)
1517 name = name.Left(MAX_LENGTH);
1518
1519 // Don't allow surrounding whitespace
1520 name.Trim(PS_TRIM_BOTH);
1521
1522 // Don't allow empty name
1523 if (name.empty())
1524 name = L"Anonymous";
1525
1526 return name;
1527 }
1528
DeduplicatePlayerName(const CStrW & original)1529 CStrW CNetServerWorker::DeduplicatePlayerName(const CStrW& original)
1530 {
1531 CStrW name = original;
1532
1533 // Try names "Foo", "Foo (2)", "Foo (3)", etc
1534 size_t id = 2;
1535 while (true)
1536 {
1537 bool unique = true;
1538 for (const CNetServerSession* session : m_Sessions)
1539 {
1540 if (session->GetUserName() == name)
1541 {
1542 unique = false;
1543 break;
1544 }
1545 }
1546
1547 if (unique)
1548 return name;
1549
1550 name = original + L" (" + CStrW::FromUInt(id++) + L")";
1551 }
1552 }
1553
SendHolePunchingMessage(const CStr & ipStr,u16 port)1554 void CNetServerWorker::SendHolePunchingMessage(const CStr& ipStr, u16 port)
1555 {
1556 StunClient::SendHolePunchingMessages(m_Host, ipStr.c_str(), port);
1557 }
1558
1559
1560
1561
CNetServer(bool useLobbyAuth,int autostartPlayers)1562 CNetServer::CNetServer(bool useLobbyAuth, int autostartPlayers) :
1563 m_Worker(new CNetServerWorker(useLobbyAuth, autostartPlayers)),
1564 m_LobbyAuth(useLobbyAuth)
1565 {
1566 }
1567
~CNetServer()1568 CNetServer::~CNetServer()
1569 {
1570 delete m_Worker;
1571 }
1572
UseLobbyAuth() const1573 bool CNetServer::UseLobbyAuth() const
1574 {
1575 return m_LobbyAuth;
1576 }
1577
SetupConnection(const u16 port)1578 bool CNetServer::SetupConnection(const u16 port)
1579 {
1580 return m_Worker->SetupConnection(port);
1581 }
1582
StartGame()1583 void CNetServer::StartGame()
1584 {
1585 CScopeLock lock(m_Worker->m_WorkerMutex);
1586 m_Worker->m_StartGameQueue.push_back(true);
1587 }
1588
UpdateGameAttributes(JS::MutableHandleValue attrs,const ScriptInterface & scriptInterface)1589 void CNetServer::UpdateGameAttributes(JS::MutableHandleValue attrs, const ScriptInterface& scriptInterface)
1590 {
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);
1594
1595 CScopeLock lock(m_Worker->m_WorkerMutex);
1596 m_Worker->m_GameAttributesQueue.push_back(attrsJSON);
1597 }
1598
OnLobbyAuth(const CStr & name,const CStr & token)1599 void CNetServer::OnLobbyAuth(const CStr& name, const CStr& token)
1600 {
1601 CScopeLock lock(m_Worker->m_WorkerMutex);
1602 m_Worker->m_LobbyAuthQueue.push_back(std::make_pair(name, token));
1603 }
1604
SetTurnLength(u32 msecs)1605 void CNetServer::SetTurnLength(u32 msecs)
1606 {
1607 CScopeLock lock(m_Worker->m_WorkerMutex);
1608 m_Worker->m_TurnLengthQueue.push_back(msecs);
1609 }
1610
SendHolePunchingMessage(const CStr & ip,u16 port)1611 void CNetServer::SendHolePunchingMessage(const CStr& ip, u16 port)
1612 {
1613 m_Worker->SendHolePunchingMessage(ip, port);
1614 }
1615