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