1 /* Copyright (C) 2022 Wildfire Games.
2 * This file is part of 0 A.D.
4 * 0 A.D. is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 2 of the License, or
7 * (at your option) any later version.
9 * 0 A.D. is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
18 #include "precompiled.h"
20 #include "NetServer.h"
22 #include "NetClient.h"
23 #include "NetMessage.h"
24 #include "NetSession.h"
25 #include "NetServerTurnManager.h"
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"
34 #include "ps/Hashing.h"
35 #include "ps/Profile.h"
36 #include "ps/Threading.h"
37 #include "scriptinterface/ScriptContext.h"
38 #include "scriptinterface/ScriptInterface.h"
39 #include "scriptinterface/JSON.h"
40 #include "simulation2/Simulation2.h"
41 #include "simulation2/system/TurnManager.h"
44 #include <miniupnpc/miniwget.h>
45 #include <miniupnpc/miniupnpc.h>
46 #include <miniupnpc/upnpcommands.h>
47 #include <miniupnpc/upnperrors.h>
53 * Number of peers to allocate for the enet host.
54 * Limited by ENET_PROTOCOL_MAXIMUM_PEER_ID (4096).
56 * At most 8 players, 32 observers and 1 temporary connection to send the "server full" disconnect-reason.
58 #define MAX_CLIENTS 41
60 #define DEFAULT_SERVER_NAME L"Unnamed Server"
62 constexpr int CHANNEL_COUNT
= 1;
63 constexpr int FAILED_PASSWORD_TRIES_BEFORE_BAN
= 3;
66 * enet_host_service timeout (msecs).
67 * Smaller numbers may hurt performance; larger numbers will
68 * hurt latency responding to messages from game thread.
70 static const int HOST_SERVICE_TIMEOUT
= 50;
73 * Once ping goes above turn length * command delay,
74 * the game will start 'freezing' for other clients while we catch up.
75 * Since commands are sent client -> server -> client, divide by 2.
76 * (duplicated in NetServer.cpp to avoid having to fetch the constants in a header file)
78 constexpr u32 NETWORK_BAD_PING
= DEFAULT_TURN_LENGTH
* COMMAND_DELAY_MP
/ 2;
80 CNetServer
* g_NetServer
= NULL
;
82 static CStr
DebugName(CNetServerSession
* session
)
85 return "[unknown host]";
86 if (session
->GetGUID().empty())
87 return "[unauthed host]";
88 return "[" + session
->GetGUID().substr(0, 8) + "...]";
92 * Async task for receiving the initial game state to be forwarded to another
93 * client that is rejoining an in-progress network game.
95 class CNetFileReceiveTask_ServerRejoin
: public CNetFileReceiveTask
97 NONCOPYABLE(CNetFileReceiveTask_ServerRejoin
);
99 CNetFileReceiveTask_ServerRejoin(CNetServerWorker
& server
, u32 hostID
)
100 : m_Server(server
), m_RejoinerHostID(hostID
)
104 virtual void OnComplete()
106 // We've received the game state from an existing player - now
107 // we need to send it onwards to the newly rejoining player
109 // Find the session corresponding to the rejoining host (if any)
110 CNetServerSession
* session
= NULL
;
111 for (CNetServerSession
* serverSession
: m_Server
.m_Sessions
)
113 if (serverSession
->GetHostID() == m_RejoinerHostID
)
115 session
= serverSession
;
122 LOGMESSAGE("Net server: rejoining client disconnected before we sent to it");
126 // Store the received state file, and tell the client to start downloading it from us
127 // TODO: this will get kind of confused if there's multiple clients downloading in parallel;
128 // they'll race and get whichever happens to be the latest received by the server,
129 // which should still work but isn't great
130 m_Server
.m_JoinSyncFile
= m_Buffer
;
132 // Send the init attributes alongside - these should be correct since the game should be started.
133 CJoinSyncStartMessage message
;
134 message
.m_InitAttributes
= Script::StringifyJSON(ScriptRequest(m_Server
.GetScriptInterface()), &m_Server
.m_InitAttributes
);
135 session
->SendMessage(&message
);
139 CNetServerWorker
& m_Server
;
140 u32 m_RejoinerHostID
;
144 * XXX: We use some non-threadsafe functions from the worker thread.
145 * See http://trac.wildfiregames.com/ticket/654
148 CNetServerWorker::CNetServerWorker(bool useLobbyAuth
) :
149 m_LobbyAuth(useLobbyAuth
),
151 m_ScriptInterface(NULL
),
152 m_NextHostID(1), m_Host(NULL
), m_ControllerGUID(), m_Stats(NULL
),
153 m_LastConnectionCheck(0)
155 m_State
= SERVER_STATE_UNCONNECTED
;
157 m_ServerTurnManager
= NULL
;
159 m_ServerName
= DEFAULT_SERVER_NAME
;
162 CNetServerWorker::~CNetServerWorker()
164 if (m_State
!= SERVER_STATE_UNCONNECTED
)
166 // Tell the thread to shut down
168 std::lock_guard
<std::mutex
> lock(m_WorkerMutex
);
172 // Wait for it to shut down cleanly
173 m_WorkerThread
.join();
176 #if CONFIG2_MINIUPNPC
177 if (m_UPnPThread
.joinable())
178 m_UPnPThread
.detach();
181 // Clean up resources
185 for (CNetServerSession
* session
: m_Sessions
)
187 session
->DisconnectNow(NDR_SERVER_SHUTDOWN
);
192 enet_host_destroy(m_Host
);
194 delete m_ServerTurnManager
;
197 void CNetServerWorker::SetPassword(const CStr
& hashedPassword
)
199 m_Password
= hashedPassword
;
203 void CNetServerWorker::SetControllerSecret(const std::string
& secret
)
205 m_ControllerSecret
= secret
;
209 bool CNetServerWorker::CheckPassword(const std::string
& password
, const std::string
& salt
) const
211 return HashCryptographically(m_Password
, salt
) == password
;
215 bool CNetServerWorker::SetupConnection(const u16 port
)
217 ENSURE(m_State
== SERVER_STATE_UNCONNECTED
);
220 // Bind to default host
222 addr
.host
= ENET_HOST_ANY
;
225 // Create ENet server
226 m_Host
= enet_host_create(&addr
, MAX_CLIENTS
, CHANNEL_COUNT
, 0, 0);
229 LOGERROR("Net server: enet_host_create failed");
233 m_Stats
= new CNetStatsTable();
234 if (CProfileViewer::IsInitialised())
235 g_ProfileViewer
.AddRootTable(m_Stats
);
237 m_State
= SERVER_STATE_PREGAME
;
239 // Launch the worker thread
240 m_WorkerThread
= std::thread(Threading::HandleExceptions
<RunThread
>::Wrapper
, this);
242 #if CONFIG2_MINIUPNPC
243 // Launch the UPnP thread
244 m_UPnPThread
= std::thread(Threading::HandleExceptions
<SetupUPnP
>::Wrapper
);
250 #if CONFIG2_MINIUPNPC
251 void CNetServerWorker::SetupUPnP()
253 debug_SetThreadName("UPnP");
255 // Values we want to set.
257 sprintf_s(psPort
, ARRAY_SIZE(psPort
), "%d", PS_DEFAULT_PORT
);
258 const char* leaseDuration
= "0"; // Indefinite/permanent lease duration.
259 const char* description
= "0AD Multiplayer";
260 const char* protocall
= "UDP";
261 char internalIPAddress
[64];
262 char externalIPAddress
[40];
264 // Variables to hold the values that actually get set.
269 // Intermediate variables.
270 bool allocatedUrls
= false;
271 struct UPNPUrls urls
;
272 struct IGDdatas data
;
273 struct UPNPDev
* devlist
= NULL
;
275 // Make sure everything is properly freed.
276 std::function
<void()> freeUPnP
= [&allocatedUrls
, &urls
, &devlist
]()
280 freeUPNPDevlist(devlist
);
281 // IGDdatas does not need to be freed according to UPNP_GetIGDFromUrl
284 // Cached root descriptor URL.
285 std::string rootDescURL
;
286 CFG_GET_VAL("network.upnprootdescurl", rootDescURL
);
287 if (!rootDescURL
.empty())
288 LOGMESSAGE("Net server: attempting to use cached root descriptor URL: %s", rootDescURL
.c_str());
292 // Try a cached URL first
293 if (!rootDescURL
.empty() && UPNP_GetIGDFromUrl(rootDescURL
.c_str(), &urls
, &data
, internalIPAddress
, sizeof(internalIPAddress
)))
295 LOGMESSAGE("Net server: using cached IGD = %s", urls
.controlURL
);
298 // No cached URL, or it did not respond. Try getting a valid UPnP device for 10 seconds.
299 #if defined(MINIUPNPC_API_VERSION) && MINIUPNPC_API_VERSION >= 14
300 else if ((devlist
= upnpDiscover(10000, 0, 0, 0, 0, 2, 0)) != NULL
)
302 else if ((devlist
= upnpDiscover(10000, 0, 0, 0, 0, 0)) != NULL
)
305 ret
= UPNP_GetValidIGD(devlist
, &urls
, &data
, internalIPAddress
, sizeof(internalIPAddress
));
306 allocatedUrls
= ret
!= 0; // urls is allocated on non-zero return values
310 LOGMESSAGE("Net server: upnpDiscover failed and no working cached URL.");
318 LOGMESSAGE("Net server: No IGD found");
321 LOGMESSAGE("Net server: found valid IGD = %s", urls
.controlURL
);
324 LOGMESSAGE("Net server: found a valid, not connected IGD = %s, will try to continue anyway", urls
.controlURL
);
327 LOGMESSAGE("Net server: found a UPnP device unrecognized as IGD = %s, will try to continue anyway", urls
.controlURL
);
330 debug_warn(L
"Unrecognized return value from UPNP_GetValidIGD");
333 // Try getting our external/internet facing IP. TODO: Display this on the game-setup page for conviniance.
334 ret
= UPNP_GetExternalIPAddress(urls
.controlURL
, data
.first
.servicetype
, externalIPAddress
);
335 if (ret
!= UPNPCOMMAND_SUCCESS
)
337 LOGMESSAGE("Net server: GetExternalIPAddress failed with code %d (%s)", ret
, strupnperror(ret
));
341 LOGMESSAGE("Net server: ExternalIPAddress = %s", externalIPAddress
);
343 // Try to setup port forwarding.
344 ret
= UPNP_AddPortMapping(urls
.controlURL
, data
.first
.servicetype
, psPort
, psPort
,
345 internalIPAddress
, description
, protocall
, 0, leaseDuration
);
346 if (ret
!= UPNPCOMMAND_SUCCESS
)
348 LOGMESSAGE("Net server: AddPortMapping(%s, %s, %s) failed with code %d (%s)",
349 psPort
, psPort
, internalIPAddress
, ret
, strupnperror(ret
));
354 // Check that the port was actually forwarded.
355 ret
= UPNP_GetSpecificPortMappingEntry(urls
.controlURL
,
356 data
.first
.servicetype
,
358 #if defined(MINIUPNPC_API_VERSION) && MINIUPNPC_API_VERSION >= 10
361 intClient
, intPort
, NULL
/*desc*/,
362 NULL
/*enabled*/, duration
);
364 if (ret
!= UPNPCOMMAND_SUCCESS
)
366 LOGMESSAGE("Net server: GetSpecificPortMappingEntry() failed with code %d (%s)", ret
, strupnperror(ret
));
371 LOGMESSAGE("Net server: External %s:%s %s is redirected to internal %s:%s (duration=%s)",
372 externalIPAddress
, psPort
, protocall
, intClient
, intPort
, duration
);
374 // Cache root descriptor URL to try to avoid discovery next time.
375 g_ConfigDB
.SetValueString(CFG_USER
, "network.upnprootdescurl", urls
.controlURL
);
376 g_ConfigDB
.WriteValueToFile(CFG_USER
, "network.upnprootdescurl", urls
.controlURL
);
377 LOGMESSAGE("Net server: cached UPnP root descriptor URL as %s", urls
.controlURL
);
381 #endif // CONFIG2_MINIUPNPC
383 bool CNetServerWorker::SendMessage(ENetPeer
* peer
, const CNetMessage
* message
)
387 CNetServerSession
* session
= static_cast<CNetServerSession
*>(peer
->data
);
389 return CNetHost::SendMessage(message
, peer
, DebugName(session
).c_str());
392 bool CNetServerWorker::Broadcast(const CNetMessage
* message
, const std::vector
<NetServerSessionState
>& targetStates
)
398 // TODO: this does lots of repeated message serialisation if we have lots
399 // of remote peers; could do it more efficiently if that's a real problem
401 for (CNetServerSession
* session
: m_Sessions
)
402 if (std::find(targetStates
.begin(), targetStates
.end(), static_cast<NetServerSessionState
>(session
->GetCurrState())) != targetStates
.end() &&
403 !session
->SendMessage(message
))
409 void CNetServerWorker::RunThread(CNetServerWorker
* data
)
411 debug_SetThreadName("NetServer");
416 void CNetServerWorker::Run()
418 // The script context uses the profiler and therefore the thread must be registered before the context is created
419 g_Profiler2
.RegisterCurrentThread("Net server");
421 // We create a new ScriptContext for this network thread, with a single ScriptInterface.
422 std::shared_ptr
<ScriptContext
> netServerContext
= ScriptContext::CreateContext();
423 m_ScriptInterface
= new ScriptInterface("Engine", "Net server", netServerContext
);
424 m_InitAttributes
.init(m_ScriptInterface
->GetGeneralJSContext(), JS::UndefinedValue());
431 // Update profiler stats
432 m_Stats
->LatchHostState(m_Host
);
435 // Clear roots before deleting their context
436 m_SavedCommands
.clear();
438 SAFE_DELETE(m_ScriptInterface
);
441 bool CNetServerWorker::RunStep()
443 // Check for messages from the game thread.
444 // (Do as little work as possible while the mutex is held open,
445 // to avoid performance problems and deadlocks.)
447 m_ScriptInterface
->GetContext()->MaybeIncrementalGC(0.5f
);
449 ScriptRequest
rq(m_ScriptInterface
);
451 std::vector
<bool> newStartGame
;
452 std::vector
<std::string
> newGameAttributes
;
453 std::vector
<std::pair
<CStr
, CStr
>> newLobbyAuths
;
454 std::vector
<u32
> newTurnLength
;
457 std::lock_guard
<std::mutex
> lock(m_WorkerMutex
);
462 newStartGame
.swap(m_StartGameQueue
);
463 newGameAttributes
.swap(m_InitAttributesQueue
);
464 newLobbyAuths
.swap(m_LobbyAuthQueue
);
465 newTurnLength
.swap(m_TurnLengthQueue
);
468 if (!newGameAttributes
.empty())
470 if (m_State
!= SERVER_STATE_UNCONNECTED
&& m_State
!= SERVER_STATE_PREGAME
)
471 LOGERROR("NetServer: Init Attributes cannot be changed after the server starts loading.");
474 JS::RootedValue
gameAttributesVal(rq
.cx
);
475 Script::ParseJSON(rq
, newGameAttributes
.back(), &gameAttributesVal
);
476 m_InitAttributes
= gameAttributesVal
;
480 if (!newTurnLength
.empty())
481 SetTurnLength(newTurnLength
.back());
483 while (!newLobbyAuths
.empty())
485 const std::pair
<CStr
, CStr
>& auth
= newLobbyAuths
.back();
486 ProcessLobbyAuth(auth
.first
, auth
.second
);
487 newLobbyAuths
.pop_back();
490 // Perform file transfers
491 for (CNetServerSession
* session
: m_Sessions
)
492 session
->GetFileTransferer().Poll();
494 CheckClientConnections();
496 // Process network events:
499 int status
= enet_host_service(m_Host
, &event
, HOST_SERVICE_TIMEOUT
);
502 LOGERROR("CNetServerWorker: enet_host_service failed (%d)", status
);
503 // TODO: notify game that the server has shut down
509 // Reached timeout with no events - try again
513 // Process the event:
517 case ENET_EVENT_TYPE_CONNECT
:
519 // Report the client address
520 char hostname
[256] = "(error)";
521 enet_address_get_host_ip(&event
.peer
->address
, hostname
, ARRAY_SIZE(hostname
));
522 LOGMESSAGE("Net server: Received connection from %s:%u", hostname
, (unsigned int)event
.peer
->address
.port
);
524 // Set up a session object for this peer
526 CNetServerSession
* session
= new CNetServerSession(*this, event
.peer
);
528 m_Sessions
.push_back(session
);
530 SetupSession(session
);
532 ENSURE(event
.peer
->data
== NULL
);
533 event
.peer
->data
= session
;
535 HandleConnect(session
);
540 case ENET_EVENT_TYPE_DISCONNECT
:
542 // If there is an active session with this peer, then reset and delete it
544 CNetServerSession
* session
= static_cast<CNetServerSession
*>(event
.peer
->data
);
547 LOGMESSAGE("Net server: Disconnected %s", DebugName(session
).c_str());
549 // Remove the session first, so we won't send player-update messages to it
550 // when updating the FSM
551 m_Sessions
.erase(remove(m_Sessions
.begin(), m_Sessions
.end(), session
), m_Sessions
.end());
553 session
->Update((uint
)NMT_CONNECTION_LOST
, NULL
);
556 event
.peer
->data
= NULL
;
559 if (m_State
== SERVER_STATE_LOADING
)
560 CheckGameLoadStatus(NULL
);
565 case ENET_EVENT_TYPE_RECEIVE
:
567 // If there is an active session with this peer, then process the message
569 CNetServerSession
* session
= static_cast<CNetServerSession
*>(event
.peer
->data
);
572 // Create message from raw data
573 CNetMessage
* msg
= CNetMessageFactory::CreateMessage(event
.packet
->data
, event
.packet
->dataLength
, GetScriptInterface());
576 LOGMESSAGE("Net server: Received message %s of size %lu from %s", msg
->ToString().c_str(), (unsigned long)msg
->GetSerializedLength(), DebugName(session
).c_str());
578 HandleMessageReceive(msg
, session
);
584 // Done using the packet
585 enet_packet_destroy(event
.packet
);
590 case ENET_EVENT_TYPE_NONE
:
597 void CNetServerWorker::CheckClientConnections()
599 // Send messages at most once per second
600 std::time_t now
= std::time(nullptr);
601 if (now
<= m_LastConnectionCheck
)
604 m_LastConnectionCheck
= now
;
606 for (size_t i
= 0; i
< m_Sessions
.size(); ++i
)
608 u32 lastReceived
= m_Sessions
[i
]->GetLastReceivedTime();
609 u32 meanRTT
= m_Sessions
[i
]->GetMeanRTT();
611 CNetMessage
* message
= nullptr;
613 // Report if we didn't hear from the client since few seconds
614 if (lastReceived
> NETWORK_WARNING_TIMEOUT
)
616 CClientTimeoutMessage
* msg
= new CClientTimeoutMessage();
617 msg
->m_GUID
= m_Sessions
[i
]->GetGUID();
618 msg
->m_LastReceivedTime
= lastReceived
;
621 // Report if the client has bad ping
622 else if (meanRTT
> NETWORK_BAD_PING
)
624 CClientPerformanceMessage
* msg
= new CClientPerformanceMessage();
625 CClientPerformanceMessage::S_m_Clients client
;
626 client
.m_GUID
= m_Sessions
[i
]->GetGUID();
627 client
.m_MeanRTT
= meanRTT
;
628 msg
->m_Clients
.push_back(client
);
632 // Send to all clients except the affected one
633 // (since that will show the locally triggered warning instead).
634 // Also send it to clients that finished the loading screen while
635 // the game is still waiting for other clients to finish the loading screen.
637 for (size_t j
= 0; j
< m_Sessions
.size(); ++j
)
640 (m_Sessions
[j
]->GetCurrState() == NSS_PREGAME
&& m_State
== SERVER_STATE_PREGAME
) ||
641 m_Sessions
[j
]->GetCurrState() == NSS_INGAME
))
643 m_Sessions
[j
]->SendMessage(message
);
647 SAFE_DELETE(message
);
651 void CNetServerWorker::HandleMessageReceive(const CNetMessage
* message
, CNetServerSession
* session
)
653 // Handle non-FSM messages first
654 Status status
= session
->GetFileTransferer().HandleMessageReceive(*message
);
655 if (status
!= INFO::SKIPPED
)
658 if (message
->GetType() == NMT_FILE_TRANSFER_REQUEST
)
660 CFileTransferRequestMessage
* reqMessage
= (CFileTransferRequestMessage
*)message
;
662 // Rejoining client got our JoinSyncStart after we received the state from
663 // another client, and has now requested that we forward it to them
665 ENSURE(!m_JoinSyncFile
.empty());
666 session
->GetFileTransferer().StartResponse(reqMessage
->m_RequestID
, m_JoinSyncFile
);
672 if (!session
->Update(message
->GetType(), (void*)message
))
673 LOGERROR("Net server: Error running FSM update (type=%d state=%d)", (int)message
->GetType(), (int)session
->GetCurrState());
676 void CNetServerWorker::SetupSession(CNetServerSession
* session
)
678 void* context
= session
;
680 // Set up transitions for session
682 session
->AddTransition(NSS_UNCONNECTED
, (uint
)NMT_CONNECTION_LOST
, NSS_UNCONNECTED
);
684 session
->AddTransition(NSS_HANDSHAKE
, (uint
)NMT_CONNECTION_LOST
, NSS_UNCONNECTED
);
685 session
->AddTransition(NSS_HANDSHAKE
, (uint
)NMT_CLIENT_HANDSHAKE
, NSS_AUTHENTICATE
, (void*)&OnClientHandshake
, context
);
687 session
->AddTransition(NSS_LOBBY_AUTHENTICATE
, (uint
)NMT_CONNECTION_LOST
, NSS_UNCONNECTED
);
688 session
->AddTransition(NSS_LOBBY_AUTHENTICATE
, (uint
)NMT_AUTHENTICATE
, NSS_PREGAME
, (void*)&OnAuthenticate
, context
);
690 session
->AddTransition(NSS_AUTHENTICATE
, (uint
)NMT_CONNECTION_LOST
, NSS_UNCONNECTED
);
691 session
->AddTransition(NSS_AUTHENTICATE
, (uint
)NMT_AUTHENTICATE
, NSS_PREGAME
, (void*)&OnAuthenticate
, context
);
693 session
->AddTransition(NSS_PREGAME
, (uint
)NMT_CONNECTION_LOST
, NSS_UNCONNECTED
, (void*)&OnDisconnect
, context
);
694 session
->AddTransition(NSS_PREGAME
, (uint
)NMT_CHAT
, NSS_PREGAME
, (void*)&OnChat
, context
);
695 session
->AddTransition(NSS_PREGAME
, (uint
)NMT_READY
, NSS_PREGAME
, (void*)&OnReady
, context
);
696 session
->AddTransition(NSS_PREGAME
, (uint
)NMT_CLEAR_ALL_READY
, NSS_PREGAME
, (void*)&OnClearAllReady
, context
);
697 session
->AddTransition(NSS_PREGAME
, (uint
)NMT_GAME_SETUP
, NSS_PREGAME
, (void*)&OnGameSetup
, context
);
698 session
->AddTransition(NSS_PREGAME
, (uint
)NMT_ASSIGN_PLAYER
, NSS_PREGAME
, (void*)&OnAssignPlayer
, context
);
699 session
->AddTransition(NSS_PREGAME
, (uint
)NMT_KICKED
, NSS_PREGAME
, (void*)&OnKickPlayer
, context
);
700 session
->AddTransition(NSS_PREGAME
, (uint
)NMT_GAME_START
, NSS_PREGAME
, (void*)&OnGameStart
, context
);
701 session
->AddTransition(NSS_PREGAME
, (uint
)NMT_LOADED_GAME
, NSS_INGAME
, (void*)&OnLoadedGame
, context
);
703 session
->AddTransition(NSS_JOIN_SYNCING
, (uint
)NMT_KICKED
, NSS_JOIN_SYNCING
, (void*)&OnKickPlayer
, context
);
704 session
->AddTransition(NSS_JOIN_SYNCING
, (uint
)NMT_CONNECTION_LOST
, NSS_UNCONNECTED
, (void*)&OnDisconnect
, context
);
705 session
->AddTransition(NSS_JOIN_SYNCING
, (uint
)NMT_LOADED_GAME
, NSS_INGAME
, (void*)&OnJoinSyncingLoadedGame
, context
);
707 session
->AddTransition(NSS_INGAME
, (uint
)NMT_REJOINED
, NSS_INGAME
, (void*)&OnRejoined
, context
);
708 session
->AddTransition(NSS_INGAME
, (uint
)NMT_KICKED
, NSS_INGAME
, (void*)&OnKickPlayer
, context
);
709 session
->AddTransition(NSS_INGAME
, (uint
)NMT_CLIENT_PAUSED
, NSS_INGAME
, (void*)&OnClientPaused
, context
);
710 session
->AddTransition(NSS_INGAME
, (uint
)NMT_CONNECTION_LOST
, NSS_UNCONNECTED
, (void*)&OnDisconnect
, context
);
711 session
->AddTransition(NSS_INGAME
, (uint
)NMT_CHAT
, NSS_INGAME
, (void*)&OnChat
, context
);
712 session
->AddTransition(NSS_INGAME
, (uint
)NMT_SIMULATION_COMMAND
, NSS_INGAME
, (void*)&OnSimulationCommand
, context
);
713 session
->AddTransition(NSS_INGAME
, (uint
)NMT_SYNC_CHECK
, NSS_INGAME
, (void*)&OnSyncCheck
, context
);
714 session
->AddTransition(NSS_INGAME
, (uint
)NMT_END_COMMAND_BATCH
, NSS_INGAME
, (void*)&OnEndCommandBatch
, context
);
717 session
->SetFirstState(NSS_HANDSHAKE
);
720 bool CNetServerWorker::HandleConnect(CNetServerSession
* session
)
722 if (std::find(m_BannedIPs
.begin(), m_BannedIPs
.end(), session
->GetIPAddress()) != m_BannedIPs
.end())
724 session
->Disconnect(NDR_BANNED
);
728 CSrvHandshakeMessage handshake
;
729 handshake
.m_Magic
= PS_PROTOCOL_MAGIC
;
730 handshake
.m_ProtocolVersion
= PS_PROTOCOL_VERSION
;
731 handshake
.m_SoftwareVersion
= PS_PROTOCOL_VERSION
;
732 return session
->SendMessage(&handshake
);
735 void CNetServerWorker::OnUserJoin(CNetServerSession
* session
)
737 AddPlayer(session
->GetGUID(), session
->GetUserName());
739 CPlayerAssignmentMessage assignMessage
;
740 ConstructPlayerAssignmentMessage(assignMessage
);
741 session
->SendMessage(&assignMessage
);
744 void CNetServerWorker::OnUserLeave(CNetServerSession
* session
)
746 std::vector
<CStr
>::iterator pausing
= std::find(m_PausingPlayers
.begin(), m_PausingPlayers
.end(), session
->GetGUID());
747 if (pausing
!= m_PausingPlayers
.end())
748 m_PausingPlayers
.erase(pausing
);
750 RemovePlayer(session
->GetGUID());
752 if (m_ServerTurnManager
&& session
->GetCurrState() != NSS_JOIN_SYNCING
)
753 m_ServerTurnManager
->UninitialiseClient(session
->GetHostID());
755 // TODO: ought to switch the player controlled by that client
756 // back to AI control, or something?
759 void CNetServerWorker::AddPlayer(const CStr
& guid
, const CStrW
& name
)
761 // Find all player IDs in active use; we mustn't give them to a second player (excluding the unassigned ID: -1)
762 std::set
<i32
> usedIDs
;
763 for (const std::pair
<const CStr
, PlayerAssignment
>& p
: m_PlayerAssignments
)
764 if (p
.second
.m_Enabled
&& p
.second
.m_PlayerID
!= -1)
765 usedIDs
.insert(p
.second
.m_PlayerID
);
767 // If the player is rejoining after disconnecting, try to give them
768 // back their old player ID. Don't do this in pregame however,
769 // as that ID might be invalid for various reasons.
773 if (m_State
!= SERVER_STATE_UNCONNECTED
&& m_State
!= SERVER_STATE_PREGAME
)
775 // Try to match GUID first
776 for (PlayerAssignmentMap::iterator it
= m_PlayerAssignments
.begin(); it
!= m_PlayerAssignments
.end(); ++it
)
778 if (!it
->second
.m_Enabled
&& it
->first
== guid
&& usedIDs
.find(it
->second
.m_PlayerID
) == usedIDs
.end())
780 playerID
= it
->second
.m_PlayerID
;
781 m_PlayerAssignments
.erase(it
); // delete the old mapping, since we've got a new one now
786 // Try to match username next
787 for (PlayerAssignmentMap::iterator it
= m_PlayerAssignments
.begin(); it
!= m_PlayerAssignments
.end(); ++it
)
789 if (!it
->second
.m_Enabled
&& it
->second
.m_Name
== name
&& usedIDs
.find(it
->second
.m_PlayerID
) == usedIDs
.end())
791 playerID
= it
->second
.m_PlayerID
;
792 m_PlayerAssignments
.erase(it
); // delete the old mapping, since we've got a new one now
799 PlayerAssignment assignment
;
800 assignment
.m_Enabled
= true;
801 assignment
.m_Name
= name
;
802 assignment
.m_PlayerID
= playerID
;
803 assignment
.m_Status
= 0;
804 m_PlayerAssignments
[guid
] = assignment
;
806 // Send the new assignments to all currently active players
807 // (which does not include the one that's just joining)
808 SendPlayerAssignments();
811 void CNetServerWorker::RemovePlayer(const CStr
& guid
)
813 m_PlayerAssignments
[guid
].m_Enabled
= false;
815 SendPlayerAssignments();
818 void CNetServerWorker::ClearAllPlayerReady()
820 for (std::pair
<const CStr
, PlayerAssignment
>& p
: m_PlayerAssignments
)
821 if (p
.second
.m_Status
!= 2)
822 p
.second
.m_Status
= 0;
824 SendPlayerAssignments();
827 void CNetServerWorker::KickPlayer(const CStrW
& playerName
, const bool ban
)
829 // Find the user with that name
830 std::vector
<CNetServerSession
*>::iterator it
= std::find_if(m_Sessions
.begin(), m_Sessions
.end(),
831 [&](CNetServerSession
* session
) { return session
->GetUserName() == playerName
; });
833 // and return if no one or the host has that name
834 if (it
== m_Sessions
.end() || (*it
)->GetGUID() == m_ControllerGUID
)
840 if (std::find(m_BannedPlayers
.begin(), m_BannedPlayers
.end(), playerName
) == m_BannedPlayers
.end())
841 m_BannedPlayers
.push_back(m_LobbyAuth
? CStrW(playerName
.substr(0, playerName
.find(L
" ("))) : playerName
);
843 // Remember IP address
844 u32 ipAddress
= (*it
)->GetIPAddress();
845 if (std::find(m_BannedIPs
.begin(), m_BannedIPs
.end(), ipAddress
) == m_BannedIPs
.end())
846 m_BannedIPs
.push_back(ipAddress
);
849 // Disconnect that user
850 (*it
)->Disconnect(ban
? NDR_BANNED
: NDR_KICKED
);
852 // Send message notifying other clients
853 CKickedMessage kickedMessage
;
854 kickedMessage
.m_Name
= playerName
;
855 kickedMessage
.m_Ban
= ban
;
856 Broadcast(&kickedMessage
, { NSS_PREGAME
, NSS_JOIN_SYNCING
, NSS_INGAME
});
859 void CNetServerWorker::AssignPlayer(int playerID
, const CStr
& guid
)
861 // Remove anyone who's already assigned to this player
862 for (std::pair
<const CStr
, PlayerAssignment
>& p
: m_PlayerAssignments
)
864 if (p
.second
.m_PlayerID
== playerID
)
865 p
.second
.m_PlayerID
= -1;
868 // Update this host's assignment if it exists
869 if (m_PlayerAssignments
.find(guid
) != m_PlayerAssignments
.end())
870 m_PlayerAssignments
[guid
].m_PlayerID
= playerID
;
872 SendPlayerAssignments();
875 void CNetServerWorker::ConstructPlayerAssignmentMessage(CPlayerAssignmentMessage
& message
)
877 for (const std::pair
<const CStr
, PlayerAssignment
>& p
: m_PlayerAssignments
)
879 if (!p
.second
.m_Enabled
)
882 CPlayerAssignmentMessage::S_m_Hosts h
;
884 h
.m_Name
= p
.second
.m_Name
;
885 h
.m_PlayerID
= p
.second
.m_PlayerID
;
886 h
.m_Status
= p
.second
.m_Status
;
887 message
.m_Hosts
.push_back(h
);
891 void CNetServerWorker::SendPlayerAssignments()
893 CPlayerAssignmentMessage message
;
894 ConstructPlayerAssignmentMessage(message
);
895 Broadcast(&message
, { NSS_PREGAME
, NSS_JOIN_SYNCING
, NSS_INGAME
});
898 const ScriptInterface
& CNetServerWorker::GetScriptInterface()
900 return *m_ScriptInterface
;
903 void CNetServerWorker::SetTurnLength(u32 msecs
)
905 if (m_ServerTurnManager
)
906 m_ServerTurnManager
->SetTurnLength(msecs
);
909 void CNetServerWorker::ProcessLobbyAuth(const CStr
& name
, const CStr
& token
)
911 LOGMESSAGE("Net Server: Received lobby auth message from %s with %s", name
, token
);
912 // Find the user with that guid
913 std::vector
<CNetServerSession
*>::iterator it
= std::find_if(m_Sessions
.begin(), m_Sessions
.end(),
914 [&](CNetServerSession
* session
)
915 { return session
->GetGUID() == token
; });
917 if (it
== m_Sessions
.end())
920 (*it
)->SetUserName(name
.FromUTF8());
921 // Send an empty message to request the authentication message from the client
922 // after its identity has been confirmed via the lobby
923 CAuthenticateMessage emptyMessage
;
924 (*it
)->SendMessage(&emptyMessage
);
927 bool CNetServerWorker::OnClientHandshake(void* context
, CFsmEvent
* event
)
929 ENSURE(event
->GetType() == (uint
)NMT_CLIENT_HANDSHAKE
);
931 CNetServerSession
* session
= (CNetServerSession
*)context
;
932 CNetServerWorker
& server
= session
->GetServer();
934 CCliHandshakeMessage
* message
= (CCliHandshakeMessage
*)event
->GetParamRef();
935 if (message
->m_ProtocolVersion
!= PS_PROTOCOL_VERSION
)
937 session
->Disconnect(NDR_INCORRECT_PROTOCOL_VERSION
);
941 CStr guid
= ps_generate_guid();
943 // Ensure unique GUID
945 server
.m_Sessions
.begin(), server
.m_Sessions
.end(),
946 [&guid
] (const CNetServerSession
* session
)
947 { return session
->GetGUID() == guid
; }) != server
.m_Sessions
.end())
951 session
->Disconnect(NDR_GUID_FAILED
);
954 guid
= ps_generate_guid();
957 session
->SetGUID(guid
);
959 CSrvHandshakeResponseMessage handshakeResponse
;
960 handshakeResponse
.m_UseProtocolVersion
= PS_PROTOCOL_VERSION
;
961 handshakeResponse
.m_GUID
= guid
;
962 handshakeResponse
.m_Flags
= 0;
964 if (server
.m_LobbyAuth
)
966 handshakeResponse
.m_Flags
|= PS_NETWORK_FLAG_REQUIRE_LOBBYAUTH
;
967 session
->SetNextState(NSS_LOBBY_AUTHENTICATE
);
970 session
->SendMessage(&handshakeResponse
);
975 bool CNetServerWorker::OnAuthenticate(void* context
, CFsmEvent
* event
)
977 ENSURE(event
->GetType() == (uint
)NMT_AUTHENTICATE
);
979 CNetServerSession
* session
= (CNetServerSession
*)context
;
980 CNetServerWorker
& server
= session
->GetServer();
982 // Prohibit joins while the game is loading
983 if (server
.m_State
== SERVER_STATE_LOADING
)
985 LOGMESSAGE("Refused connection while the game is loading");
986 session
->Disconnect(NDR_SERVER_LOADING
);
990 CAuthenticateMessage
* message
= (CAuthenticateMessage
*)event
->GetParamRef();
991 CStrW username
= SanitisePlayerName(message
->m_Name
);
992 CStrW
usernameWithoutRating(username
.substr(0, username
.find(L
" (")));
994 // Compare the lowercase names as specified by https://xmpp.org/extensions/xep-0029.html#sect-idm139493404168176
995 // "[...] comparisons will be made in case-normalized canonical form."
996 if (server
.m_LobbyAuth
&& usernameWithoutRating
.LowerCase() != session
->GetUserName().LowerCase())
998 LOGERROR("Net server: lobby auth: %s tried joining as %s",
999 session
->GetUserName().ToUTF8(),
1000 usernameWithoutRating
.ToUTF8());
1001 session
->Disconnect(NDR_LOBBY_AUTH_FAILED
);
1005 // Check the password before anything else.
1006 // NB: m_Name must match the client's salt, @see CNetClient::SetGamePassword
1007 if (!server
.CheckPassword(message
->m_Password
, message
->m_Name
.ToUTF8()))
1009 // Noisy logerror because players are not supposed to be able to get the IP,
1010 // so this might be someone targeting the host for some reason
1011 // (or TODO a dedicated server and we do want to log anyways)
1012 LOGERROR("Net server: user %s tried joining with the wrong password",
1013 session
->GetUserName().ToUTF8());
1014 session
->Disconnect(NDR_SERVER_REFUSED
);
1018 // Either deduplicate or prohibit join if name is in use
1019 bool duplicatePlayernames
= false;
1020 CFG_GET_VAL("network.duplicateplayernames", duplicatePlayernames
);
1021 // If lobby authentication is enabled, the clients playername has already been registered.
1022 // There also can't be any duplicated names.
1023 if (!server
.m_LobbyAuth
&& duplicatePlayernames
)
1024 username
= server
.DeduplicatePlayerName(username
);
1027 std::vector
<CNetServerSession
*>::iterator it
= std::find_if(
1028 server
.m_Sessions
.begin(), server
.m_Sessions
.end(),
1029 [&username
] (const CNetServerSession
* session
)
1030 { return session
->GetUserName() == username
; });
1032 if (it
!= server
.m_Sessions
.end() && (*it
) != session
)
1034 session
->Disconnect(NDR_PLAYERNAME_IN_USE
);
1039 // Disconnect banned usernames
1040 if (std::find(server
.m_BannedPlayers
.begin(), server
.m_BannedPlayers
.end(), server
.m_LobbyAuth
? usernameWithoutRating
: username
) != server
.m_BannedPlayers
.end())
1042 session
->Disconnect(NDR_BANNED
);
1046 int maxObservers
= 0;
1047 CFG_GET_VAL("network.observerlimit", maxObservers
);
1049 bool isRejoining
= false;
1050 bool serverFull
= false;
1051 if (server
.m_State
== SERVER_STATE_PREGAME
)
1053 // Don't check for maxObservers in the gamesetup, as we don't know yet who will be assigned
1054 serverFull
= server
.m_Sessions
.size() >= MAX_CLIENTS
;
1058 bool isObserver
= true;
1059 int disconnectedPlayers
= 0;
1060 int connectedPlayers
= 0;
1061 // (TODO: if GUIDs were stable, we should use them instead)
1062 for (const std::pair
<const CStr
, PlayerAssignment
>& p
: server
.m_PlayerAssignments
)
1064 const PlayerAssignment
& assignment
= p
.second
;
1066 if (!assignment
.m_Enabled
&& assignment
.m_Name
== username
)
1068 isObserver
= assignment
.m_PlayerID
== -1;
1072 if (assignment
.m_PlayerID
== -1)
1075 if (assignment
.m_Enabled
)
1078 ++disconnectedPlayers
;
1081 // Optionally allow everyone or only buddies to join after the game has started
1084 CStr observerLateJoin
;
1085 CFG_GET_VAL("network.lateobservers", observerLateJoin
);
1087 if (observerLateJoin
== "everyone")
1091 else if (observerLateJoin
== "buddies")
1094 CFG_GET_VAL("lobby.buddies", buddies
);
1095 std::wstringstream
buddiesStream(wstring_from_utf8(buddies
));
1097 while (std::getline(buddiesStream
, buddy
, L
','))
1099 if (buddy
== usernameWithoutRating
)
1110 LOGMESSAGE("Refused connection after game start from not-previously-known user \"%s\"", utf8_from_wstring(username
));
1111 session
->Disconnect(NDR_SERVER_ALREADY_IN_GAME
);
1115 // Ensure all players will be able to rejoin
1116 serverFull
= isObserver
&& (
1117 (int) server
.m_Sessions
.size() - connectedPlayers
> maxObservers
||
1118 (int) server
.m_Sessions
.size() + disconnectedPlayers
>= MAX_CLIENTS
);
1123 session
->Disconnect(NDR_SERVER_FULL
);
1127 u32 newHostID
= server
.m_NextHostID
++;
1129 session
->SetUserName(username
);
1130 session
->SetHostID(newHostID
);
1132 CAuthenticateResultMessage authenticateResult
;
1133 authenticateResult
.m_Code
= isRejoining
? ARC_OK_REJOINING
: ARC_OK
;
1134 authenticateResult
.m_HostID
= newHostID
;
1135 authenticateResult
.m_Message
= L
"Logged in";
1136 authenticateResult
.m_IsController
= 0;
1138 if (message
->m_ControllerSecret
== server
.m_ControllerSecret
)
1140 if (server
.m_ControllerGUID
.empty())
1142 server
.m_ControllerGUID
= session
->GetGUID();
1143 authenticateResult
.m_IsController
= 1;
1145 // TODO: we could probably handle having several controllers, or swapping?
1148 session
->SendMessage(&authenticateResult
);
1150 server
.OnUserJoin(session
);
1154 ENSURE(server
.m_State
!= SERVER_STATE_UNCONNECTED
&& server
.m_State
!= SERVER_STATE_PREGAME
);
1156 // Request a copy of the current game state from an existing player,
1157 // so we can send it on to the new player
1159 // Assume session 0 is most likely the local player, so they're
1160 // the most efficient client to request a copy from
1161 CNetServerSession
* sourceSession
= server
.m_Sessions
.at(0);
1163 sourceSession
->GetFileTransferer().StartTask(
1164 std::shared_ptr
<CNetFileReceiveTask
>(new CNetFileReceiveTask_ServerRejoin(server
, newHostID
))
1167 session
->SetNextState(NSS_JOIN_SYNCING
);
1172 bool CNetServerWorker::OnSimulationCommand(void* context
, CFsmEvent
* event
)
1174 ENSURE(event
->GetType() == (uint
)NMT_SIMULATION_COMMAND
);
1176 CNetServerSession
* session
= (CNetServerSession
*)context
;
1177 CNetServerWorker
& server
= session
->GetServer();
1179 CSimulationMessage
* message
= (CSimulationMessage
*)event
->GetParamRef();
1181 // Ignore messages sent by one player on behalf of another player
1182 // unless cheating is enabled
1183 bool cheatsEnabled
= false;
1184 const ScriptInterface
& scriptInterface
= server
.GetScriptInterface();
1185 ScriptRequest
rq(scriptInterface
);
1186 JS::RootedValue
settings(rq
.cx
);
1187 Script::GetProperty(rq
, server
.m_InitAttributes
, "settings", &settings
);
1188 if (Script::HasProperty(rq
, settings
, "CheatsEnabled"))
1189 Script::GetProperty(rq
, settings
, "CheatsEnabled", cheatsEnabled
);
1191 PlayerAssignmentMap::iterator it
= server
.m_PlayerAssignments
.find(session
->GetGUID());
1192 // When cheating is disabled, fail if the player the message claims to
1193 // represent does not exist or does not match the sender's player name
1194 if (!cheatsEnabled
&& (it
== server
.m_PlayerAssignments
.end() || it
->second
.m_PlayerID
!= message
->m_Player
))
1197 // Send it back to all clients that have finished
1198 // the loading screen (and the synchronization when rejoining)
1199 server
.Broadcast(message
, { NSS_INGAME
});
1201 // Save all the received commands
1202 if (server
.m_SavedCommands
.size() < message
->m_Turn
+ 1)
1203 server
.m_SavedCommands
.resize(message
->m_Turn
+ 1);
1204 server
.m_SavedCommands
[message
->m_Turn
].push_back(*message
);
1206 // TODO: we shouldn't send the message back to the client that first sent it
1210 bool CNetServerWorker::OnSyncCheck(void* context
, CFsmEvent
* event
)
1212 ENSURE(event
->GetType() == (uint
)NMT_SYNC_CHECK
);
1214 CNetServerSession
* session
= (CNetServerSession
*)context
;
1215 CNetServerWorker
& server
= session
->GetServer();
1217 CSyncCheckMessage
* message
= (CSyncCheckMessage
*)event
->GetParamRef();
1219 server
.m_ServerTurnManager
->NotifyFinishedClientUpdate(*session
, message
->m_Turn
, message
->m_Hash
);
1223 bool CNetServerWorker::OnEndCommandBatch(void* context
, CFsmEvent
* event
)
1225 ENSURE(event
->GetType() == (uint
)NMT_END_COMMAND_BATCH
);
1227 CNetServerSession
* session
= (CNetServerSession
*)context
;
1228 CNetServerWorker
& server
= session
->GetServer();
1230 CEndCommandBatchMessage
* message
= (CEndCommandBatchMessage
*)event
->GetParamRef();
1232 // The turn-length field is ignored
1233 server
.m_ServerTurnManager
->NotifyFinishedClientCommands(*session
, message
->m_Turn
);
1237 bool CNetServerWorker::OnChat(void* context
, CFsmEvent
* event
)
1239 ENSURE(event
->GetType() == (uint
)NMT_CHAT
);
1241 CNetServerSession
* session
= (CNetServerSession
*)context
;
1242 CNetServerWorker
& server
= session
->GetServer();
1244 CChatMessage
* message
= (CChatMessage
*)event
->GetParamRef();
1246 message
->m_GUID
= session
->GetGUID();
1248 server
.Broadcast(message
, { NSS_PREGAME
, NSS_INGAME
});
1253 bool CNetServerWorker::OnReady(void* context
, CFsmEvent
* event
)
1255 ENSURE(event
->GetType() == (uint
)NMT_READY
);
1257 CNetServerSession
* session
= (CNetServerSession
*)context
;
1258 CNetServerWorker
& server
= session
->GetServer();
1260 // Occurs if a client presses not-ready
1261 // in the very last moment before the hosts starts the game
1262 if (server
.m_State
== SERVER_STATE_LOADING
)
1265 CReadyMessage
* message
= (CReadyMessage
*)event
->GetParamRef();
1266 message
->m_GUID
= session
->GetGUID();
1267 server
.Broadcast(message
, { NSS_PREGAME
});
1269 server
.m_PlayerAssignments
[message
->m_GUID
].m_Status
= message
->m_Status
;
1274 bool CNetServerWorker::OnClearAllReady(void* context
, CFsmEvent
* event
)
1276 ENSURE(event
->GetType() == (uint
)NMT_CLEAR_ALL_READY
);
1278 CNetServerSession
* session
= (CNetServerSession
*)context
;
1279 CNetServerWorker
& server
= session
->GetServer();
1281 if (session
->GetGUID() == server
.m_ControllerGUID
)
1282 server
.ClearAllPlayerReady();
1287 bool CNetServerWorker::OnGameSetup(void* context
, CFsmEvent
* event
)
1289 ENSURE(event
->GetType() == (uint
)NMT_GAME_SETUP
);
1291 CNetServerSession
* session
= (CNetServerSession
*)context
;
1292 CNetServerWorker
& server
= session
->GetServer();
1294 // Changing the settings after gamestart is not implemented and would cause an Out-of-sync error.
1295 // This happened when doubleclicking on the startgame button.
1296 if (server
.m_State
!= SERVER_STATE_PREGAME
)
1299 // Only the controller is allowed to send game setup updates.
1300 // TODO: it would be good to allow other players to request changes to some settings,
1301 // e.g. their civilisation.
1302 // Possibly this should use another message, to enforce a single source of truth.
1303 if (session
->GetGUID() == server
.m_ControllerGUID
)
1305 CGameSetupMessage
* message
= (CGameSetupMessage
*)event
->GetParamRef();
1306 server
.Broadcast(message
, { NSS_PREGAME
});
1311 bool CNetServerWorker::OnAssignPlayer(void* context
, CFsmEvent
* event
)
1313 ENSURE(event
->GetType() == (uint
)NMT_ASSIGN_PLAYER
);
1314 CNetServerSession
* session
= (CNetServerSession
*)context
;
1315 CNetServerWorker
& server
= session
->GetServer();
1317 if (session
->GetGUID() == server
.m_ControllerGUID
)
1319 CAssignPlayerMessage
* message
= (CAssignPlayerMessage
*)event
->GetParamRef();
1320 server
.AssignPlayer(message
->m_PlayerID
, message
->m_GUID
);
1325 bool CNetServerWorker::OnGameStart(void* context
, CFsmEvent
* event
)
1327 ENSURE(event
->GetType() == (uint
)NMT_GAME_START
);
1328 CNetServerSession
* session
= (CNetServerSession
*)context
;
1329 CNetServerWorker
& server
= session
->GetServer();
1331 if (session
->GetGUID() != server
.m_ControllerGUID
)
1334 CGameStartMessage
* message
= (CGameStartMessage
*)event
->GetParamRef();
1335 server
.StartGame(message
->m_InitAttributes
);
1339 bool CNetServerWorker::OnLoadedGame(void* context
, CFsmEvent
* event
)
1341 ENSURE(event
->GetType() == (uint
)NMT_LOADED_GAME
);
1343 CNetServerSession
* loadedSession
= (CNetServerSession
*)context
;
1344 CNetServerWorker
& server
= loadedSession
->GetServer();
1346 // We're in the loading state, so wait until every client has loaded
1347 // before starting the game
1348 ENSURE(server
.m_State
== SERVER_STATE_LOADING
);
1349 if (server
.CheckGameLoadStatus(loadedSession
))
1352 CClientsLoadingMessage message
;
1353 // We always send all GUIDs of clients in the loading state
1354 // so that we don't have to bother about switching GUI pages
1355 for (CNetServerSession
* session
: server
.m_Sessions
)
1356 if (session
->GetCurrState() != NSS_INGAME
&& loadedSession
->GetGUID() != session
->GetGUID())
1358 CClientsLoadingMessage::S_m_Clients client
;
1359 client
.m_GUID
= session
->GetGUID();
1360 message
.m_Clients
.push_back(client
);
1363 // Send to the client who has loaded the game but did not reach the NSS_INGAME state yet
1364 loadedSession
->SendMessage(&message
);
1365 server
.Broadcast(&message
, { NSS_INGAME
});
1370 bool CNetServerWorker::OnJoinSyncingLoadedGame(void* context
, CFsmEvent
* event
)
1372 // A client rejoining an in-progress game has now finished loading the
1373 // map and deserialized the initial state.
1374 // The simulation may have progressed since then, so send any subsequent
1375 // commands to them and set them as an active player so they can participate
1376 // in all future turns.
1378 // (TODO: if it takes a long time for them to receive and execute all these
1379 // commands, the other players will get frozen for that time and may be unhappy;
1380 // we could try repeating this process a few times until the client converges
1381 // on the up-to-date state, before setting them as active.)
1383 ENSURE(event
->GetType() == (uint
)NMT_LOADED_GAME
);
1385 CNetServerSession
* session
= (CNetServerSession
*)context
;
1386 CNetServerWorker
& server
= session
->GetServer();
1388 CLoadedGameMessage
* message
= (CLoadedGameMessage
*)event
->GetParamRef();
1390 u32 turn
= message
->m_CurrentTurn
;
1391 u32 readyTurn
= server
.m_ServerTurnManager
->GetReadyTurn();
1393 // Send them all commands received since their saved state,
1394 // and turn-ended messages for any turns that have already been processed
1395 for (size_t i
= turn
+ 1; i
< std::max(readyTurn
+1, (u32
)server
.m_SavedCommands
.size()); ++i
)
1397 if (i
< server
.m_SavedCommands
.size())
1398 for (size_t j
= 0; j
< server
.m_SavedCommands
[i
].size(); ++j
)
1399 session
->SendMessage(&server
.m_SavedCommands
[i
][j
]);
1403 CEndCommandBatchMessage endMessage
;
1404 endMessage
.m_Turn
= i
;
1405 endMessage
.m_TurnLength
= server
.m_ServerTurnManager
->GetSavedTurnLength(i
);
1406 session
->SendMessage(&endMessage
);
1410 // Tell the turn manager to expect commands from this new client
1411 // Special case: the controller shouldn't be treated as an observer in any case.
1412 bool isObserver
= server
.m_PlayerAssignments
[session
->GetGUID()].m_PlayerID
== -1 && server
.m_ControllerGUID
!= session
->GetGUID();
1413 server
.m_ServerTurnManager
->InitialiseClient(session
->GetHostID(), readyTurn
, isObserver
);
1415 // Tell the client that everything has finished loading and it should start now
1416 CLoadedGameMessage loaded
;
1417 loaded
.m_CurrentTurn
= readyTurn
;
1418 session
->SendMessage(&loaded
);
1423 bool CNetServerWorker::OnRejoined(void* context
, CFsmEvent
* event
)
1425 // A client has finished rejoining and the loading screen disappeared.
1426 ENSURE(event
->GetType() == (uint
)NMT_REJOINED
);
1428 CNetServerSession
* session
= (CNetServerSession
*)context
;
1429 CNetServerWorker
& server
= session
->GetServer();
1431 // Inform everyone of the client having rejoined
1432 CRejoinedMessage
* message
= (CRejoinedMessage
*)event
->GetParamRef();
1433 message
->m_GUID
= session
->GetGUID();
1434 server
.Broadcast(message
, { NSS_INGAME
});
1436 // Send all pausing players to the rejoined client.
1437 for (const CStr
& guid
: server
.m_PausingPlayers
)
1439 CClientPausedMessage pausedMessage
;
1440 pausedMessage
.m_GUID
= guid
;
1441 pausedMessage
.m_Pause
= true;
1442 session
->SendMessage(&pausedMessage
);
1448 bool CNetServerWorker::OnKickPlayer(void* context
, CFsmEvent
* event
)
1450 ENSURE(event
->GetType() == (uint
)NMT_KICKED
);
1452 CNetServerSession
* session
= (CNetServerSession
*)context
;
1453 CNetServerWorker
& server
= session
->GetServer();
1455 if (session
->GetGUID() == server
.m_ControllerGUID
)
1457 CKickedMessage
* message
= (CKickedMessage
*)event
->GetParamRef();
1458 server
.KickPlayer(message
->m_Name
, message
->m_Ban
);
1463 bool CNetServerWorker::OnDisconnect(void* context
, CFsmEvent
* event
)
1465 ENSURE(event
->GetType() == (uint
)NMT_CONNECTION_LOST
);
1467 CNetServerSession
* session
= (CNetServerSession
*)context
;
1468 CNetServerWorker
& server
= session
->GetServer();
1470 server
.OnUserLeave(session
);
1475 bool CNetServerWorker::OnClientPaused(void* context
, CFsmEvent
* event
)
1477 ENSURE(event
->GetType() == (uint
)NMT_CLIENT_PAUSED
);
1479 CNetServerSession
* session
= (CNetServerSession
*)context
;
1480 CNetServerWorker
& server
= session
->GetServer();
1482 CClientPausedMessage
* message
= (CClientPausedMessage
*)event
->GetParamRef();
1484 message
->m_GUID
= session
->GetGUID();
1486 // Update the list of pausing players.
1487 std::vector
<CStr
>::iterator player
= std::find(server
.m_PausingPlayers
.begin(), server
.m_PausingPlayers
.end(), session
->GetGUID());
1489 if (message
->m_Pause
)
1491 if (player
!= server
.m_PausingPlayers
.end())
1494 server
.m_PausingPlayers
.push_back(session
->GetGUID());
1498 if (player
== server
.m_PausingPlayers
.end())
1501 server
.m_PausingPlayers
.erase(player
);
1504 // Send messages to clients that are in game, and are not the client who paused.
1505 for (CNetServerSession
* netSession
: server
.m_Sessions
)
1506 if (netSession
->GetCurrState() == NSS_INGAME
&& message
->m_GUID
!= netSession
->GetGUID())
1507 netSession
->SendMessage(message
);
1512 bool CNetServerWorker::CheckGameLoadStatus(CNetServerSession
* changedSession
)
1514 for (const CNetServerSession
* session
: m_Sessions
)
1515 if (session
!= changedSession
&& session
->GetCurrState() != NSS_INGAME
)
1518 // Inform clients that everyone has loaded the map and that the game can start
1519 CLoadedGameMessage loaded
;
1520 loaded
.m_CurrentTurn
= 0;
1522 // Notice the changedSession is still in the NSS_PREGAME state
1523 Broadcast(&loaded
, { NSS_PREGAME
, NSS_INGAME
});
1525 m_State
= SERVER_STATE_INGAME
;
1529 void CNetServerWorker::StartGame(const CStr
& initAttribs
)
1531 for (std::pair
<const CStr
, PlayerAssignment
>& player
: m_PlayerAssignments
)
1532 if (player
.second
.m_Enabled
&& player
.second
.m_PlayerID
!= -1 && player
.second
.m_Status
== 0)
1534 LOGERROR("Tried to start the game without player \"%s\" being ready!", utf8_from_wstring(player
.second
.m_Name
).c_str());
1538 m_ServerTurnManager
= new CNetServerTurnManager(*this);
1540 for (CNetServerSession
* session
: m_Sessions
)
1542 // Special case: the controller shouldn't be treated as an observer in any case.
1543 bool isObserver
= m_PlayerAssignments
[session
->GetGUID()].m_PlayerID
== -1 && m_ControllerGUID
!= session
->GetGUID();
1544 m_ServerTurnManager
->InitialiseClient(session
->GetHostID(), 0, isObserver
);
1547 m_State
= SERVER_STATE_LOADING
;
1549 // Remove players and observers that are not present when the game starts
1550 for (PlayerAssignmentMap::iterator it
= m_PlayerAssignments
.begin(); it
!= m_PlayerAssignments
.end();)
1551 if (it
->second
.m_Enabled
)
1554 it
= m_PlayerAssignments
.erase(it
);
1556 SendPlayerAssignments();
1558 // Update init attributes. They should no longer change.
1559 Script::ParseJSON(ScriptRequest(m_ScriptInterface
), initAttribs
, &m_InitAttributes
);
1561 CGameStartMessage gameStart
;
1562 gameStart
.m_InitAttributes
= initAttribs
;
1563 Broadcast(&gameStart
, { NSS_PREGAME
});
1566 CStrW
CNetServerWorker::SanitisePlayerName(const CStrW
& original
)
1568 const size_t MAX_LENGTH
= 32;
1570 CStrW name
= original
;
1571 name
.Replace(L
"[", L
"{"); // remove GUI tags
1572 name
.Replace(L
"]", L
"}"); // remove for symmetry
1574 // Restrict the length
1575 if (name
.length() > MAX_LENGTH
)
1576 name
= name
.Left(MAX_LENGTH
);
1578 // Don't allow surrounding whitespace
1579 name
.Trim(PS_TRIM_BOTH
);
1581 // Don't allow empty name
1583 name
= L
"Anonymous";
1588 CStrW
CNetServerWorker::DeduplicatePlayerName(const CStrW
& original
)
1590 CStrW name
= original
;
1592 // Try names "Foo", "Foo (2)", "Foo (3)", etc
1597 for (const CNetServerSession
* session
: m_Sessions
)
1599 if (session
->GetUserName() == name
)
1609 name
= original
+ L
" (" + CStrW::FromUInt(id
++) + L
")";
1613 void CNetServerWorker::SendHolePunchingMessage(const CStr
& ipStr
, u16 port
)
1616 StunClient::SendHolePunchingMessages(*m_Host
, ipStr
, port
);
1622 CNetServer::CNetServer(bool useLobbyAuth
) :
1623 m_Worker(new CNetServerWorker(useLobbyAuth
)),
1624 m_LobbyAuth(useLobbyAuth
), m_UseSTUN(false), m_PublicIp(""), m_PublicPort(20595), m_Password()
1628 CNetServer::~CNetServer()
1633 bool CNetServer::GetUseSTUN() const
1638 bool CNetServer::UseLobbyAuth() const
1643 bool CNetServer::SetupConnection(const u16 port
)
1645 return m_Worker
->SetupConnection(port
);
1648 CStr
CNetServer::GetPublicIp() const
1653 u16
CNetServer::GetPublicPort() const
1655 return m_PublicPort
;
1658 u16
CNetServer::GetLocalPort() const
1660 std::lock_guard
<std::mutex
> lock(m_Worker
->m_WorkerMutex
);
1661 if (!m_Worker
->m_Host
)
1663 return m_Worker
->m_Host
->address
.port
;
1666 void CNetServer::SetConnectionData(const CStr
& ip
, const u16 port
)
1669 m_PublicPort
= port
;
1673 bool CNetServer::SetConnectionDataViaSTUN()
1676 std::lock_guard
<std::mutex
> lock(m_Worker
->m_WorkerMutex
);
1677 if (!m_Worker
->m_Host
)
1679 return StunClient::FindPublicIP(*m_Worker
->m_Host
, m_PublicIp
, m_PublicPort
);
1682 bool CNetServer::CheckPasswordAndIncrement(const std::string
& username
, const std::string
& password
, const std::string
& salt
)
1684 std::unordered_map
<std::string
, int>::iterator it
= m_FailedAttempts
.find(username
);
1685 if (m_Worker
->CheckPassword(password
, salt
))
1687 if (it
!= m_FailedAttempts
.end())
1691 if (it
== m_FailedAttempts
.end())
1692 m_FailedAttempts
.emplace(username
, 1);
1698 bool CNetServer::IsBanned(const std::string
& username
) const
1700 std::unordered_map
<std::string
, int>::const_iterator it
= m_FailedAttempts
.find(username
);
1701 return it
!= m_FailedAttempts
.end() && it
->second
>= FAILED_PASSWORD_TRIES_BEFORE_BAN
;
1704 void CNetServer::SetPassword(const CStr
& password
)
1706 m_Password
= password
;
1707 std::lock_guard
<std::mutex
> lock(m_Worker
->m_WorkerMutex
);
1708 m_Worker
->SetPassword(password
);
1711 void CNetServer::SetControllerSecret(const std::string
& secret
)
1713 std::lock_guard
<std::mutex
> lock(m_Worker
->m_WorkerMutex
);
1714 m_Worker
->SetControllerSecret(secret
);
1717 void CNetServer::StartGame()
1719 std::lock_guard
<std::mutex
> lock(m_Worker
->m_WorkerMutex
);
1720 m_Worker
->m_StartGameQueue
.push_back(true);
1723 void CNetServer::UpdateInitAttributes(JS::MutableHandleValue attrs
, const ScriptRequest
& rq
)
1725 // Pass the attributes as JSON, since that's the easiest safe
1726 // cross-thread way of passing script data
1727 std::string attrsJSON
= Script::StringifyJSON(rq
, attrs
, false);
1729 std::lock_guard
<std::mutex
> lock(m_Worker
->m_WorkerMutex
);
1730 m_Worker
->m_InitAttributesQueue
.push_back(attrsJSON
);
1733 void CNetServer::OnLobbyAuth(const CStr
& name
, const CStr
& token
)
1735 std::lock_guard
<std::mutex
> lock(m_Worker
->m_WorkerMutex
);
1736 m_Worker
->m_LobbyAuthQueue
.push_back(std::make_pair(name
, token
));
1739 void CNetServer::SetTurnLength(u32 msecs
)
1741 std::lock_guard
<std::mutex
> lock(m_Worker
->m_WorkerMutex
);
1742 m_Worker
->m_TurnLengthQueue
.push_back(msecs
);
1745 void CNetServer::SendHolePunchingMessage(const CStr
& ip
, u16 port
)
1747 m_Worker
->SendHolePunchingMessage(ip
, port
);