1 /* Copyright (C) 2023 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>
54 * Number of peers to allocate for the enet host.
55 * Limited by ENET_PROTOCOL_MAXIMUM_PEER_ID (4096).
57 * At most 8 players, 32 observers and 1 temporary connection to send the "server full" disconnect-reason.
59 #define MAX_CLIENTS 41
61 #define DEFAULT_SERVER_NAME L"Unnamed Server"
63 constexpr int CHANNEL_COUNT
= 1;
64 constexpr int FAILED_PASSWORD_TRIES_BEFORE_BAN
= 3;
67 * enet_host_service timeout (msecs).
68 * Smaller numbers may hurt performance; larger numbers will
69 * hurt latency responding to messages from game thread.
71 static const int HOST_SERVICE_TIMEOUT
= 50;
74 * Once ping goes above turn length * command delay,
75 * the game will start 'freezing' for other clients while we catch up.
76 * Since commands are sent client -> server -> client, divide by 2.
77 * (duplicated in NetServer.cpp to avoid having to fetch the constants in a header file)
79 constexpr u32 NETWORK_BAD_PING
= DEFAULT_TURN_LENGTH
* COMMAND_DELAY_MP
/ 2;
81 CNetServer
* g_NetServer
= NULL
;
83 static CStr
DebugName(CNetServerSession
* session
)
86 return "[unknown host]";
87 if (session
->GetGUID().empty())
88 return "[unauthed host]";
89 return "[" + session
->GetGUID().substr(0, 8) + "...]";
93 * Async task for receiving the initial game state to be forwarded to another
94 * client that is rejoining an in-progress network game.
96 class CNetFileReceiveTask_ServerRejoin
: public CNetFileReceiveTask
98 NONCOPYABLE(CNetFileReceiveTask_ServerRejoin
);
100 CNetFileReceiveTask_ServerRejoin(CNetServerWorker
& server
, u32 hostID
)
101 : m_Server(server
), m_RejoinerHostID(hostID
)
105 virtual void OnComplete()
107 // We've received the game state from an existing player - now
108 // we need to send it onwards to the newly rejoining player
110 // Find the session corresponding to the rejoining host (if any)
111 CNetServerSession
* session
= NULL
;
112 for (CNetServerSession
* serverSession
: m_Server
.m_Sessions
)
114 if (serverSession
->GetHostID() == m_RejoinerHostID
)
116 session
= serverSession
;
123 LOGMESSAGE("Net server: rejoining client disconnected before we sent to it");
127 // Store the received state file, and tell the client to start downloading it from us
128 // TODO: this will get kind of confused if there's multiple clients downloading in parallel;
129 // they'll race and get whichever happens to be the latest received by the server,
130 // which should still work but isn't great
131 m_Server
.m_JoinSyncFile
= m_Buffer
;
133 // Send the init attributes alongside - these should be correct since the game should be started.
134 CJoinSyncStartMessage message
;
135 message
.m_InitAttributes
= Script::StringifyJSON(ScriptRequest(m_Server
.GetScriptInterface()), &m_Server
.m_InitAttributes
);
136 session
->SendMessage(&message
);
140 CNetServerWorker
& m_Server
;
141 u32 m_RejoinerHostID
;
145 * XXX: We use some non-threadsafe functions from the worker thread.
146 * See http://trac.wildfiregames.com/ticket/654
149 CNetServerWorker::CNetServerWorker(bool useLobbyAuth
) :
150 m_LobbyAuth(useLobbyAuth
),
152 m_ScriptInterface(NULL
),
153 m_NextHostID(1), m_Host(NULL
), m_ControllerGUID(), m_Stats(NULL
),
154 m_LastConnectionCheck(0)
156 m_State
= SERVER_STATE_UNCONNECTED
;
158 m_ServerTurnManager
= NULL
;
160 m_ServerName
= DEFAULT_SERVER_NAME
;
163 CNetServerWorker::~CNetServerWorker()
165 if (m_State
!= SERVER_STATE_UNCONNECTED
)
167 // Tell the thread to shut down
169 std::lock_guard
<std::mutex
> lock(m_WorkerMutex
);
173 // Wait for it to shut down cleanly
174 m_WorkerThread
.join();
177 #if CONFIG2_MINIUPNPC
178 if (m_UPnPThread
.joinable())
179 m_UPnPThread
.detach();
182 // Clean up resources
186 for (CNetServerSession
* session
: m_Sessions
)
188 session
->DisconnectNow(NDR_SERVER_SHUTDOWN
);
193 enet_host_destroy(m_Host
);
195 delete m_ServerTurnManager
;
198 void CNetServerWorker::SetPassword(const CStr
& hashedPassword
)
200 m_Password
= hashedPassword
;
204 void CNetServerWorker::SetControllerSecret(const std::string
& secret
)
206 m_ControllerSecret
= secret
;
210 bool CNetServerWorker::CheckPassword(const std::string
& password
, const std::string
& salt
) const
212 return HashCryptographically(m_Password
, salt
) == password
;
216 bool CNetServerWorker::SetupConnection(const u16 port
)
218 ENSURE(m_State
== SERVER_STATE_UNCONNECTED
);
221 // Bind to default host
223 addr
.host
= ENET_HOST_ANY
;
226 // Create ENet server
227 m_Host
= PS::Enet::CreateHost(&addr
, MAX_CLIENTS
, CHANNEL_COUNT
);
230 LOGERROR("Net server: enet_host_create failed");
234 m_Stats
= new CNetStatsTable();
235 if (CProfileViewer::IsInitialised())
236 g_ProfileViewer
.AddRootTable(m_Stats
);
238 m_State
= SERVER_STATE_PREGAME
;
240 // Launch the worker thread
241 m_WorkerThread
= std::thread(Threading::HandleExceptions
<RunThread
>::Wrapper
, this);
243 #if CONFIG2_MINIUPNPC
244 // Launch the UPnP thread
245 m_UPnPThread
= std::thread(Threading::HandleExceptions
<SetupUPnP
>::Wrapper
);
251 #if CONFIG2_MINIUPNPC
252 void CNetServerWorker::SetupUPnP()
254 debug_SetThreadName("UPnP");
256 // Values we want to set.
258 sprintf_s(psPort
, ARRAY_SIZE(psPort
), "%d", PS_DEFAULT_PORT
);
259 const char* leaseDuration
= "0"; // Indefinite/permanent lease duration.
260 const char* description
= "0AD Multiplayer";
261 const char* protocall
= "UDP";
262 char internalIPAddress
[64];
263 char externalIPAddress
[40];
265 // Variables to hold the values that actually get set.
270 // Intermediate variables.
271 bool allocatedUrls
= false;
272 struct UPNPUrls urls
;
273 struct IGDdatas data
;
274 struct UPNPDev
* devlist
= NULL
;
276 // Make sure everything is properly freed.
277 std::function
<void()> freeUPnP
= [&allocatedUrls
, &urls
, &devlist
]()
281 freeUPNPDevlist(devlist
);
282 // IGDdatas does not need to be freed according to UPNP_GetIGDFromUrl
285 // Cached root descriptor URL.
286 std::string rootDescURL
;
287 CFG_GET_VAL("network.upnprootdescurl", rootDescURL
);
288 if (!rootDescURL
.empty())
289 LOGMESSAGE("Net server: attempting to use cached root descriptor URL: %s", rootDescURL
.c_str());
293 // Try a cached URL first
294 if (!rootDescURL
.empty() && UPNP_GetIGDFromUrl(rootDescURL
.c_str(), &urls
, &data
, internalIPAddress
, sizeof(internalIPAddress
)))
296 LOGMESSAGE("Net server: using cached IGD = %s", urls
.controlURL
);
299 // No cached URL, or it did not respond. Try getting a valid UPnP device for 10 seconds.
300 #if defined(MINIUPNPC_API_VERSION) && MINIUPNPC_API_VERSION >= 14
301 else if ((devlist
= upnpDiscover(10000, 0, 0, 0, 0, 2, 0)) != NULL
)
303 else if ((devlist
= upnpDiscover(10000, 0, 0, 0, 0, 0)) != NULL
)
306 ret
= UPNP_GetValidIGD(devlist
, &urls
, &data
, internalIPAddress
, sizeof(internalIPAddress
));
307 allocatedUrls
= ret
!= 0; // urls is allocated on non-zero return values
311 LOGMESSAGE("Net server: upnpDiscover failed and no working cached URL.");
319 LOGMESSAGE("Net server: No IGD found");
322 LOGMESSAGE("Net server: found valid IGD = %s", urls
.controlURL
);
325 LOGMESSAGE("Net server: found a valid, not connected IGD = %s, will try to continue anyway", urls
.controlURL
);
328 LOGMESSAGE("Net server: found a UPnP device unrecognized as IGD = %s, will try to continue anyway", urls
.controlURL
);
331 debug_warn(L
"Unrecognized return value from UPNP_GetValidIGD");
334 // Try getting our external/internet facing IP. TODO: Display this on the game-setup page for conviniance.
335 ret
= UPNP_GetExternalIPAddress(urls
.controlURL
, data
.first
.servicetype
, externalIPAddress
);
336 if (ret
!= UPNPCOMMAND_SUCCESS
)
338 LOGMESSAGE("Net server: GetExternalIPAddress failed with code %d (%s)", ret
, strupnperror(ret
));
342 LOGMESSAGE("Net server: ExternalIPAddress = %s", externalIPAddress
);
344 // Try to setup port forwarding.
345 ret
= UPNP_AddPortMapping(urls
.controlURL
, data
.first
.servicetype
, psPort
, psPort
,
346 internalIPAddress
, description
, protocall
, 0, leaseDuration
);
347 if (ret
!= UPNPCOMMAND_SUCCESS
)
349 LOGMESSAGE("Net server: AddPortMapping(%s, %s, %s) failed with code %d (%s)",
350 psPort
, psPort
, internalIPAddress
, ret
, strupnperror(ret
));
355 // Check that the port was actually forwarded.
356 ret
= UPNP_GetSpecificPortMappingEntry(urls
.controlURL
,
357 data
.first
.servicetype
,
359 #if defined(MINIUPNPC_API_VERSION) && MINIUPNPC_API_VERSION >= 10
362 intClient
, intPort
, NULL
/*desc*/,
363 NULL
/*enabled*/, duration
);
365 if (ret
!= UPNPCOMMAND_SUCCESS
)
367 LOGMESSAGE("Net server: GetSpecificPortMappingEntry() failed with code %d (%s)", ret
, strupnperror(ret
));
372 LOGMESSAGE("Net server: External %s:%s %s is redirected to internal %s:%s (duration=%s)",
373 externalIPAddress
, psPort
, protocall
, intClient
, intPort
, duration
);
375 // Cache root descriptor URL to try to avoid discovery next time.
376 g_ConfigDB
.SetValueString(CFG_USER
, "network.upnprootdescurl", urls
.controlURL
);
377 g_ConfigDB
.WriteValueToFile(CFG_USER
, "network.upnprootdescurl", urls
.controlURL
);
378 LOGMESSAGE("Net server: cached UPnP root descriptor URL as %s", urls
.controlURL
);
382 #endif // CONFIG2_MINIUPNPC
384 bool CNetServerWorker::SendMessage(ENetPeer
* peer
, const CNetMessage
* message
)
388 CNetServerSession
* session
= static_cast<CNetServerSession
*>(peer
->data
);
390 return CNetHost::SendMessage(message
, peer
, DebugName(session
).c_str());
393 bool CNetServerWorker::Broadcast(const CNetMessage
* message
, const std::vector
<NetServerSessionState
>& targetStates
)
399 // TODO: this does lots of repeated message serialisation if we have lots
400 // of remote peers; could do it more efficiently if that's a real problem
402 for (CNetServerSession
* session
: m_Sessions
)
403 if (std::find(targetStates
.begin(), targetStates
.end(), static_cast<NetServerSessionState
>(session
->GetCurrState())) != targetStates
.end() &&
404 !session
->SendMessage(message
))
410 void CNetServerWorker::RunThread(CNetServerWorker
* data
)
412 debug_SetThreadName("NetServer");
417 void CNetServerWorker::Run()
419 // The script context uses the profiler and therefore the thread must be registered before the context is created
420 g_Profiler2
.RegisterCurrentThread("Net server");
422 // We create a new ScriptContext for this network thread, with a single ScriptInterface.
423 std::shared_ptr
<ScriptContext
> netServerContext
= ScriptContext::CreateContext();
424 m_ScriptInterface
= new ScriptInterface("Engine", "Net server", netServerContext
);
425 m_InitAttributes
.init(m_ScriptInterface
->GetGeneralJSContext(), JS::UndefinedValue());
432 // Update profiler stats
433 m_Stats
->LatchHostState(m_Host
);
436 // Clear roots before deleting their context
437 m_SavedCommands
.clear();
439 SAFE_DELETE(m_ScriptInterface
);
442 bool CNetServerWorker::RunStep()
444 // Check for messages from the game thread.
445 // (Do as little work as possible while the mutex is held open,
446 // to avoid performance problems and deadlocks.)
448 m_ScriptInterface
->GetContext().MaybeIncrementalGC(0.5f
);
450 ScriptRequest
rq(m_ScriptInterface
);
452 std::vector
<bool> newStartGame
;
453 std::vector
<std::string
> newGameAttributes
;
454 std::vector
<std::pair
<CStr
, CStr
>> newLobbyAuths
;
455 std::vector
<u32
> newTurnLength
;
458 std::lock_guard
<std::mutex
> lock(m_WorkerMutex
);
463 newStartGame
.swap(m_StartGameQueue
);
464 newGameAttributes
.swap(m_InitAttributesQueue
);
465 newLobbyAuths
.swap(m_LobbyAuthQueue
);
466 newTurnLength
.swap(m_TurnLengthQueue
);
469 if (!newGameAttributes
.empty())
471 if (m_State
!= SERVER_STATE_UNCONNECTED
&& m_State
!= SERVER_STATE_PREGAME
)
472 LOGERROR("NetServer: Init Attributes cannot be changed after the server starts loading.");
475 JS::RootedValue
gameAttributesVal(rq
.cx
);
476 Script::ParseJSON(rq
, newGameAttributes
.back(), &gameAttributesVal
);
477 m_InitAttributes
= gameAttributesVal
;
481 if (!newTurnLength
.empty())
482 SetTurnLength(newTurnLength
.back());
484 while (!newLobbyAuths
.empty())
486 const std::pair
<CStr
, CStr
>& auth
= newLobbyAuths
.back();
487 ProcessLobbyAuth(auth
.first
, auth
.second
);
488 newLobbyAuths
.pop_back();
491 // Perform file transfers
492 for (CNetServerSession
* session
: m_Sessions
)
493 session
->GetFileTransferer().Poll();
495 CheckClientConnections();
497 // Process network events:
500 int status
= enet_host_service(m_Host
, &event
, HOST_SERVICE_TIMEOUT
);
503 LOGERROR("CNetServerWorker: enet_host_service failed (%d)", status
);
504 // TODO: notify game that the server has shut down
510 // Reached timeout with no events - try again
514 // Process the event:
518 case ENET_EVENT_TYPE_CONNECT
:
520 // Report the client address
521 char hostname
[256] = "(error)";
522 enet_address_get_host_ip(&event
.peer
->address
, hostname
, ARRAY_SIZE(hostname
));
523 LOGMESSAGE("Net server: Received connection from %s:%u", hostname
, (unsigned int)event
.peer
->address
.port
);
525 // Set up a session object for this peer
527 CNetServerSession
* session
= new CNetServerSession(*this, event
.peer
);
529 m_Sessions
.push_back(session
);
531 SetupSession(session
);
533 ENSURE(event
.peer
->data
== NULL
);
534 event
.peer
->data
= session
;
536 HandleConnect(session
);
541 case ENET_EVENT_TYPE_DISCONNECT
:
543 // If there is an active session with this peer, then reset and delete it
545 CNetServerSession
* session
= static_cast<CNetServerSession
*>(event
.peer
->data
);
548 LOGMESSAGE("Net server: Disconnected %s", DebugName(session
).c_str());
550 // Remove the session first, so we won't send player-update messages to it
551 // when updating the FSM
552 m_Sessions
.erase(remove(m_Sessions
.begin(), m_Sessions
.end(), session
), m_Sessions
.end());
554 session
->Update((uint
)NMT_CONNECTION_LOST
, NULL
);
557 event
.peer
->data
= NULL
;
560 if (m_State
== SERVER_STATE_LOADING
)
561 CheckGameLoadStatus(NULL
);
566 case ENET_EVENT_TYPE_RECEIVE
:
568 // If there is an active session with this peer, then process the message
570 CNetServerSession
* session
= static_cast<CNetServerSession
*>(event
.peer
->data
);
573 // Create message from raw data
574 CNetMessage
* msg
= CNetMessageFactory::CreateMessage(event
.packet
->data
, event
.packet
->dataLength
, GetScriptInterface());
577 LOGMESSAGE("Net server: Received message %s of size %lu from %s", msg
->ToString().c_str(), (unsigned long)msg
->GetSerializedLength(), DebugName(session
).c_str());
579 HandleMessageReceive(msg
, session
);
585 // Done using the packet
586 enet_packet_destroy(event
.packet
);
591 case ENET_EVENT_TYPE_NONE
:
598 void CNetServerWorker::CheckClientConnections()
600 // Send messages at most once per second
601 std::time_t now
= std::time(nullptr);
602 if (now
<= m_LastConnectionCheck
)
605 m_LastConnectionCheck
= now
;
607 for (size_t i
= 0; i
< m_Sessions
.size(); ++i
)
609 u32 lastReceived
= m_Sessions
[i
]->GetLastReceivedTime();
610 u32 meanRTT
= m_Sessions
[i
]->GetMeanRTT();
612 CNetMessage
* message
= nullptr;
614 // Report if we didn't hear from the client since few seconds
615 if (lastReceived
> NETWORK_WARNING_TIMEOUT
)
617 CClientTimeoutMessage
* msg
= new CClientTimeoutMessage();
618 msg
->m_GUID
= m_Sessions
[i
]->GetGUID();
619 msg
->m_LastReceivedTime
= lastReceived
;
622 // Report if the client has bad ping
623 else if (meanRTT
> NETWORK_BAD_PING
)
625 CClientPerformanceMessage
* msg
= new CClientPerformanceMessage();
626 CClientPerformanceMessage::S_m_Clients client
;
627 client
.m_GUID
= m_Sessions
[i
]->GetGUID();
628 client
.m_MeanRTT
= meanRTT
;
629 msg
->m_Clients
.push_back(client
);
633 // Send to all clients except the affected one
634 // (since that will show the locally triggered warning instead).
635 // Also send it to clients that finished the loading screen while
636 // the game is still waiting for other clients to finish the loading screen.
638 for (size_t j
= 0; j
< m_Sessions
.size(); ++j
)
641 (m_Sessions
[j
]->GetCurrState() == NSS_PREGAME
&& m_State
== SERVER_STATE_PREGAME
) ||
642 m_Sessions
[j
]->GetCurrState() == NSS_INGAME
))
644 m_Sessions
[j
]->SendMessage(message
);
648 SAFE_DELETE(message
);
652 void CNetServerWorker::HandleMessageReceive(const CNetMessage
* message
, CNetServerSession
* session
)
654 // Handle non-FSM messages first
655 Status status
= session
->GetFileTransferer().HandleMessageReceive(*message
);
656 if (status
!= INFO::SKIPPED
)
659 if (message
->GetType() == NMT_FILE_TRANSFER_REQUEST
)
661 CFileTransferRequestMessage
* reqMessage
= (CFileTransferRequestMessage
*)message
;
663 // Rejoining client got our JoinSyncStart after we received the state from
664 // another client, and has now requested that we forward it to them
666 ENSURE(!m_JoinSyncFile
.empty());
667 session
->GetFileTransferer().StartResponse(reqMessage
->m_RequestID
, m_JoinSyncFile
);
673 if (!session
->Update(message
->GetType(), (void*)message
))
674 LOGERROR("Net server: Error running FSM update (type=%d state=%d)", (int)message
->GetType(), (int)session
->GetCurrState());
677 void CNetServerWorker::SetupSession(CNetServerSession
* session
)
679 void* context
= session
;
681 // Set up transitions for session
683 session
->AddTransition(NSS_UNCONNECTED
, (uint
)NMT_CONNECTION_LOST
, NSS_UNCONNECTED
);
685 session
->AddTransition(NSS_HANDSHAKE
, (uint
)NMT_CONNECTION_LOST
, NSS_UNCONNECTED
);
686 session
->AddTransition(NSS_HANDSHAKE
, (uint
)NMT_CLIENT_HANDSHAKE
, NSS_AUTHENTICATE
, &OnClientHandshake
, context
);
688 session
->AddTransition(NSS_LOBBY_AUTHENTICATE
, (uint
)NMT_CONNECTION_LOST
, NSS_UNCONNECTED
);
689 session
->AddTransition(NSS_LOBBY_AUTHENTICATE
, (uint
)NMT_AUTHENTICATE
, NSS_PREGAME
, &OnAuthenticate
, context
);
691 session
->AddTransition(NSS_AUTHENTICATE
, (uint
)NMT_CONNECTION_LOST
, NSS_UNCONNECTED
);
692 session
->AddTransition(NSS_AUTHENTICATE
, (uint
)NMT_AUTHENTICATE
, NSS_PREGAME
, &OnAuthenticate
, context
);
694 session
->AddTransition(NSS_PREGAME
, (uint
)NMT_CONNECTION_LOST
, NSS_UNCONNECTED
, &OnDisconnect
, context
);
695 session
->AddTransition(NSS_PREGAME
, (uint
)NMT_CHAT
, NSS_PREGAME
, &OnChat
, context
);
696 session
->AddTransition(NSS_PREGAME
, (uint
)NMT_READY
, NSS_PREGAME
, &OnReady
, context
);
697 session
->AddTransition(NSS_PREGAME
, (uint
)NMT_CLEAR_ALL_READY
, NSS_PREGAME
, &OnClearAllReady
, context
);
698 session
->AddTransition(NSS_PREGAME
, (uint
)NMT_GAME_SETUP
, NSS_PREGAME
, &OnGameSetup
, context
);
699 session
->AddTransition(NSS_PREGAME
, (uint
)NMT_ASSIGN_PLAYER
, NSS_PREGAME
, &OnAssignPlayer
, context
);
700 session
->AddTransition(NSS_PREGAME
, (uint
)NMT_KICKED
, NSS_PREGAME
, &OnKickPlayer
, context
);
701 session
->AddTransition(NSS_PREGAME
, (uint
)NMT_GAME_START
, NSS_PREGAME
, &OnGameStart
, context
);
702 session
->AddTransition(NSS_PREGAME
, (uint
)NMT_LOADED_GAME
, NSS_INGAME
, &OnLoadedGame
, context
);
704 session
->AddTransition(NSS_JOIN_SYNCING
, (uint
)NMT_KICKED
, NSS_JOIN_SYNCING
, &OnKickPlayer
, context
);
705 session
->AddTransition(NSS_JOIN_SYNCING
, (uint
)NMT_CONNECTION_LOST
, NSS_UNCONNECTED
, &OnDisconnect
, context
);
706 session
->AddTransition(NSS_JOIN_SYNCING
, (uint
)NMT_LOADED_GAME
, NSS_INGAME
, &OnJoinSyncingLoadedGame
, context
);
708 session
->AddTransition(NSS_INGAME
, (uint
)NMT_REJOINED
, NSS_INGAME
, &OnRejoined
, context
);
709 session
->AddTransition(NSS_INGAME
, (uint
)NMT_KICKED
, NSS_INGAME
, &OnKickPlayer
, context
);
710 session
->AddTransition(NSS_INGAME
, (uint
)NMT_CLIENT_PAUSED
, NSS_INGAME
, &OnClientPaused
, context
);
711 session
->AddTransition(NSS_INGAME
, (uint
)NMT_CONNECTION_LOST
, NSS_UNCONNECTED
, &OnDisconnect
, context
);
712 session
->AddTransition(NSS_INGAME
, (uint
)NMT_CHAT
, NSS_INGAME
, &OnChat
, context
);
713 session
->AddTransition(NSS_INGAME
, (uint
)NMT_SIMULATION_COMMAND
, NSS_INGAME
, &OnSimulationCommand
, context
);
714 session
->AddTransition(NSS_INGAME
, (uint
)NMT_SYNC_CHECK
, NSS_INGAME
, &OnSyncCheck
, context
);
715 session
->AddTransition(NSS_INGAME
, (uint
)NMT_END_COMMAND_BATCH
, NSS_INGAME
, &OnEndCommandBatch
, context
);
718 session
->SetFirstState(NSS_HANDSHAKE
);
721 bool CNetServerWorker::HandleConnect(CNetServerSession
* session
)
723 if (std::find(m_BannedIPs
.begin(), m_BannedIPs
.end(), session
->GetIPAddress()) != m_BannedIPs
.end())
725 session
->Disconnect(NDR_BANNED
);
729 CSrvHandshakeMessage handshake
;
730 handshake
.m_Magic
= PS_PROTOCOL_MAGIC
;
731 handshake
.m_ProtocolVersion
= PS_PROTOCOL_VERSION
;
732 handshake
.m_SoftwareVersion
= PS_PROTOCOL_VERSION
;
733 return session
->SendMessage(&handshake
);
736 void CNetServerWorker::OnUserJoin(CNetServerSession
* session
)
738 AddPlayer(session
->GetGUID(), session
->GetUserName());
740 CPlayerAssignmentMessage assignMessage
;
741 ConstructPlayerAssignmentMessage(assignMessage
);
742 session
->SendMessage(&assignMessage
);
745 void CNetServerWorker::OnUserLeave(CNetServerSession
* session
)
747 std::vector
<CStr
>::iterator pausing
= std::find(m_PausingPlayers
.begin(), m_PausingPlayers
.end(), session
->GetGUID());
748 if (pausing
!= m_PausingPlayers
.end())
749 m_PausingPlayers
.erase(pausing
);
751 RemovePlayer(session
->GetGUID());
753 if (m_ServerTurnManager
&& session
->GetCurrState() != NSS_JOIN_SYNCING
)
754 m_ServerTurnManager
->UninitialiseClient(session
->GetHostID());
756 // TODO: ought to switch the player controlled by that client
757 // back to AI control, or something?
760 void CNetServerWorker::AddPlayer(const CStr
& guid
, const CStrW
& name
)
762 // Find all player IDs in active use; we mustn't give them to a second player (excluding the unassigned ID: -1)
763 std::set
<i32
> usedIDs
;
764 for (const std::pair
<const CStr
, PlayerAssignment
>& p
: m_PlayerAssignments
)
765 if (p
.second
.m_Enabled
&& p
.second
.m_PlayerID
!= -1)
766 usedIDs
.insert(p
.second
.m_PlayerID
);
768 // If the player is rejoining after disconnecting, try to give them
769 // back their old player ID. Don't do this in pregame however,
770 // as that ID might be invalid for various reasons.
774 if (m_State
!= SERVER_STATE_UNCONNECTED
&& m_State
!= SERVER_STATE_PREGAME
)
776 // Try to match GUID first
777 for (PlayerAssignmentMap::iterator it
= m_PlayerAssignments
.begin(); it
!= m_PlayerAssignments
.end(); ++it
)
779 if (!it
->second
.m_Enabled
&& it
->first
== guid
&& usedIDs
.find(it
->second
.m_PlayerID
) == usedIDs
.end())
781 playerID
= it
->second
.m_PlayerID
;
782 m_PlayerAssignments
.erase(it
); // delete the old mapping, since we've got a new one now
787 // Try to match username next
788 for (PlayerAssignmentMap::iterator it
= m_PlayerAssignments
.begin(); it
!= m_PlayerAssignments
.end(); ++it
)
790 if (!it
->second
.m_Enabled
&& it
->second
.m_Name
== name
&& usedIDs
.find(it
->second
.m_PlayerID
) == usedIDs
.end())
792 playerID
= it
->second
.m_PlayerID
;
793 m_PlayerAssignments
.erase(it
); // delete the old mapping, since we've got a new one now
800 PlayerAssignment assignment
;
801 assignment
.m_Enabled
= true;
802 assignment
.m_Name
= name
;
803 assignment
.m_PlayerID
= playerID
;
804 assignment
.m_Status
= 0;
805 m_PlayerAssignments
[guid
] = assignment
;
807 // Send the new assignments to all currently active players
808 // (which does not include the one that's just joining)
809 SendPlayerAssignments();
812 void CNetServerWorker::RemovePlayer(const CStr
& guid
)
814 m_PlayerAssignments
[guid
].m_Enabled
= false;
816 SendPlayerAssignments();
819 void CNetServerWorker::ClearAllPlayerReady()
821 for (std::pair
<const CStr
, PlayerAssignment
>& p
: m_PlayerAssignments
)
822 if (p
.second
.m_Status
!= 2)
823 p
.second
.m_Status
= 0;
825 SendPlayerAssignments();
828 void CNetServerWorker::KickPlayer(const CStrW
& playerName
, const bool ban
)
830 // Find the user with that name
831 std::vector
<CNetServerSession
*>::iterator it
= std::find_if(m_Sessions
.begin(), m_Sessions
.end(),
832 [&](CNetServerSession
* session
) { return session
->GetUserName() == playerName
; });
834 // and return if no one or the host has that name
835 if (it
== m_Sessions
.end() || (*it
)->GetGUID() == m_ControllerGUID
)
841 if (std::find(m_BannedPlayers
.begin(), m_BannedPlayers
.end(), playerName
) == m_BannedPlayers
.end())
842 m_BannedPlayers
.push_back(m_LobbyAuth
? CStrW(playerName
.substr(0, playerName
.find(L
" ("))) : playerName
);
844 // Remember IP address
845 u32 ipAddress
= (*it
)->GetIPAddress();
846 if (std::find(m_BannedIPs
.begin(), m_BannedIPs
.end(), ipAddress
) == m_BannedIPs
.end())
847 m_BannedIPs
.push_back(ipAddress
);
850 // Disconnect that user
851 (*it
)->Disconnect(ban
? NDR_BANNED
: NDR_KICKED
);
853 // Send message notifying other clients
854 CKickedMessage kickedMessage
;
855 kickedMessage
.m_Name
= playerName
;
856 kickedMessage
.m_Ban
= ban
;
857 Broadcast(&kickedMessage
, { NSS_PREGAME
, NSS_JOIN_SYNCING
, NSS_INGAME
});
860 void CNetServerWorker::AssignPlayer(int playerID
, const CStr
& guid
)
862 // Remove anyone who's already assigned to this player
863 for (std::pair
<const CStr
, PlayerAssignment
>& p
: m_PlayerAssignments
)
865 if (p
.second
.m_PlayerID
== playerID
)
866 p
.second
.m_PlayerID
= -1;
869 // Update this host's assignment if it exists
870 if (m_PlayerAssignments
.find(guid
) != m_PlayerAssignments
.end())
871 m_PlayerAssignments
[guid
].m_PlayerID
= playerID
;
873 SendPlayerAssignments();
876 void CNetServerWorker::ConstructPlayerAssignmentMessage(CPlayerAssignmentMessage
& message
)
878 for (const std::pair
<const CStr
, PlayerAssignment
>& p
: m_PlayerAssignments
)
880 if (!p
.second
.m_Enabled
)
883 CPlayerAssignmentMessage::S_m_Hosts h
;
885 h
.m_Name
= p
.second
.m_Name
;
886 h
.m_PlayerID
= p
.second
.m_PlayerID
;
887 h
.m_Status
= p
.second
.m_Status
;
888 message
.m_Hosts
.push_back(h
);
892 void CNetServerWorker::SendPlayerAssignments()
894 CPlayerAssignmentMessage message
;
895 ConstructPlayerAssignmentMessage(message
);
896 Broadcast(&message
, { NSS_PREGAME
, NSS_JOIN_SYNCING
, NSS_INGAME
});
899 const ScriptInterface
& CNetServerWorker::GetScriptInterface()
901 return *m_ScriptInterface
;
904 void CNetServerWorker::SetTurnLength(u32 msecs
)
906 if (m_ServerTurnManager
)
907 m_ServerTurnManager
->SetTurnLength(msecs
);
910 void CNetServerWorker::ProcessLobbyAuth(const CStr
& name
, const CStr
& token
)
912 LOGMESSAGE("Net Server: Received lobby auth message from %s with %s", name
, token
);
913 // Find the user with that guid
914 std::vector
<CNetServerSession
*>::iterator it
= std::find_if(m_Sessions
.begin(), m_Sessions
.end(),
915 [&](CNetServerSession
* session
)
916 { return session
->GetGUID() == token
; });
918 if (it
== m_Sessions
.end())
921 (*it
)->SetUserName(name
.FromUTF8());
922 // Send an empty message to request the authentication message from the client
923 // after its identity has been confirmed via the lobby
924 CAuthenticateMessage emptyMessage
;
925 (*it
)->SendMessage(&emptyMessage
);
928 bool CNetServerWorker::OnClientHandshake(void* context
, CFsmEvent
* event
)
930 ENSURE(event
->GetType() == (uint
)NMT_CLIENT_HANDSHAKE
);
932 CNetServerSession
* session
= (CNetServerSession
*)context
;
933 CNetServerWorker
& server
= session
->GetServer();
935 CCliHandshakeMessage
* message
= (CCliHandshakeMessage
*)event
->GetParamRef();
936 if (message
->m_ProtocolVersion
!= PS_PROTOCOL_VERSION
)
938 session
->Disconnect(NDR_INCORRECT_PROTOCOL_VERSION
);
942 CStr guid
= ps_generate_guid();
944 // Ensure unique GUID
946 server
.m_Sessions
.begin(), server
.m_Sessions
.end(),
947 [&guid
] (const CNetServerSession
* session
)
948 { return session
->GetGUID() == guid
; }) != server
.m_Sessions
.end())
952 session
->Disconnect(NDR_GUID_FAILED
);
955 guid
= ps_generate_guid();
958 session
->SetGUID(guid
);
960 CSrvHandshakeResponseMessage handshakeResponse
;
961 handshakeResponse
.m_UseProtocolVersion
= PS_PROTOCOL_VERSION
;
962 handshakeResponse
.m_GUID
= guid
;
963 handshakeResponse
.m_Flags
= 0;
965 if (server
.m_LobbyAuth
)
967 handshakeResponse
.m_Flags
|= PS_NETWORK_FLAG_REQUIRE_LOBBYAUTH
;
968 session
->SetNextState(NSS_LOBBY_AUTHENTICATE
);
971 session
->SendMessage(&handshakeResponse
);
976 bool CNetServerWorker::OnAuthenticate(void* context
, CFsmEvent
* event
)
978 ENSURE(event
->GetType() == (uint
)NMT_AUTHENTICATE
);
980 CNetServerSession
* session
= (CNetServerSession
*)context
;
981 CNetServerWorker
& server
= session
->GetServer();
983 // Prohibit joins while the game is loading
984 if (server
.m_State
== SERVER_STATE_LOADING
)
986 LOGMESSAGE("Refused connection while the game is loading");
987 session
->Disconnect(NDR_SERVER_LOADING
);
991 CAuthenticateMessage
* message
= (CAuthenticateMessage
*)event
->GetParamRef();
992 CStrW username
= SanitisePlayerName(message
->m_Name
);
993 CStrW
usernameWithoutRating(username
.substr(0, username
.find(L
" (")));
995 // Compare the lowercase names as specified by https://xmpp.org/extensions/xep-0029.html#sect-idm139493404168176
996 // "[...] comparisons will be made in case-normalized canonical form."
997 if (server
.m_LobbyAuth
&& usernameWithoutRating
.LowerCase() != session
->GetUserName().LowerCase())
999 LOGERROR("Net server: lobby auth: %s tried joining as %s",
1000 session
->GetUserName().ToUTF8(),
1001 usernameWithoutRating
.ToUTF8());
1002 session
->Disconnect(NDR_LOBBY_AUTH_FAILED
);
1006 // Check the password before anything else.
1007 // NB: m_Name must match the client's salt, @see CNetClient::SetGamePassword
1008 if (!server
.CheckPassword(message
->m_Password
, message
->m_Name
.ToUTF8()))
1010 // Noisy logerror because players are not supposed to be able to get the IP,
1011 // so this might be someone targeting the host for some reason
1012 // (or TODO a dedicated server and we do want to log anyways)
1013 LOGERROR("Net server: user %s tried joining with the wrong password",
1014 session
->GetUserName().ToUTF8());
1015 session
->Disconnect(NDR_SERVER_REFUSED
);
1019 // Either deduplicate or prohibit join if name is in use
1020 bool duplicatePlayernames
= false;
1021 CFG_GET_VAL("network.duplicateplayernames", duplicatePlayernames
);
1022 // If lobby authentication is enabled, the clients playername has already been registered.
1023 // There also can't be any duplicated names.
1024 if (!server
.m_LobbyAuth
&& duplicatePlayernames
)
1025 username
= server
.DeduplicatePlayerName(username
);
1028 std::vector
<CNetServerSession
*>::iterator it
= std::find_if(
1029 server
.m_Sessions
.begin(), server
.m_Sessions
.end(),
1030 [&username
] (const CNetServerSession
* session
)
1031 { return session
->GetUserName() == username
; });
1033 if (it
!= server
.m_Sessions
.end() && (*it
) != session
)
1035 session
->Disconnect(NDR_PLAYERNAME_IN_USE
);
1040 // Disconnect banned usernames
1041 if (std::find(server
.m_BannedPlayers
.begin(), server
.m_BannedPlayers
.end(), server
.m_LobbyAuth
? usernameWithoutRating
: username
) != server
.m_BannedPlayers
.end())
1043 session
->Disconnect(NDR_BANNED
);
1047 int maxObservers
= 0;
1048 CFG_GET_VAL("network.observerlimit", maxObservers
);
1050 bool isRejoining
= false;
1051 bool serverFull
= false;
1052 if (server
.m_State
== SERVER_STATE_PREGAME
)
1054 // Don't check for maxObservers in the gamesetup, as we don't know yet who will be assigned
1055 serverFull
= server
.m_Sessions
.size() >= MAX_CLIENTS
;
1059 bool isObserver
= true;
1060 int disconnectedPlayers
= 0;
1061 int connectedPlayers
= 0;
1062 // (TODO: if GUIDs were stable, we should use them instead)
1063 for (const std::pair
<const CStr
, PlayerAssignment
>& p
: server
.m_PlayerAssignments
)
1065 const PlayerAssignment
& assignment
= p
.second
;
1067 if (!assignment
.m_Enabled
&& assignment
.m_Name
== username
)
1069 isObserver
= assignment
.m_PlayerID
== -1;
1073 if (assignment
.m_PlayerID
== -1)
1076 if (assignment
.m_Enabled
)
1079 ++disconnectedPlayers
;
1082 // Optionally allow everyone or only buddies to join after the game has started
1085 CStr observerLateJoin
;
1086 CFG_GET_VAL("network.lateobservers", observerLateJoin
);
1088 if (observerLateJoin
== "everyone")
1092 else if (observerLateJoin
== "buddies")
1095 CFG_GET_VAL("lobby.buddies", buddies
);
1096 std::wstringstream
buddiesStream(wstring_from_utf8(buddies
));
1098 while (std::getline(buddiesStream
, buddy
, L
','))
1100 if (buddy
== usernameWithoutRating
)
1111 LOGMESSAGE("Refused connection after game start from not-previously-known user \"%s\"", utf8_from_wstring(username
));
1112 session
->Disconnect(NDR_SERVER_ALREADY_IN_GAME
);
1116 // Ensure all players will be able to rejoin
1117 serverFull
= isObserver
&& (
1118 (int) server
.m_Sessions
.size() - connectedPlayers
> maxObservers
||
1119 (int) server
.m_Sessions
.size() + disconnectedPlayers
>= MAX_CLIENTS
);
1124 session
->Disconnect(NDR_SERVER_FULL
);
1128 u32 newHostID
= server
.m_NextHostID
++;
1130 session
->SetUserName(username
);
1131 session
->SetHostID(newHostID
);
1133 CAuthenticateResultMessage authenticateResult
;
1134 authenticateResult
.m_Code
= isRejoining
? ARC_OK_REJOINING
: ARC_OK
;
1135 authenticateResult
.m_HostID
= newHostID
;
1136 authenticateResult
.m_Message
= L
"Logged in";
1137 authenticateResult
.m_IsController
= 0;
1139 if (message
->m_ControllerSecret
== server
.m_ControllerSecret
)
1141 if (server
.m_ControllerGUID
.empty())
1143 server
.m_ControllerGUID
= session
->GetGUID();
1144 authenticateResult
.m_IsController
= 1;
1146 // TODO: we could probably handle having several controllers, or swapping?
1149 session
->SendMessage(&authenticateResult
);
1151 server
.OnUserJoin(session
);
1155 ENSURE(server
.m_State
!= SERVER_STATE_UNCONNECTED
&& server
.m_State
!= SERVER_STATE_PREGAME
);
1157 // Request a copy of the current game state from an existing player,
1158 // so we can send it on to the new player
1160 // Assume session 0 is most likely the local player, so they're
1161 // the most efficient client to request a copy from
1162 CNetServerSession
* sourceSession
= server
.m_Sessions
.at(0);
1164 sourceSession
->GetFileTransferer().StartTask(
1165 std::shared_ptr
<CNetFileReceiveTask
>(new CNetFileReceiveTask_ServerRejoin(server
, newHostID
))
1168 session
->SetNextState(NSS_JOIN_SYNCING
);
1173 bool CNetServerWorker::OnSimulationCommand(void* context
, CFsmEvent
* event
)
1175 ENSURE(event
->GetType() == (uint
)NMT_SIMULATION_COMMAND
);
1177 CNetServerSession
* session
= (CNetServerSession
*)context
;
1178 CNetServerWorker
& server
= session
->GetServer();
1180 CSimulationMessage
* message
= (CSimulationMessage
*)event
->GetParamRef();
1182 // Ignore messages sent by one player on behalf of another player
1183 // unless cheating is enabled
1184 bool cheatsEnabled
= false;
1185 const ScriptInterface
& scriptInterface
= server
.GetScriptInterface();
1186 ScriptRequest
rq(scriptInterface
);
1187 JS::RootedValue
settings(rq
.cx
);
1188 Script::GetProperty(rq
, server
.m_InitAttributes
, "settings", &settings
);
1189 if (Script::HasProperty(rq
, settings
, "CheatsEnabled"))
1190 Script::GetProperty(rq
, settings
, "CheatsEnabled", cheatsEnabled
);
1192 PlayerAssignmentMap::iterator it
= server
.m_PlayerAssignments
.find(session
->GetGUID());
1193 // When cheating is disabled, fail if the player the message claims to
1194 // represent does not exist or does not match the sender's player name
1195 if (!cheatsEnabled
&& (it
== server
.m_PlayerAssignments
.end() || it
->second
.m_PlayerID
!= message
->m_Player
))
1198 // Send it back to all clients that have finished
1199 // the loading screen (and the synchronization when rejoining)
1200 server
.Broadcast(message
, { NSS_INGAME
});
1202 // Save all the received commands
1203 if (server
.m_SavedCommands
.size() < message
->m_Turn
+ 1)
1204 server
.m_SavedCommands
.resize(message
->m_Turn
+ 1);
1205 server
.m_SavedCommands
[message
->m_Turn
].push_back(*message
);
1207 // TODO: we shouldn't send the message back to the client that first sent it
1211 bool CNetServerWorker::OnSyncCheck(void* context
, CFsmEvent
* event
)
1213 ENSURE(event
->GetType() == (uint
)NMT_SYNC_CHECK
);
1215 CNetServerSession
* session
= (CNetServerSession
*)context
;
1216 CNetServerWorker
& server
= session
->GetServer();
1218 CSyncCheckMessage
* message
= (CSyncCheckMessage
*)event
->GetParamRef();
1220 server
.m_ServerTurnManager
->NotifyFinishedClientUpdate(*session
, message
->m_Turn
, message
->m_Hash
);
1224 bool CNetServerWorker::OnEndCommandBatch(void* context
, CFsmEvent
* event
)
1226 ENSURE(event
->GetType() == (uint
)NMT_END_COMMAND_BATCH
);
1228 CNetServerSession
* session
= (CNetServerSession
*)context
;
1229 CNetServerWorker
& server
= session
->GetServer();
1231 CEndCommandBatchMessage
* message
= (CEndCommandBatchMessage
*)event
->GetParamRef();
1233 // The turn-length field is ignored
1234 server
.m_ServerTurnManager
->NotifyFinishedClientCommands(*session
, message
->m_Turn
);
1238 bool CNetServerWorker::OnChat(void* context
, CFsmEvent
* event
)
1240 ENSURE(event
->GetType() == (uint
)NMT_CHAT
);
1242 CNetServerSession
* session
= (CNetServerSession
*)context
;
1243 CNetServerWorker
& server
= session
->GetServer();
1245 CChatMessage
* message
= (CChatMessage
*)event
->GetParamRef();
1247 message
->m_GUID
= session
->GetGUID();
1249 server
.Broadcast(message
, { NSS_PREGAME
, NSS_INGAME
});
1254 bool CNetServerWorker::OnReady(void* context
, CFsmEvent
* event
)
1256 ENSURE(event
->GetType() == (uint
)NMT_READY
);
1258 CNetServerSession
* session
= (CNetServerSession
*)context
;
1259 CNetServerWorker
& server
= session
->GetServer();
1261 // Occurs if a client presses not-ready
1262 // in the very last moment before the hosts starts the game
1263 if (server
.m_State
== SERVER_STATE_LOADING
)
1266 CReadyMessage
* message
= (CReadyMessage
*)event
->GetParamRef();
1267 message
->m_GUID
= session
->GetGUID();
1268 server
.Broadcast(message
, { NSS_PREGAME
});
1270 server
.m_PlayerAssignments
[message
->m_GUID
].m_Status
= message
->m_Status
;
1275 bool CNetServerWorker::OnClearAllReady(void* context
, CFsmEvent
* event
)
1277 ENSURE(event
->GetType() == (uint
)NMT_CLEAR_ALL_READY
);
1279 CNetServerSession
* session
= (CNetServerSession
*)context
;
1280 CNetServerWorker
& server
= session
->GetServer();
1282 if (session
->GetGUID() == server
.m_ControllerGUID
)
1283 server
.ClearAllPlayerReady();
1288 bool CNetServerWorker::OnGameSetup(void* context
, CFsmEvent
* event
)
1290 ENSURE(event
->GetType() == (uint
)NMT_GAME_SETUP
);
1292 CNetServerSession
* session
= (CNetServerSession
*)context
;
1293 CNetServerWorker
& server
= session
->GetServer();
1295 // Changing the settings after gamestart is not implemented and would cause an Out-of-sync error.
1296 // This happened when doubleclicking on the startgame button.
1297 if (server
.m_State
!= SERVER_STATE_PREGAME
)
1300 // Only the controller is allowed to send game setup updates.
1301 // TODO: it would be good to allow other players to request changes to some settings,
1302 // e.g. their civilisation.
1303 // Possibly this should use another message, to enforce a single source of truth.
1304 if (session
->GetGUID() == server
.m_ControllerGUID
)
1306 CGameSetupMessage
* message
= (CGameSetupMessage
*)event
->GetParamRef();
1307 server
.Broadcast(message
, { NSS_PREGAME
});
1312 bool CNetServerWorker::OnAssignPlayer(void* context
, CFsmEvent
* event
)
1314 ENSURE(event
->GetType() == (uint
)NMT_ASSIGN_PLAYER
);
1315 CNetServerSession
* session
= (CNetServerSession
*)context
;
1316 CNetServerWorker
& server
= session
->GetServer();
1318 if (session
->GetGUID() == server
.m_ControllerGUID
)
1320 CAssignPlayerMessage
* message
= (CAssignPlayerMessage
*)event
->GetParamRef();
1321 server
.AssignPlayer(message
->m_PlayerID
, message
->m_GUID
);
1326 bool CNetServerWorker::OnGameStart(void* context
, CFsmEvent
* event
)
1328 ENSURE(event
->GetType() == (uint
)NMT_GAME_START
);
1329 CNetServerSession
* session
= (CNetServerSession
*)context
;
1330 CNetServerWorker
& server
= session
->GetServer();
1332 if (session
->GetGUID() != server
.m_ControllerGUID
)
1335 CGameStartMessage
* message
= (CGameStartMessage
*)event
->GetParamRef();
1336 server
.StartGame(message
->m_InitAttributes
);
1340 bool CNetServerWorker::OnLoadedGame(void* context
, CFsmEvent
* event
)
1342 ENSURE(event
->GetType() == (uint
)NMT_LOADED_GAME
);
1344 CNetServerSession
* loadedSession
= (CNetServerSession
*)context
;
1345 CNetServerWorker
& server
= loadedSession
->GetServer();
1347 // We're in the loading state, so wait until every client has loaded
1348 // before starting the game
1349 ENSURE(server
.m_State
== SERVER_STATE_LOADING
);
1350 if (server
.CheckGameLoadStatus(loadedSession
))
1353 CClientsLoadingMessage message
;
1354 // We always send all GUIDs of clients in the loading state
1355 // so that we don't have to bother about switching GUI pages
1356 for (CNetServerSession
* session
: server
.m_Sessions
)
1357 if (session
->GetCurrState() != NSS_INGAME
&& loadedSession
->GetGUID() != session
->GetGUID())
1359 CClientsLoadingMessage::S_m_Clients client
;
1360 client
.m_GUID
= session
->GetGUID();
1361 message
.m_Clients
.push_back(client
);
1364 // Send to the client who has loaded the game but did not reach the NSS_INGAME state yet
1365 loadedSession
->SendMessage(&message
);
1366 server
.Broadcast(&message
, { NSS_INGAME
});
1371 bool CNetServerWorker::OnJoinSyncingLoadedGame(void* context
, CFsmEvent
* event
)
1373 // A client rejoining an in-progress game has now finished loading the
1374 // map and deserialized the initial state.
1375 // The simulation may have progressed since then, so send any subsequent
1376 // commands to them and set them as an active player so they can participate
1377 // in all future turns.
1379 // (TODO: if it takes a long time for them to receive and execute all these
1380 // commands, the other players will get frozen for that time and may be unhappy;
1381 // we could try repeating this process a few times until the client converges
1382 // on the up-to-date state, before setting them as active.)
1384 ENSURE(event
->GetType() == (uint
)NMT_LOADED_GAME
);
1386 CNetServerSession
* session
= (CNetServerSession
*)context
;
1387 CNetServerWorker
& server
= session
->GetServer();
1389 CLoadedGameMessage
* message
= (CLoadedGameMessage
*)event
->GetParamRef();
1391 u32 turn
= message
->m_CurrentTurn
;
1392 u32 readyTurn
= server
.m_ServerTurnManager
->GetReadyTurn();
1394 // Send them all commands received since their saved state,
1395 // and turn-ended messages for any turns that have already been processed
1396 for (size_t i
= turn
+ 1; i
< std::max(readyTurn
+1, (u32
)server
.m_SavedCommands
.size()); ++i
)
1398 if (i
< server
.m_SavedCommands
.size())
1399 for (size_t j
= 0; j
< server
.m_SavedCommands
[i
].size(); ++j
)
1400 session
->SendMessage(&server
.m_SavedCommands
[i
][j
]);
1404 CEndCommandBatchMessage endMessage
;
1405 endMessage
.m_Turn
= i
;
1406 endMessage
.m_TurnLength
= server
.m_ServerTurnManager
->GetSavedTurnLength(i
);
1407 session
->SendMessage(&endMessage
);
1411 // Tell the turn manager to expect commands from this new client
1412 // Special case: the controller shouldn't be treated as an observer in any case.
1413 bool isObserver
= server
.m_PlayerAssignments
[session
->GetGUID()].m_PlayerID
== -1 && server
.m_ControllerGUID
!= session
->GetGUID();
1414 server
.m_ServerTurnManager
->InitialiseClient(session
->GetHostID(), readyTurn
, isObserver
);
1416 // Tell the client that everything has finished loading and it should start now
1417 CLoadedGameMessage loaded
;
1418 loaded
.m_CurrentTurn
= readyTurn
;
1419 session
->SendMessage(&loaded
);
1424 bool CNetServerWorker::OnRejoined(void* context
, CFsmEvent
* event
)
1426 // A client has finished rejoining and the loading screen disappeared.
1427 ENSURE(event
->GetType() == (uint
)NMT_REJOINED
);
1429 CNetServerSession
* session
= (CNetServerSession
*)context
;
1430 CNetServerWorker
& server
= session
->GetServer();
1432 // Inform everyone of the client having rejoined
1433 CRejoinedMessage
* message
= (CRejoinedMessage
*)event
->GetParamRef();
1434 message
->m_GUID
= session
->GetGUID();
1435 server
.Broadcast(message
, { NSS_INGAME
});
1437 // Send all pausing players to the rejoined client.
1438 for (const CStr
& guid
: server
.m_PausingPlayers
)
1440 CClientPausedMessage pausedMessage
;
1441 pausedMessage
.m_GUID
= guid
;
1442 pausedMessage
.m_Pause
= true;
1443 session
->SendMessage(&pausedMessage
);
1449 bool CNetServerWorker::OnKickPlayer(void* context
, CFsmEvent
* event
)
1451 ENSURE(event
->GetType() == (uint
)NMT_KICKED
);
1453 CNetServerSession
* session
= (CNetServerSession
*)context
;
1454 CNetServerWorker
& server
= session
->GetServer();
1456 if (session
->GetGUID() == server
.m_ControllerGUID
)
1458 CKickedMessage
* message
= (CKickedMessage
*)event
->GetParamRef();
1459 server
.KickPlayer(message
->m_Name
, message
->m_Ban
);
1464 bool CNetServerWorker::OnDisconnect(void* context
, CFsmEvent
* event
)
1466 ENSURE(event
->GetType() == (uint
)NMT_CONNECTION_LOST
);
1468 CNetServerSession
* session
= (CNetServerSession
*)context
;
1469 CNetServerWorker
& server
= session
->GetServer();
1471 server
.OnUserLeave(session
);
1476 bool CNetServerWorker::OnClientPaused(void* context
, CFsmEvent
* event
)
1478 ENSURE(event
->GetType() == (uint
)NMT_CLIENT_PAUSED
);
1480 CNetServerSession
* session
= (CNetServerSession
*)context
;
1481 CNetServerWorker
& server
= session
->GetServer();
1483 CClientPausedMessage
* message
= (CClientPausedMessage
*)event
->GetParamRef();
1485 message
->m_GUID
= session
->GetGUID();
1487 // Update the list of pausing players.
1488 std::vector
<CStr
>::iterator player
= std::find(server
.m_PausingPlayers
.begin(), server
.m_PausingPlayers
.end(), session
->GetGUID());
1490 if (message
->m_Pause
)
1492 if (player
!= server
.m_PausingPlayers
.end())
1495 server
.m_PausingPlayers
.push_back(session
->GetGUID());
1499 if (player
== server
.m_PausingPlayers
.end())
1502 server
.m_PausingPlayers
.erase(player
);
1505 // Send messages to clients that are in game, and are not the client who paused.
1506 for (CNetServerSession
* netSession
: server
.m_Sessions
)
1507 if (netSession
->GetCurrState() == NSS_INGAME
&& message
->m_GUID
!= netSession
->GetGUID())
1508 netSession
->SendMessage(message
);
1513 bool CNetServerWorker::CheckGameLoadStatus(CNetServerSession
* changedSession
)
1515 for (const CNetServerSession
* session
: m_Sessions
)
1516 if (session
!= changedSession
&& session
->GetCurrState() != NSS_INGAME
)
1519 // Inform clients that everyone has loaded the map and that the game can start
1520 CLoadedGameMessage loaded
;
1521 loaded
.m_CurrentTurn
= 0;
1523 // Notice the changedSession is still in the NSS_PREGAME state
1524 Broadcast(&loaded
, { NSS_PREGAME
, NSS_INGAME
});
1526 m_State
= SERVER_STATE_INGAME
;
1530 void CNetServerWorker::StartGame(const CStr
& initAttribs
)
1532 for (std::pair
<const CStr
, PlayerAssignment
>& player
: m_PlayerAssignments
)
1533 if (player
.second
.m_Enabled
&& player
.second
.m_PlayerID
!= -1 && player
.second
.m_Status
== 0)
1535 LOGERROR("Tried to start the game without player \"%s\" being ready!", utf8_from_wstring(player
.second
.m_Name
).c_str());
1539 m_ServerTurnManager
= new CNetServerTurnManager(*this);
1541 for (CNetServerSession
* session
: m_Sessions
)
1543 // Special case: the controller shouldn't be treated as an observer in any case.
1544 bool isObserver
= m_PlayerAssignments
[session
->GetGUID()].m_PlayerID
== -1 && m_ControllerGUID
!= session
->GetGUID();
1545 m_ServerTurnManager
->InitialiseClient(session
->GetHostID(), 0, isObserver
);
1548 m_State
= SERVER_STATE_LOADING
;
1550 // Remove players and observers that are not present when the game starts
1551 for (PlayerAssignmentMap::iterator it
= m_PlayerAssignments
.begin(); it
!= m_PlayerAssignments
.end();)
1552 if (it
->second
.m_Enabled
)
1555 it
= m_PlayerAssignments
.erase(it
);
1557 SendPlayerAssignments();
1559 // Update init attributes. They should no longer change.
1560 Script::ParseJSON(ScriptRequest(m_ScriptInterface
), initAttribs
, &m_InitAttributes
);
1562 CGameStartMessage gameStart
;
1563 gameStart
.m_InitAttributes
= initAttribs
;
1564 Broadcast(&gameStart
, { NSS_PREGAME
});
1567 CStrW
CNetServerWorker::SanitisePlayerName(const CStrW
& original
)
1569 const size_t MAX_LENGTH
= 32;
1571 CStrW name
= original
;
1572 name
.Replace(L
"[", L
"{"); // remove GUI tags
1573 name
.Replace(L
"]", L
"}"); // remove for symmetry
1575 // Restrict the length
1576 if (name
.length() > MAX_LENGTH
)
1577 name
= name
.Left(MAX_LENGTH
);
1579 // Don't allow surrounding whitespace
1580 name
.Trim(PS_TRIM_BOTH
);
1582 // Don't allow empty name
1584 name
= L
"Anonymous";
1589 CStrW
CNetServerWorker::DeduplicatePlayerName(const CStrW
& original
)
1591 CStrW name
= original
;
1593 // Try names "Foo", "Foo (2)", "Foo (3)", etc
1598 for (const CNetServerSession
* session
: m_Sessions
)
1600 if (session
->GetUserName() == name
)
1610 name
= original
+ L
" (" + CStrW::FromUInt(id
++) + L
")";
1614 void CNetServerWorker::SendHolePunchingMessage(const CStr
& ipStr
, u16 port
)
1617 StunClient::SendHolePunchingMessages(*m_Host
, ipStr
, port
);
1623 CNetServer::CNetServer(bool useLobbyAuth
) :
1624 m_Worker(new CNetServerWorker(useLobbyAuth
)),
1625 m_LobbyAuth(useLobbyAuth
), m_UseSTUN(false), m_PublicIp(""), m_PublicPort(20595), m_Password()
1629 CNetServer::~CNetServer()
1634 bool CNetServer::GetUseSTUN() const
1639 bool CNetServer::UseLobbyAuth() const
1644 bool CNetServer::SetupConnection(const u16 port
)
1646 return m_Worker
->SetupConnection(port
);
1649 CStr
CNetServer::GetPublicIp() const
1654 u16
CNetServer::GetPublicPort() const
1656 return m_PublicPort
;
1659 u16
CNetServer::GetLocalPort() const
1661 std::lock_guard
<std::mutex
> lock(m_Worker
->m_WorkerMutex
);
1662 if (!m_Worker
->m_Host
)
1664 return m_Worker
->m_Host
->address
.port
;
1667 void CNetServer::SetConnectionData(const CStr
& ip
, const u16 port
)
1670 m_PublicPort
= port
;
1674 bool CNetServer::SetConnectionDataViaSTUN()
1677 std::lock_guard
<std::mutex
> lock(m_Worker
->m_WorkerMutex
);
1678 if (!m_Worker
->m_Host
)
1680 return StunClient::FindPublicIP(*m_Worker
->m_Host
, m_PublicIp
, m_PublicPort
);
1683 bool CNetServer::CheckPasswordAndIncrement(const std::string
& username
, const std::string
& password
, const std::string
& salt
)
1685 std::unordered_map
<std::string
, int>::iterator it
= m_FailedAttempts
.find(username
);
1686 if (m_Worker
->CheckPassword(password
, salt
))
1688 if (it
!= m_FailedAttempts
.end())
1692 if (it
== m_FailedAttempts
.end())
1693 m_FailedAttempts
.emplace(username
, 1);
1699 bool CNetServer::IsBanned(const std::string
& username
) const
1701 std::unordered_map
<std::string
, int>::const_iterator it
= m_FailedAttempts
.find(username
);
1702 return it
!= m_FailedAttempts
.end() && it
->second
>= FAILED_PASSWORD_TRIES_BEFORE_BAN
;
1705 void CNetServer::SetPassword(const CStr
& password
)
1707 m_Password
= password
;
1708 std::lock_guard
<std::mutex
> lock(m_Worker
->m_WorkerMutex
);
1709 m_Worker
->SetPassword(password
);
1712 void CNetServer::SetControllerSecret(const std::string
& secret
)
1714 std::lock_guard
<std::mutex
> lock(m_Worker
->m_WorkerMutex
);
1715 m_Worker
->SetControllerSecret(secret
);
1718 void CNetServer::StartGame()
1720 std::lock_guard
<std::mutex
> lock(m_Worker
->m_WorkerMutex
);
1721 m_Worker
->m_StartGameQueue
.push_back(true);
1724 void CNetServer::UpdateInitAttributes(JS::MutableHandleValue attrs
, const ScriptRequest
& rq
)
1726 // Pass the attributes as JSON, since that's the easiest safe
1727 // cross-thread way of passing script data
1728 std::string attrsJSON
= Script::StringifyJSON(rq
, attrs
, false);
1730 std::lock_guard
<std::mutex
> lock(m_Worker
->m_WorkerMutex
);
1731 m_Worker
->m_InitAttributesQueue
.push_back(attrsJSON
);
1734 void CNetServer::OnLobbyAuth(const CStr
& name
, const CStr
& token
)
1736 std::lock_guard
<std::mutex
> lock(m_Worker
->m_WorkerMutex
);
1737 m_Worker
->m_LobbyAuthQueue
.push_back(std::make_pair(name
, token
));
1740 void CNetServer::SetTurnLength(u32 msecs
)
1742 std::lock_guard
<std::mutex
> lock(m_Worker
->m_WorkerMutex
);
1743 m_Worker
->m_TurnLengthQueue
.push_back(msecs
);
1746 void CNetServer::SendHolePunchingMessage(const CStr
& ip
, u16 port
)
1748 m_Worker
->SendHolePunchingMessage(ip
, port
);