1 /* Copyright (C) 2021 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
, int autostartPlayers
) :
149 m_AutostartPlayers(autostartPlayers
),
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
= enet_host_create(&addr
, MAX_CLIENTS
, CHANNEL_COUNT
, 0, 0);
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 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 // Implement autostart mode
433 if (m_State
== SERVER_STATE_PREGAME
&& (int)m_PlayerAssignments
.size() == m_AutostartPlayers
)
434 StartGame(Script::StringifyJSON(ScriptRequest(m_ScriptInterface
), &m_InitAttributes
));
436 // Update profiler stats
437 m_Stats
->LatchHostState(m_Host
);
440 // Clear roots before deleting their context
441 m_SavedCommands
.clear();
443 SAFE_DELETE(m_ScriptInterface
);
446 bool CNetServerWorker::RunStep()
448 // Check for messages from the game thread.
449 // (Do as little work as possible while the mutex is held open,
450 // to avoid performance problems and deadlocks.)
452 m_ScriptInterface
->GetContext()->MaybeIncrementalGC(0.5f
);
454 ScriptRequest
rq(m_ScriptInterface
);
456 std::vector
<bool> newStartGame
;
457 std::vector
<std::string
> newGameAttributes
;
458 std::vector
<std::pair
<CStr
, CStr
>> newLobbyAuths
;
459 std::vector
<u32
> newTurnLength
;
462 std::lock_guard
<std::mutex
> lock(m_WorkerMutex
);
467 newStartGame
.swap(m_StartGameQueue
);
468 newGameAttributes
.swap(m_InitAttributesQueue
);
469 newLobbyAuths
.swap(m_LobbyAuthQueue
);
470 newTurnLength
.swap(m_TurnLengthQueue
);
473 if (!newGameAttributes
.empty())
475 if (m_State
!= SERVER_STATE_UNCONNECTED
&& m_State
!= SERVER_STATE_PREGAME
)
476 LOGERROR("NetServer: Init Attributes cannot be changed after the server starts loading.");
479 JS::RootedValue
gameAttributesVal(rq
.cx
);
480 Script::ParseJSON(rq
, newGameAttributes
.back(), &gameAttributesVal
);
481 m_InitAttributes
= gameAttributesVal
;
485 if (!newTurnLength
.empty())
486 SetTurnLength(newTurnLength
.back());
488 while (!newLobbyAuths
.empty())
490 const std::pair
<CStr
, CStr
>& auth
= newLobbyAuths
.back();
491 ProcessLobbyAuth(auth
.first
, auth
.second
);
492 newLobbyAuths
.pop_back();
495 // Perform file transfers
496 for (CNetServerSession
* session
: m_Sessions
)
497 session
->GetFileTransferer().Poll();
499 CheckClientConnections();
501 // Process network events:
504 int status
= enet_host_service(m_Host
, &event
, HOST_SERVICE_TIMEOUT
);
507 LOGERROR("CNetServerWorker: enet_host_service failed (%d)", status
);
508 // TODO: notify game that the server has shut down
514 // Reached timeout with no events - try again
518 // Process the event:
522 case ENET_EVENT_TYPE_CONNECT
:
524 // Report the client address
525 char hostname
[256] = "(error)";
526 enet_address_get_host_ip(&event
.peer
->address
, hostname
, ARRAY_SIZE(hostname
));
527 LOGMESSAGE("Net server: Received connection from %s:%u", hostname
, (unsigned int)event
.peer
->address
.port
);
529 // Set up a session object for this peer
531 CNetServerSession
* session
= new CNetServerSession(*this, event
.peer
);
533 m_Sessions
.push_back(session
);
535 SetupSession(session
);
537 ENSURE(event
.peer
->data
== NULL
);
538 event
.peer
->data
= session
;
540 HandleConnect(session
);
545 case ENET_EVENT_TYPE_DISCONNECT
:
547 // If there is an active session with this peer, then reset and delete it
549 CNetServerSession
* session
= static_cast<CNetServerSession
*>(event
.peer
->data
);
552 LOGMESSAGE("Net server: Disconnected %s", DebugName(session
).c_str());
554 // Remove the session first, so we won't send player-update messages to it
555 // when updating the FSM
556 m_Sessions
.erase(remove(m_Sessions
.begin(), m_Sessions
.end(), session
), m_Sessions
.end());
558 session
->Update((uint
)NMT_CONNECTION_LOST
, NULL
);
561 event
.peer
->data
= NULL
;
564 if (m_State
== SERVER_STATE_LOADING
)
565 CheckGameLoadStatus(NULL
);
570 case ENET_EVENT_TYPE_RECEIVE
:
572 // If there is an active session with this peer, then process the message
574 CNetServerSession
* session
= static_cast<CNetServerSession
*>(event
.peer
->data
);
577 // Create message from raw data
578 CNetMessage
* msg
= CNetMessageFactory::CreateMessage(event
.packet
->data
, event
.packet
->dataLength
, GetScriptInterface());
581 LOGMESSAGE("Net server: Received message %s of size %lu from %s", msg
->ToString().c_str(), (unsigned long)msg
->GetSerializedLength(), DebugName(session
).c_str());
583 HandleMessageReceive(msg
, session
);
589 // Done using the packet
590 enet_packet_destroy(event
.packet
);
595 case ENET_EVENT_TYPE_NONE
:
602 void CNetServerWorker::CheckClientConnections()
604 // Send messages at most once per second
605 std::time_t now
= std::time(nullptr);
606 if (now
<= m_LastConnectionCheck
)
609 m_LastConnectionCheck
= now
;
611 for (size_t i
= 0; i
< m_Sessions
.size(); ++i
)
613 u32 lastReceived
= m_Sessions
[i
]->GetLastReceivedTime();
614 u32 meanRTT
= m_Sessions
[i
]->GetMeanRTT();
616 CNetMessage
* message
= nullptr;
618 // Report if we didn't hear from the client since few seconds
619 if (lastReceived
> NETWORK_WARNING_TIMEOUT
)
621 CClientTimeoutMessage
* msg
= new CClientTimeoutMessage();
622 msg
->m_GUID
= m_Sessions
[i
]->GetGUID();
623 msg
->m_LastReceivedTime
= lastReceived
;
626 // Report if the client has bad ping
627 else if (meanRTT
> NETWORK_BAD_PING
)
629 CClientPerformanceMessage
* msg
= new CClientPerformanceMessage();
630 CClientPerformanceMessage::S_m_Clients client
;
631 client
.m_GUID
= m_Sessions
[i
]->GetGUID();
632 client
.m_MeanRTT
= meanRTT
;
633 msg
->m_Clients
.push_back(client
);
637 // Send to all clients except the affected one
638 // (since that will show the locally triggered warning instead).
639 // Also send it to clients that finished the loading screen while
640 // the game is still waiting for other clients to finish the loading screen.
642 for (size_t j
= 0; j
< m_Sessions
.size(); ++j
)
645 (m_Sessions
[j
]->GetCurrState() == NSS_PREGAME
&& m_State
== SERVER_STATE_PREGAME
) ||
646 m_Sessions
[j
]->GetCurrState() == NSS_INGAME
))
648 m_Sessions
[j
]->SendMessage(message
);
652 SAFE_DELETE(message
);
656 void CNetServerWorker::HandleMessageReceive(const CNetMessage
* message
, CNetServerSession
* session
)
658 // Handle non-FSM messages first
659 Status status
= session
->GetFileTransferer().HandleMessageReceive(*message
);
660 if (status
!= INFO::SKIPPED
)
663 if (message
->GetType() == NMT_FILE_TRANSFER_REQUEST
)
665 CFileTransferRequestMessage
* reqMessage
= (CFileTransferRequestMessage
*)message
;
667 // Rejoining client got our JoinSyncStart after we received the state from
668 // another client, and has now requested that we forward it to them
670 ENSURE(!m_JoinSyncFile
.empty());
671 session
->GetFileTransferer().StartResponse(reqMessage
->m_RequestID
, m_JoinSyncFile
);
677 if (!session
->Update(message
->GetType(), (void*)message
))
678 LOGERROR("Net server: Error running FSM update (type=%d state=%d)", (int)message
->GetType(), (int)session
->GetCurrState());
681 void CNetServerWorker::SetupSession(CNetServerSession
* session
)
683 void* context
= session
;
685 // Set up transitions for session
687 session
->AddTransition(NSS_UNCONNECTED
, (uint
)NMT_CONNECTION_LOST
, NSS_UNCONNECTED
);
689 session
->AddTransition(NSS_HANDSHAKE
, (uint
)NMT_CONNECTION_LOST
, NSS_UNCONNECTED
);
690 session
->AddTransition(NSS_HANDSHAKE
, (uint
)NMT_CLIENT_HANDSHAKE
, NSS_AUTHENTICATE
, (void*)&OnClientHandshake
, context
);
692 session
->AddTransition(NSS_LOBBY_AUTHENTICATE
, (uint
)NMT_CONNECTION_LOST
, NSS_UNCONNECTED
);
693 session
->AddTransition(NSS_LOBBY_AUTHENTICATE
, (uint
)NMT_AUTHENTICATE
, NSS_PREGAME
, (void*)&OnAuthenticate
, context
);
695 session
->AddTransition(NSS_AUTHENTICATE
, (uint
)NMT_CONNECTION_LOST
, NSS_UNCONNECTED
);
696 session
->AddTransition(NSS_AUTHENTICATE
, (uint
)NMT_AUTHENTICATE
, NSS_PREGAME
, (void*)&OnAuthenticate
, context
);
698 session
->AddTransition(NSS_PREGAME
, (uint
)NMT_CONNECTION_LOST
, NSS_UNCONNECTED
, (void*)&OnDisconnect
, context
);
699 session
->AddTransition(NSS_PREGAME
, (uint
)NMT_CHAT
, NSS_PREGAME
, (void*)&OnChat
, context
);
700 session
->AddTransition(NSS_PREGAME
, (uint
)NMT_READY
, NSS_PREGAME
, (void*)&OnReady
, context
);
701 session
->AddTransition(NSS_PREGAME
, (uint
)NMT_CLEAR_ALL_READY
, NSS_PREGAME
, (void*)&OnClearAllReady
, context
);
702 session
->AddTransition(NSS_PREGAME
, (uint
)NMT_GAME_SETUP
, NSS_PREGAME
, (void*)&OnGameSetup
, context
);
703 session
->AddTransition(NSS_PREGAME
, (uint
)NMT_ASSIGN_PLAYER
, NSS_PREGAME
, (void*)&OnAssignPlayer
, context
);
704 session
->AddTransition(NSS_PREGAME
, (uint
)NMT_KICKED
, NSS_PREGAME
, (void*)&OnKickPlayer
, context
);
705 session
->AddTransition(NSS_PREGAME
, (uint
)NMT_GAME_START
, NSS_PREGAME
, (void*)&OnGameStart
, context
);
706 session
->AddTransition(NSS_PREGAME
, (uint
)NMT_LOADED_GAME
, NSS_INGAME
, (void*)&OnLoadedGame
, context
);
708 session
->AddTransition(NSS_JOIN_SYNCING
, (uint
)NMT_KICKED
, NSS_JOIN_SYNCING
, (void*)&OnKickPlayer
, context
);
709 session
->AddTransition(NSS_JOIN_SYNCING
, (uint
)NMT_CONNECTION_LOST
, NSS_UNCONNECTED
, (void*)&OnDisconnect
, context
);
710 session
->AddTransition(NSS_JOIN_SYNCING
, (uint
)NMT_LOADED_GAME
, NSS_INGAME
, (void*)&OnJoinSyncingLoadedGame
, context
);
712 session
->AddTransition(NSS_INGAME
, (uint
)NMT_REJOINED
, NSS_INGAME
, (void*)&OnRejoined
, context
);
713 session
->AddTransition(NSS_INGAME
, (uint
)NMT_KICKED
, NSS_INGAME
, (void*)&OnKickPlayer
, context
);
714 session
->AddTransition(NSS_INGAME
, (uint
)NMT_CLIENT_PAUSED
, NSS_INGAME
, (void*)&OnClientPaused
, context
);
715 session
->AddTransition(NSS_INGAME
, (uint
)NMT_CONNECTION_LOST
, NSS_UNCONNECTED
, (void*)&OnDisconnect
, context
);
716 session
->AddTransition(NSS_INGAME
, (uint
)NMT_CHAT
, NSS_INGAME
, (void*)&OnChat
, context
);
717 session
->AddTransition(NSS_INGAME
, (uint
)NMT_SIMULATION_COMMAND
, NSS_INGAME
, (void*)&OnSimulationCommand
, context
);
718 session
->AddTransition(NSS_INGAME
, (uint
)NMT_SYNC_CHECK
, NSS_INGAME
, (void*)&OnSyncCheck
, context
);
719 session
->AddTransition(NSS_INGAME
, (uint
)NMT_END_COMMAND_BATCH
, NSS_INGAME
, (void*)&OnEndCommandBatch
, context
);
722 session
->SetFirstState(NSS_HANDSHAKE
);
725 bool CNetServerWorker::HandleConnect(CNetServerSession
* session
)
727 if (std::find(m_BannedIPs
.begin(), m_BannedIPs
.end(), session
->GetIPAddress()) != m_BannedIPs
.end())
729 session
->Disconnect(NDR_BANNED
);
733 CSrvHandshakeMessage handshake
;
734 handshake
.m_Magic
= PS_PROTOCOL_MAGIC
;
735 handshake
.m_ProtocolVersion
= PS_PROTOCOL_VERSION
;
736 handshake
.m_SoftwareVersion
= PS_PROTOCOL_VERSION
;
737 return session
->SendMessage(&handshake
);
740 void CNetServerWorker::OnUserJoin(CNetServerSession
* session
)
742 AddPlayer(session
->GetGUID(), session
->GetUserName());
744 CPlayerAssignmentMessage assignMessage
;
745 ConstructPlayerAssignmentMessage(assignMessage
);
746 session
->SendMessage(&assignMessage
);
749 void CNetServerWorker::OnUserLeave(CNetServerSession
* session
)
751 std::vector
<CStr
>::iterator pausing
= std::find(m_PausingPlayers
.begin(), m_PausingPlayers
.end(), session
->GetGUID());
752 if (pausing
!= m_PausingPlayers
.end())
753 m_PausingPlayers
.erase(pausing
);
755 RemovePlayer(session
->GetGUID());
757 if (m_ServerTurnManager
&& session
->GetCurrState() != NSS_JOIN_SYNCING
)
758 m_ServerTurnManager
->UninitialiseClient(session
->GetHostID());
760 // TODO: ought to switch the player controlled by that client
761 // back to AI control, or something?
764 void CNetServerWorker::AddPlayer(const CStr
& guid
, const CStrW
& name
)
766 // Find all player IDs in active use; we mustn't give them to a second player (excluding the unassigned ID: -1)
767 std::set
<i32
> usedIDs
;
768 for (const std::pair
<const CStr
, PlayerAssignment
>& p
: m_PlayerAssignments
)
769 if (p
.second
.m_Enabled
&& p
.second
.m_PlayerID
!= -1)
770 usedIDs
.insert(p
.second
.m_PlayerID
);
772 // If the player is rejoining after disconnecting, try to give them
773 // back their old player ID
777 // Try to match GUID first
778 for (PlayerAssignmentMap::iterator it
= m_PlayerAssignments
.begin(); it
!= m_PlayerAssignments
.end(); ++it
)
780 if (!it
->second
.m_Enabled
&& it
->first
== guid
&& usedIDs
.find(it
->second
.m_PlayerID
) == usedIDs
.end())
782 playerID
= it
->second
.m_PlayerID
;
783 m_PlayerAssignments
.erase(it
); // delete the old mapping, since we've got a new one now
788 // Try to match username next
789 for (PlayerAssignmentMap::iterator it
= m_PlayerAssignments
.begin(); it
!= m_PlayerAssignments
.end(); ++it
)
791 if (!it
->second
.m_Enabled
&& it
->second
.m_Name
== name
&& usedIDs
.find(it
->second
.m_PlayerID
) == usedIDs
.end())
793 playerID
= it
->second
.m_PlayerID
;
794 m_PlayerAssignments
.erase(it
); // delete the old mapping, since we've got a new one now
799 // Otherwise leave the player ID as -1 (observer) and let gamesetup change it as needed.
802 PlayerAssignment assignment
;
803 assignment
.m_Enabled
= true;
804 assignment
.m_Name
= name
;
805 assignment
.m_PlayerID
= playerID
;
806 assignment
.m_Status
= 0;
807 m_PlayerAssignments
[guid
] = assignment
;
809 // Send the new assignments to all currently active players
810 // (which does not include the one that's just joining)
811 SendPlayerAssignments();
814 void CNetServerWorker::RemovePlayer(const CStr
& guid
)
816 m_PlayerAssignments
[guid
].m_Enabled
= false;
818 SendPlayerAssignments();
821 void CNetServerWorker::ClearAllPlayerReady()
823 for (std::pair
<const CStr
, PlayerAssignment
>& p
: m_PlayerAssignments
)
824 if (p
.second
.m_Status
!= 2)
825 p
.second
.m_Status
= 0;
827 SendPlayerAssignments();
830 void CNetServerWorker::KickPlayer(const CStrW
& playerName
, const bool ban
)
832 // Find the user with that name
833 std::vector
<CNetServerSession
*>::iterator it
= std::find_if(m_Sessions
.begin(), m_Sessions
.end(),
834 [&](CNetServerSession
* session
) { return session
->GetUserName() == playerName
; });
836 // and return if no one or the host has that name
837 if (it
== m_Sessions
.end() || (*it
)->GetGUID() == m_ControllerGUID
)
843 if (std::find(m_BannedPlayers
.begin(), m_BannedPlayers
.end(), playerName
) == m_BannedPlayers
.end())
844 m_BannedPlayers
.push_back(m_LobbyAuth
? CStrW(playerName
.substr(0, playerName
.find(L
" ("))) : playerName
);
846 // Remember IP address
847 u32 ipAddress
= (*it
)->GetIPAddress();
848 if (std::find(m_BannedIPs
.begin(), m_BannedIPs
.end(), ipAddress
) == m_BannedIPs
.end())
849 m_BannedIPs
.push_back(ipAddress
);
852 // Disconnect that user
853 (*it
)->Disconnect(ban
? NDR_BANNED
: NDR_KICKED
);
855 // Send message notifying other clients
856 CKickedMessage kickedMessage
;
857 kickedMessage
.m_Name
= playerName
;
858 kickedMessage
.m_Ban
= ban
;
859 Broadcast(&kickedMessage
, { NSS_PREGAME
, NSS_JOIN_SYNCING
, NSS_INGAME
});
862 void CNetServerWorker::AssignPlayer(int playerID
, const CStr
& guid
)
864 // Remove anyone who's already assigned to this player
865 for (std::pair
<const CStr
, PlayerAssignment
>& p
: m_PlayerAssignments
)
867 if (p
.second
.m_PlayerID
== playerID
)
868 p
.second
.m_PlayerID
= -1;
871 // Update this host's assignment if it exists
872 if (m_PlayerAssignments
.find(guid
) != m_PlayerAssignments
.end())
873 m_PlayerAssignments
[guid
].m_PlayerID
= playerID
;
875 SendPlayerAssignments();
878 void CNetServerWorker::ConstructPlayerAssignmentMessage(CPlayerAssignmentMessage
& message
)
880 for (const std::pair
<const CStr
, PlayerAssignment
>& p
: m_PlayerAssignments
)
882 if (!p
.second
.m_Enabled
)
885 CPlayerAssignmentMessage::S_m_Hosts h
;
887 h
.m_Name
= p
.second
.m_Name
;
888 h
.m_PlayerID
= p
.second
.m_PlayerID
;
889 h
.m_Status
= p
.second
.m_Status
;
890 message
.m_Hosts
.push_back(h
);
894 void CNetServerWorker::SendPlayerAssignments()
896 CPlayerAssignmentMessage message
;
897 ConstructPlayerAssignmentMessage(message
);
898 Broadcast(&message
, { NSS_PREGAME
, NSS_JOIN_SYNCING
, NSS_INGAME
});
901 const ScriptInterface
& CNetServerWorker::GetScriptInterface()
903 return *m_ScriptInterface
;
906 void CNetServerWorker::SetTurnLength(u32 msecs
)
908 if (m_ServerTurnManager
)
909 m_ServerTurnManager
->SetTurnLength(msecs
);
912 void CNetServerWorker::ProcessLobbyAuth(const CStr
& name
, const CStr
& token
)
914 LOGMESSAGE("Net Server: Received lobby auth message from %s with %s", name
, token
);
915 // Find the user with that guid
916 std::vector
<CNetServerSession
*>::iterator it
= std::find_if(m_Sessions
.begin(), m_Sessions
.end(),
917 [&](CNetServerSession
* session
)
918 { return session
->GetGUID() == token
; });
920 if (it
== m_Sessions
.end())
923 (*it
)->SetUserName(name
.FromUTF8());
924 // Send an empty message to request the authentication message from the client
925 // after its identity has been confirmed via the lobby
926 CAuthenticateMessage emptyMessage
;
927 (*it
)->SendMessage(&emptyMessage
);
930 bool CNetServerWorker::OnClientHandshake(void* context
, CFsmEvent
* event
)
932 ENSURE(event
->GetType() == (uint
)NMT_CLIENT_HANDSHAKE
);
934 CNetServerSession
* session
= (CNetServerSession
*)context
;
935 CNetServerWorker
& server
= session
->GetServer();
937 CCliHandshakeMessage
* message
= (CCliHandshakeMessage
*)event
->GetParamRef();
938 if (message
->m_ProtocolVersion
!= PS_PROTOCOL_VERSION
)
940 session
->Disconnect(NDR_INCORRECT_PROTOCOL_VERSION
);
944 CStr guid
= ps_generate_guid();
946 // Ensure unique GUID
948 server
.m_Sessions
.begin(), server
.m_Sessions
.end(),
949 [&guid
] (const CNetServerSession
* session
)
950 { return session
->GetGUID() == guid
; }) != server
.m_Sessions
.end())
954 session
->Disconnect(NDR_GUID_FAILED
);
957 guid
= ps_generate_guid();
960 session
->SetGUID(guid
);
962 CSrvHandshakeResponseMessage handshakeResponse
;
963 handshakeResponse
.m_UseProtocolVersion
= PS_PROTOCOL_VERSION
;
964 handshakeResponse
.m_GUID
= guid
;
965 handshakeResponse
.m_Flags
= 0;
967 if (server
.m_LobbyAuth
)
969 handshakeResponse
.m_Flags
|= PS_NETWORK_FLAG_REQUIRE_LOBBYAUTH
;
970 session
->SetNextState(NSS_LOBBY_AUTHENTICATE
);
973 session
->SendMessage(&handshakeResponse
);
978 bool CNetServerWorker::OnAuthenticate(void* context
, CFsmEvent
* event
)
980 ENSURE(event
->GetType() == (uint
)NMT_AUTHENTICATE
);
982 CNetServerSession
* session
= (CNetServerSession
*)context
;
983 CNetServerWorker
& server
= session
->GetServer();
985 // Prohibit joins while the game is loading
986 if (server
.m_State
== SERVER_STATE_LOADING
)
988 LOGMESSAGE("Refused connection while the game is loading");
989 session
->Disconnect(NDR_SERVER_LOADING
);
993 CAuthenticateMessage
* message
= (CAuthenticateMessage
*)event
->GetParamRef();
994 CStrW username
= SanitisePlayerName(message
->m_Name
);
995 CStrW
usernameWithoutRating(username
.substr(0, username
.find(L
" (")));
997 // Compare the lowercase names as specified by https://xmpp.org/extensions/xep-0029.html#sect-idm139493404168176
998 // "[...] comparisons will be made in case-normalized canonical form."
999 if (server
.m_LobbyAuth
&& usernameWithoutRating
.LowerCase() != session
->GetUserName().LowerCase())
1001 LOGERROR("Net server: lobby auth: %s tried joining as %s",
1002 session
->GetUserName().ToUTF8(),
1003 usernameWithoutRating
.ToUTF8());
1004 session
->Disconnect(NDR_LOBBY_AUTH_FAILED
);
1008 // Check the password before anything else.
1009 // NB: m_Name must match the client's salt, @see CNetClient::SetGamePassword
1010 if (!server
.CheckPassword(message
->m_Password
, message
->m_Name
.ToUTF8()))
1012 // Noisy logerror because players are not supposed to be able to get the IP,
1013 // so this might be someone targeting the host for some reason
1014 // (or TODO a dedicated server and we do want to log anyways)
1015 LOGERROR("Net server: user %s tried joining with the wrong password",
1016 session
->GetUserName().ToUTF8());
1017 session
->Disconnect(NDR_SERVER_REFUSED
);
1021 // Either deduplicate or prohibit join if name is in use
1022 bool duplicatePlayernames
= false;
1023 CFG_GET_VAL("network.duplicateplayernames", duplicatePlayernames
);
1024 // If lobby authentication is enabled, the clients playername has already been registered.
1025 // There also can't be any duplicated names.
1026 if (!server
.m_LobbyAuth
&& duplicatePlayernames
)
1027 username
= server
.DeduplicatePlayerName(username
);
1030 std::vector
<CNetServerSession
*>::iterator it
= std::find_if(
1031 server
.m_Sessions
.begin(), server
.m_Sessions
.end(),
1032 [&username
] (const CNetServerSession
* session
)
1033 { return session
->GetUserName() == username
; });
1035 if (it
!= server
.m_Sessions
.end() && (*it
) != session
)
1037 session
->Disconnect(NDR_PLAYERNAME_IN_USE
);
1042 // Disconnect banned usernames
1043 if (std::find(server
.m_BannedPlayers
.begin(), server
.m_BannedPlayers
.end(), server
.m_LobbyAuth
? usernameWithoutRating
: username
) != server
.m_BannedPlayers
.end())
1045 session
->Disconnect(NDR_BANNED
);
1049 int maxObservers
= 0;
1050 CFG_GET_VAL("network.observerlimit", maxObservers
);
1052 bool isRejoining
= false;
1053 bool serverFull
= false;
1054 if (server
.m_State
== SERVER_STATE_PREGAME
)
1056 // Don't check for maxObservers in the gamesetup, as we don't know yet who will be assigned
1057 serverFull
= server
.m_Sessions
.size() >= MAX_CLIENTS
;
1061 bool isObserver
= true;
1062 int disconnectedPlayers
= 0;
1063 int connectedPlayers
= 0;
1064 // (TODO: if GUIDs were stable, we should use them instead)
1065 for (const std::pair
<const CStr
, PlayerAssignment
>& p
: server
.m_PlayerAssignments
)
1067 const PlayerAssignment
& assignment
= p
.second
;
1069 if (!assignment
.m_Enabled
&& assignment
.m_Name
== username
)
1071 isObserver
= assignment
.m_PlayerID
== -1;
1075 if (assignment
.m_PlayerID
== -1)
1078 if (assignment
.m_Enabled
)
1081 ++disconnectedPlayers
;
1084 // Optionally allow everyone or only buddies to join after the game has started
1087 CStr observerLateJoin
;
1088 CFG_GET_VAL("network.lateobservers", observerLateJoin
);
1090 if (observerLateJoin
== "everyone")
1094 else if (observerLateJoin
== "buddies")
1097 CFG_GET_VAL("lobby.buddies", buddies
);
1098 std::wstringstream
buddiesStream(wstring_from_utf8(buddies
));
1100 while (std::getline(buddiesStream
, buddy
, L
','))
1102 if (buddy
== usernameWithoutRating
)
1113 LOGMESSAGE("Refused connection after game start from not-previously-known user \"%s\"", utf8_from_wstring(username
));
1114 session
->Disconnect(NDR_SERVER_ALREADY_IN_GAME
);
1118 // Ensure all players will be able to rejoin
1119 serverFull
= isObserver
&& (
1120 (int) server
.m_Sessions
.size() - connectedPlayers
> maxObservers
||
1121 (int) server
.m_Sessions
.size() + disconnectedPlayers
>= MAX_CLIENTS
);
1126 session
->Disconnect(NDR_SERVER_FULL
);
1130 u32 newHostID
= server
.m_NextHostID
++;
1132 session
->SetUserName(username
);
1133 session
->SetHostID(newHostID
);
1135 CAuthenticateResultMessage authenticateResult
;
1136 authenticateResult
.m_Code
= isRejoining
? ARC_OK_REJOINING
: ARC_OK
;
1137 authenticateResult
.m_HostID
= newHostID
;
1138 authenticateResult
.m_Message
= L
"Logged in";
1139 authenticateResult
.m_IsController
= 0;
1141 if (message
->m_ControllerSecret
== server
.m_ControllerSecret
)
1143 if (server
.m_ControllerGUID
.empty())
1145 server
.m_ControllerGUID
= session
->GetGUID();
1146 authenticateResult
.m_IsController
= 1;
1148 // TODO: we could probably handle having several controllers, or swapping?
1151 session
->SendMessage(&authenticateResult
);
1153 server
.OnUserJoin(session
);
1157 ENSURE(server
.m_State
!= SERVER_STATE_UNCONNECTED
&& server
.m_State
!= SERVER_STATE_PREGAME
);
1159 // Request a copy of the current game state from an existing player,
1160 // so we can send it on to the new player
1162 // Assume session 0 is most likely the local player, so they're
1163 // the most efficient client to request a copy from
1164 CNetServerSession
* sourceSession
= server
.m_Sessions
.at(0);
1166 sourceSession
->GetFileTransferer().StartTask(
1167 shared_ptr
<CNetFileReceiveTask
>(new CNetFileReceiveTask_ServerRejoin(server
, newHostID
))
1170 session
->SetNextState(NSS_JOIN_SYNCING
);
1175 bool CNetServerWorker::OnSimulationCommand(void* context
, CFsmEvent
* event
)
1177 ENSURE(event
->GetType() == (uint
)NMT_SIMULATION_COMMAND
);
1179 CNetServerSession
* session
= (CNetServerSession
*)context
;
1180 CNetServerWorker
& server
= session
->GetServer();
1182 CSimulationMessage
* message
= (CSimulationMessage
*)event
->GetParamRef();
1184 // Ignore messages sent by one player on behalf of another player
1185 // unless cheating is enabled
1186 bool cheatsEnabled
= false;
1187 const ScriptInterface
& scriptInterface
= server
.GetScriptInterface();
1188 ScriptRequest
rq(scriptInterface
);
1189 JS::RootedValue
settings(rq
.cx
);
1190 Script::GetProperty(rq
, server
.m_InitAttributes
, "settings", &settings
);
1191 if (Script::HasProperty(rq
, settings
, "CheatsEnabled"))
1192 Script::GetProperty(rq
, settings
, "CheatsEnabled", cheatsEnabled
);
1194 PlayerAssignmentMap::iterator it
= server
.m_PlayerAssignments
.find(session
->GetGUID());
1195 // When cheating is disabled, fail if the player the message claims to
1196 // represent does not exist or does not match the sender's player name
1197 if (!cheatsEnabled
&& (it
== server
.m_PlayerAssignments
.end() || it
->second
.m_PlayerID
!= message
->m_Player
))
1200 // Send it back to all clients that have finished
1201 // the loading screen (and the synchronization when rejoining)
1202 server
.Broadcast(message
, { NSS_INGAME
});
1204 // Save all the received commands
1205 if (server
.m_SavedCommands
.size() < message
->m_Turn
+ 1)
1206 server
.m_SavedCommands
.resize(message
->m_Turn
+ 1);
1207 server
.m_SavedCommands
[message
->m_Turn
].push_back(*message
);
1209 // TODO: we shouldn't send the message back to the client that first sent it
1213 bool CNetServerWorker::OnSyncCheck(void* context
, CFsmEvent
* event
)
1215 ENSURE(event
->GetType() == (uint
)NMT_SYNC_CHECK
);
1217 CNetServerSession
* session
= (CNetServerSession
*)context
;
1218 CNetServerWorker
& server
= session
->GetServer();
1220 CSyncCheckMessage
* message
= (CSyncCheckMessage
*)event
->GetParamRef();
1222 server
.m_ServerTurnManager
->NotifyFinishedClientUpdate(*session
, message
->m_Turn
, message
->m_Hash
);
1226 bool CNetServerWorker::OnEndCommandBatch(void* context
, CFsmEvent
* event
)
1228 ENSURE(event
->GetType() == (uint
)NMT_END_COMMAND_BATCH
);
1230 CNetServerSession
* session
= (CNetServerSession
*)context
;
1231 CNetServerWorker
& server
= session
->GetServer();
1233 CEndCommandBatchMessage
* message
= (CEndCommandBatchMessage
*)event
->GetParamRef();
1235 // The turn-length field is ignored
1236 server
.m_ServerTurnManager
->NotifyFinishedClientCommands(*session
, message
->m_Turn
);
1240 bool CNetServerWorker::OnChat(void* context
, CFsmEvent
* event
)
1242 ENSURE(event
->GetType() == (uint
)NMT_CHAT
);
1244 CNetServerSession
* session
= (CNetServerSession
*)context
;
1245 CNetServerWorker
& server
= session
->GetServer();
1247 CChatMessage
* message
= (CChatMessage
*)event
->GetParamRef();
1249 message
->m_GUID
= session
->GetGUID();
1251 server
.Broadcast(message
, { NSS_PREGAME
, NSS_INGAME
});
1256 bool CNetServerWorker::OnReady(void* context
, CFsmEvent
* event
)
1258 ENSURE(event
->GetType() == (uint
)NMT_READY
);
1260 CNetServerSession
* session
= (CNetServerSession
*)context
;
1261 CNetServerWorker
& server
= session
->GetServer();
1263 // Occurs if a client presses not-ready
1264 // in the very last moment before the hosts starts the game
1265 if (server
.m_State
== SERVER_STATE_LOADING
)
1268 CReadyMessage
* message
= (CReadyMessage
*)event
->GetParamRef();
1269 message
->m_GUID
= session
->GetGUID();
1270 server
.Broadcast(message
, { NSS_PREGAME
});
1272 server
.m_PlayerAssignments
[message
->m_GUID
].m_Status
= message
->m_Status
;
1277 bool CNetServerWorker::OnClearAllReady(void* context
, CFsmEvent
* event
)
1279 ENSURE(event
->GetType() == (uint
)NMT_CLEAR_ALL_READY
);
1281 CNetServerSession
* session
= (CNetServerSession
*)context
;
1282 CNetServerWorker
& server
= session
->GetServer();
1284 if (session
->GetGUID() == server
.m_ControllerGUID
)
1285 server
.ClearAllPlayerReady();
1290 bool CNetServerWorker::OnGameSetup(void* context
, CFsmEvent
* event
)
1292 ENSURE(event
->GetType() == (uint
)NMT_GAME_SETUP
);
1294 CNetServerSession
* session
= (CNetServerSession
*)context
;
1295 CNetServerWorker
& server
= session
->GetServer();
1297 // Changing the settings after gamestart is not implemented and would cause an Out-of-sync error.
1298 // This happened when doubleclicking on the startgame button.
1299 if (server
.m_State
!= SERVER_STATE_PREGAME
)
1302 // Only the controller is allowed to send game setup updates.
1303 // TODO: it would be good to allow other players to request changes to some settings,
1304 // e.g. their civilisation.
1305 // Possibly this should use another message, to enforce a single source of truth.
1306 if (session
->GetGUID() == server
.m_ControllerGUID
)
1308 CGameSetupMessage
* message
= (CGameSetupMessage
*)event
->GetParamRef();
1309 server
.Broadcast(message
, { NSS_PREGAME
});
1314 bool CNetServerWorker::OnAssignPlayer(void* context
, CFsmEvent
* event
)
1316 ENSURE(event
->GetType() == (uint
)NMT_ASSIGN_PLAYER
);
1317 CNetServerSession
* session
= (CNetServerSession
*)context
;
1318 CNetServerWorker
& server
= session
->GetServer();
1320 if (session
->GetGUID() == server
.m_ControllerGUID
)
1322 CAssignPlayerMessage
* message
= (CAssignPlayerMessage
*)event
->GetParamRef();
1323 server
.AssignPlayer(message
->m_PlayerID
, message
->m_GUID
);
1328 bool CNetServerWorker::OnGameStart(void* context
, CFsmEvent
* event
)
1330 ENSURE(event
->GetType() == (uint
)NMT_GAME_START
);
1331 CNetServerSession
* session
= (CNetServerSession
*)context
;
1332 CNetServerWorker
& server
= session
->GetServer();
1334 if (session
->GetGUID() != server
.m_ControllerGUID
)
1337 CGameStartMessage
* message
= (CGameStartMessage
*)event
->GetParamRef();
1338 server
.StartGame(message
->m_InitAttributes
);
1342 bool CNetServerWorker::OnLoadedGame(void* context
, CFsmEvent
* event
)
1344 ENSURE(event
->GetType() == (uint
)NMT_LOADED_GAME
);
1346 CNetServerSession
* loadedSession
= (CNetServerSession
*)context
;
1347 CNetServerWorker
& server
= loadedSession
->GetServer();
1349 // We're in the loading state, so wait until every client has loaded
1350 // before starting the game
1351 ENSURE(server
.m_State
== SERVER_STATE_LOADING
);
1352 if (server
.CheckGameLoadStatus(loadedSession
))
1355 CClientsLoadingMessage message
;
1356 // We always send all GUIDs of clients in the loading state
1357 // so that we don't have to bother about switching GUI pages
1358 for (CNetServerSession
* session
: server
.m_Sessions
)
1359 if (session
->GetCurrState() != NSS_INGAME
&& loadedSession
->GetGUID() != session
->GetGUID())
1361 CClientsLoadingMessage::S_m_Clients client
;
1362 client
.m_GUID
= session
->GetGUID();
1363 message
.m_Clients
.push_back(client
);
1366 // Send to the client who has loaded the game but did not reach the NSS_INGAME state yet
1367 loadedSession
->SendMessage(&message
);
1368 server
.Broadcast(&message
, { NSS_INGAME
});
1373 bool CNetServerWorker::OnJoinSyncingLoadedGame(void* context
, CFsmEvent
* event
)
1375 // A client rejoining an in-progress game has now finished loading the
1376 // map and deserialized the initial state.
1377 // The simulation may have progressed since then, so send any subsequent
1378 // commands to them and set them as an active player so they can participate
1379 // in all future turns.
1381 // (TODO: if it takes a long time for them to receive and execute all these
1382 // commands, the other players will get frozen for that time and may be unhappy;
1383 // we could try repeating this process a few times until the client converges
1384 // on the up-to-date state, before setting them as active.)
1386 ENSURE(event
->GetType() == (uint
)NMT_LOADED_GAME
);
1388 CNetServerSession
* session
= (CNetServerSession
*)context
;
1389 CNetServerWorker
& server
= session
->GetServer();
1391 CLoadedGameMessage
* message
= (CLoadedGameMessage
*)event
->GetParamRef();
1393 u32 turn
= message
->m_CurrentTurn
;
1394 u32 readyTurn
= server
.m_ServerTurnManager
->GetReadyTurn();
1396 // Send them all commands received since their saved state,
1397 // and turn-ended messages for any turns that have already been processed
1398 for (size_t i
= turn
+ 1; i
< std::max(readyTurn
+1, (u32
)server
.m_SavedCommands
.size()); ++i
)
1400 if (i
< server
.m_SavedCommands
.size())
1401 for (size_t j
= 0; j
< server
.m_SavedCommands
[i
].size(); ++j
)
1402 session
->SendMessage(&server
.m_SavedCommands
[i
][j
]);
1406 CEndCommandBatchMessage endMessage
;
1407 endMessage
.m_Turn
= i
;
1408 endMessage
.m_TurnLength
= server
.m_ServerTurnManager
->GetSavedTurnLength(i
);
1409 session
->SendMessage(&endMessage
);
1413 // Tell the turn manager to expect commands from this new client
1414 // Special case: the controller shouldn't be treated as an observer in any case.
1415 bool isObserver
= server
.m_PlayerAssignments
[session
->GetGUID()].m_PlayerID
== -1 && server
.m_ControllerGUID
!= session
->GetGUID();
1416 server
.m_ServerTurnManager
->InitialiseClient(session
->GetHostID(), readyTurn
, isObserver
);
1418 // Tell the client that everything has finished loading and it should start now
1419 CLoadedGameMessage loaded
;
1420 loaded
.m_CurrentTurn
= readyTurn
;
1421 session
->SendMessage(&loaded
);
1426 bool CNetServerWorker::OnRejoined(void* context
, CFsmEvent
* event
)
1428 // A client has finished rejoining and the loading screen disappeared.
1429 ENSURE(event
->GetType() == (uint
)NMT_REJOINED
);
1431 CNetServerSession
* session
= (CNetServerSession
*)context
;
1432 CNetServerWorker
& server
= session
->GetServer();
1434 // Inform everyone of the client having rejoined
1435 CRejoinedMessage
* message
= (CRejoinedMessage
*)event
->GetParamRef();
1436 message
->m_GUID
= session
->GetGUID();
1437 server
.Broadcast(message
, { NSS_INGAME
});
1439 // Send all pausing players to the rejoined client.
1440 for (const CStr
& guid
: server
.m_PausingPlayers
)
1442 CClientPausedMessage pausedMessage
;
1443 pausedMessage
.m_GUID
= guid
;
1444 pausedMessage
.m_Pause
= true;
1445 session
->SendMessage(&pausedMessage
);
1451 bool CNetServerWorker::OnKickPlayer(void* context
, CFsmEvent
* event
)
1453 ENSURE(event
->GetType() == (uint
)NMT_KICKED
);
1455 CNetServerSession
* session
= (CNetServerSession
*)context
;
1456 CNetServerWorker
& server
= session
->GetServer();
1458 if (session
->GetGUID() == server
.m_ControllerGUID
)
1460 CKickedMessage
* message
= (CKickedMessage
*)event
->GetParamRef();
1461 server
.KickPlayer(message
->m_Name
, message
->m_Ban
);
1466 bool CNetServerWorker::OnDisconnect(void* context
, CFsmEvent
* event
)
1468 ENSURE(event
->GetType() == (uint
)NMT_CONNECTION_LOST
);
1470 CNetServerSession
* session
= (CNetServerSession
*)context
;
1471 CNetServerWorker
& server
= session
->GetServer();
1473 server
.OnUserLeave(session
);
1478 bool CNetServerWorker::OnClientPaused(void* context
, CFsmEvent
* event
)
1480 ENSURE(event
->GetType() == (uint
)NMT_CLIENT_PAUSED
);
1482 CNetServerSession
* session
= (CNetServerSession
*)context
;
1483 CNetServerWorker
& server
= session
->GetServer();
1485 CClientPausedMessage
* message
= (CClientPausedMessage
*)event
->GetParamRef();
1487 message
->m_GUID
= session
->GetGUID();
1489 // Update the list of pausing players.
1490 std::vector
<CStr
>::iterator player
= std::find(server
.m_PausingPlayers
.begin(), server
.m_PausingPlayers
.end(), session
->GetGUID());
1492 if (message
->m_Pause
)
1494 if (player
!= server
.m_PausingPlayers
.end())
1497 server
.m_PausingPlayers
.push_back(session
->GetGUID());
1501 if (player
== server
.m_PausingPlayers
.end())
1504 server
.m_PausingPlayers
.erase(player
);
1507 // Send messages to clients that are in game, and are not the client who paused.
1508 for (CNetServerSession
* netSession
: server
.m_Sessions
)
1509 if (netSession
->GetCurrState() == NSS_INGAME
&& message
->m_GUID
!= netSession
->GetGUID())
1510 netSession
->SendMessage(message
);
1515 bool CNetServerWorker::CheckGameLoadStatus(CNetServerSession
* changedSession
)
1517 for (const CNetServerSession
* session
: m_Sessions
)
1518 if (session
!= changedSession
&& session
->GetCurrState() != NSS_INGAME
)
1521 // Inform clients that everyone has loaded the map and that the game can start
1522 CLoadedGameMessage loaded
;
1523 loaded
.m_CurrentTurn
= 0;
1525 // Notice the changedSession is still in the NSS_PREGAME state
1526 Broadcast(&loaded
, { NSS_PREGAME
, NSS_INGAME
});
1528 m_State
= SERVER_STATE_INGAME
;
1532 void CNetServerWorker::StartGame(const CStr
& initAttribs
)
1534 for (std::pair
<const CStr
, PlayerAssignment
>& player
: m_PlayerAssignments
)
1535 if (player
.second
.m_Enabled
&& player
.second
.m_PlayerID
!= -1 && player
.second
.m_Status
== 0)
1537 LOGERROR("Tried to start the game without player \"%s\" being ready!", utf8_from_wstring(player
.second
.m_Name
).c_str());
1541 m_ServerTurnManager
= new CNetServerTurnManager(*this);
1543 for (CNetServerSession
* session
: m_Sessions
)
1545 // Special case: the controller shouldn't be treated as an observer in any case.
1546 bool isObserver
= m_PlayerAssignments
[session
->GetGUID()].m_PlayerID
== -1 && m_ControllerGUID
!= session
->GetGUID();
1547 m_ServerTurnManager
->InitialiseClient(session
->GetHostID(), 0, isObserver
);
1550 m_State
= SERVER_STATE_LOADING
;
1552 // Remove players and observers that are not present when the game starts
1553 for (PlayerAssignmentMap::iterator it
= m_PlayerAssignments
.begin(); it
!= m_PlayerAssignments
.end();)
1554 if (it
->second
.m_Enabled
)
1557 it
= m_PlayerAssignments
.erase(it
);
1559 SendPlayerAssignments();
1561 // Update init attributes. They should no longer change.
1562 Script::ParseJSON(ScriptRequest(m_ScriptInterface
), initAttribs
, &m_InitAttributes
);
1564 CGameStartMessage gameStart
;
1565 gameStart
.m_InitAttributes
= initAttribs
;
1566 Broadcast(&gameStart
, { NSS_PREGAME
});
1569 CStrW
CNetServerWorker::SanitisePlayerName(const CStrW
& original
)
1571 const size_t MAX_LENGTH
= 32;
1573 CStrW name
= original
;
1574 name
.Replace(L
"[", L
"{"); // remove GUI tags
1575 name
.Replace(L
"]", L
"}"); // remove for symmetry
1577 // Restrict the length
1578 if (name
.length() > MAX_LENGTH
)
1579 name
= name
.Left(MAX_LENGTH
);
1581 // Don't allow surrounding whitespace
1582 name
.Trim(PS_TRIM_BOTH
);
1584 // Don't allow empty name
1586 name
= L
"Anonymous";
1591 CStrW
CNetServerWorker::DeduplicatePlayerName(const CStrW
& original
)
1593 CStrW name
= original
;
1595 // Try names "Foo", "Foo (2)", "Foo (3)", etc
1600 for (const CNetServerSession
* session
: m_Sessions
)
1602 if (session
->GetUserName() == name
)
1612 name
= original
+ L
" (" + CStrW::FromUInt(id
++) + L
")";
1616 void CNetServerWorker::SendHolePunchingMessage(const CStr
& ipStr
, u16 port
)
1619 StunClient::SendHolePunchingMessages(*m_Host
, ipStr
, port
);
1625 CNetServer::CNetServer(bool useLobbyAuth
, int autostartPlayers
) :
1626 m_Worker(new CNetServerWorker(useLobbyAuth
, autostartPlayers
)),
1627 m_LobbyAuth(useLobbyAuth
), m_UseSTUN(false), m_PublicIp(""), m_PublicPort(20595), m_Password()
1631 CNetServer::~CNetServer()
1636 bool CNetServer::GetUseSTUN() const
1641 bool CNetServer::UseLobbyAuth() const
1646 bool CNetServer::SetupConnection(const u16 port
)
1648 return m_Worker
->SetupConnection(port
);
1651 CStr
CNetServer::GetPublicIp() const
1656 u16
CNetServer::GetPublicPort() const
1658 return m_PublicPort
;
1661 u16
CNetServer::GetLocalPort() const
1663 std::lock_guard
<std::mutex
> lock(m_Worker
->m_WorkerMutex
);
1664 if (!m_Worker
->m_Host
)
1666 return m_Worker
->m_Host
->address
.port
;
1669 void CNetServer::SetConnectionData(const CStr
& ip
, const u16 port
)
1672 m_PublicPort
= port
;
1676 bool CNetServer::SetConnectionDataViaSTUN()
1679 std::lock_guard
<std::mutex
> lock(m_Worker
->m_WorkerMutex
);
1680 if (!m_Worker
->m_Host
)
1682 return StunClient::FindPublicIP(*m_Worker
->m_Host
, m_PublicIp
, m_PublicPort
);
1685 bool CNetServer::CheckPasswordAndIncrement(const std::string
& username
, const std::string
& password
, const std::string
& salt
)
1687 std::unordered_map
<std::string
, int>::iterator it
= m_FailedAttempts
.find(username
);
1688 if (m_Worker
->CheckPassword(password
, salt
))
1690 if (it
!= m_FailedAttempts
.end())
1694 if (it
== m_FailedAttempts
.end())
1695 m_FailedAttempts
.emplace(username
, 1);
1701 bool CNetServer::IsBanned(const std::string
& username
) const
1703 std::unordered_map
<std::string
, int>::const_iterator it
= m_FailedAttempts
.find(username
);
1704 return it
!= m_FailedAttempts
.end() && it
->second
>= FAILED_PASSWORD_TRIES_BEFORE_BAN
;
1707 void CNetServer::SetPassword(const CStr
& password
)
1709 m_Password
= password
;
1710 std::lock_guard
<std::mutex
> lock(m_Worker
->m_WorkerMutex
);
1711 m_Worker
->SetPassword(password
);
1714 void CNetServer::SetControllerSecret(const std::string
& secret
)
1716 std::lock_guard
<std::mutex
> lock(m_Worker
->m_WorkerMutex
);
1717 m_Worker
->SetControllerSecret(secret
);
1720 void CNetServer::StartGame()
1722 std::lock_guard
<std::mutex
> lock(m_Worker
->m_WorkerMutex
);
1723 m_Worker
->m_StartGameQueue
.push_back(true);
1726 void CNetServer::UpdateInitAttributes(JS::MutableHandleValue attrs
, const ScriptRequest
& rq
)
1728 // Pass the attributes as JSON, since that's the easiest safe
1729 // cross-thread way of passing script data
1730 std::string attrsJSON
= Script::StringifyJSON(rq
, attrs
, false);
1732 std::lock_guard
<std::mutex
> lock(m_Worker
->m_WorkerMutex
);
1733 m_Worker
->m_InitAttributesQueue
.push_back(attrsJSON
);
1736 void CNetServer::OnLobbyAuth(const CStr
& name
, const CStr
& token
)
1738 std::lock_guard
<std::mutex
> lock(m_Worker
->m_WorkerMutex
);
1739 m_Worker
->m_LobbyAuthQueue
.push_back(std::make_pair(name
, token
));
1742 void CNetServer::SetTurnLength(u32 msecs
)
1744 std::lock_guard
<std::mutex
> lock(m_Worker
->m_WorkerMutex
);
1745 m_Worker
->m_TurnLengthQueue
.push_back(msecs
);
1748 void CNetServer::SendHolePunchingMessage(const CStr
& ip
, u16 port
)
1750 m_Worker
->SendHolePunchingMessage(ip
, port
);