1 /* Copyright (C) 2018 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/Profile.h"
35 #include "ps/ThreadUtil.h"
36 #include "scriptinterface/ScriptInterface.h"
37 #include "scriptinterface/ScriptRuntime.h"
38 #include "simulation2/Simulation2.h"
39 #include "simulation2/system/TurnManager.h"
42 #include <miniupnpc/miniwget.h>
43 #include <miniupnpc/miniupnpc.h>
44 #include <miniupnpc/upnpcommands.h>
45 #include <miniupnpc/upnperrors.h>
51 * Number of peers to allocate for the enet host.
52 * Limited by ENET_PROTOCOL_MAXIMUM_PEER_ID (4096).
54 * At most 8 players, 32 observers and 1 temporary connection to send the "server full" disconnect-reason.
56 #define MAX_CLIENTS 41
58 #define DEFAULT_SERVER_NAME L"Unnamed Server"
60 static const int CHANNEL_COUNT
= 1;
63 * enet_host_service timeout (msecs).
64 * Smaller numbers may hurt performance; larger numbers will
65 * hurt latency responding to messages from game thread.
67 static const int HOST_SERVICE_TIMEOUT
= 50;
69 CNetServer
* g_NetServer
= NULL
;
71 static CStr
DebugName(CNetServerSession
* session
)
74 return "[unknown host]";
75 if (session
->GetGUID().empty())
76 return "[unauthed host]";
77 return "[" + session
->GetGUID().substr(0, 8) + "...]";
81 * Async task for receiving the initial game state to be forwarded to another
82 * client that is rejoining an in-progress network game.
84 class CNetFileReceiveTask_ServerRejoin
: public CNetFileReceiveTask
86 NONCOPYABLE(CNetFileReceiveTask_ServerRejoin
);
88 CNetFileReceiveTask_ServerRejoin(CNetServerWorker
& server
, u32 hostID
)
89 : m_Server(server
), m_RejoinerHostID(hostID
)
93 virtual void OnComplete()
95 // We've received the game state from an existing player - now
96 // we need to send it onwards to the newly rejoining player
98 // Find the session corresponding to the rejoining host (if any)
99 CNetServerSession
* session
= NULL
;
100 for (CNetServerSession
* serverSession
: m_Server
.m_Sessions
)
102 if (serverSession
->GetHostID() == m_RejoinerHostID
)
104 session
= serverSession
;
111 LOGMESSAGE("Net server: rejoining client disconnected before we sent to it");
115 // Store the received state file, and tell the client to start downloading it from us
116 // TODO: this will get kind of confused if there's multiple clients downloading in parallel;
117 // they'll race and get whichever happens to be the latest received by the server,
118 // which should still work but isn't great
119 m_Server
.m_JoinSyncFile
= m_Buffer
;
120 CJoinSyncStartMessage message
;
121 session
->SendMessage(&message
);
125 CNetServerWorker
& m_Server
;
126 u32 m_RejoinerHostID
;
130 * XXX: We use some non-threadsafe functions from the worker thread.
131 * See http://trac.wildfiregames.com/ticket/654
134 CNetServerWorker::CNetServerWorker(bool useLobbyAuth
, int autostartPlayers
) :
135 m_AutostartPlayers(autostartPlayers
),
136 m_LobbyAuth(useLobbyAuth
),
138 m_ScriptInterface(NULL
),
139 m_NextHostID(1), m_Host(NULL
), m_HostGUID(), m_Stats(NULL
),
140 m_LastConnectionCheck(0)
142 m_State
= SERVER_STATE_UNCONNECTED
;
144 m_ServerTurnManager
= NULL
;
146 m_ServerName
= DEFAULT_SERVER_NAME
;
149 CNetServerWorker::~CNetServerWorker()
151 if (m_State
!= SERVER_STATE_UNCONNECTED
)
153 // Tell the thread to shut down
155 CScopeLock
lock(m_WorkerMutex
);
159 // Wait for it to shut down cleanly
160 pthread_join(m_WorkerThread
, NULL
);
163 // Clean up resources
167 for (CNetServerSession
* session
: m_Sessions
)
169 session
->DisconnectNow(NDR_SERVER_SHUTDOWN
);
174 enet_host_destroy(m_Host
);
176 delete m_ServerTurnManager
;
179 bool CNetServerWorker::SetupConnection(const u16 port
)
181 ENSURE(m_State
== SERVER_STATE_UNCONNECTED
);
184 // Bind to default host
186 addr
.host
= ENET_HOST_ANY
;
189 // Create ENet server
190 m_Host
= enet_host_create(&addr
, MAX_CLIENTS
, CHANNEL_COUNT
, 0, 0);
193 LOGERROR("Net server: enet_host_create failed");
197 m_Stats
= new CNetStatsTable();
198 if (CProfileViewer::IsInitialised())
199 g_ProfileViewer
.AddRootTable(m_Stats
);
201 m_State
= SERVER_STATE_PREGAME
;
203 // Launch the worker thread
204 int ret
= pthread_create(&m_WorkerThread
, NULL
, &RunThread
, this);
207 #if CONFIG2_MINIUPNPC
208 // Launch the UPnP thread
209 ret
= pthread_create(&m_UPnPThread
, NULL
, &SetupUPnP
, NULL
);
216 #if CONFIG2_MINIUPNPC
217 void* CNetServerWorker::SetupUPnP(void*)
219 // Values we want to set.
221 sprintf_s(psPort
, ARRAY_SIZE(psPort
), "%d", PS_DEFAULT_PORT
);
222 const char* leaseDuration
= "0"; // Indefinite/permanent lease duration.
223 const char* description
= "0AD Multiplayer";
224 const char* protocall
= "UDP";
225 char internalIPAddress
[64];
226 char externalIPAddress
[40];
227 // Variables to hold the values that actually get set.
231 // Intermediate variables.
232 struct UPNPUrls urls
;
233 struct IGDdatas data
;
234 struct UPNPDev
* devlist
= NULL
;
236 // Cached root descriptor URL.
237 std::string rootDescURL
;
238 CFG_GET_VAL("network.upnprootdescurl", rootDescURL
);
239 if (!rootDescURL
.empty())
240 LOGMESSAGE("Net server: attempting to use cached root descriptor URL: %s", rootDescURL
.c_str());
243 bool allocatedUrls
= false;
245 // Try a cached URL first
246 if (!rootDescURL
.empty() && UPNP_GetIGDFromUrl(rootDescURL
.c_str(), &urls
, &data
, internalIPAddress
, sizeof(internalIPAddress
)))
248 LOGMESSAGE("Net server: using cached IGD = %s", urls
.controlURL
);
251 // No cached URL, or it did not respond. Try getting a valid UPnP device for 10 seconds.
252 #if defined(MINIUPNPC_API_VERSION) && MINIUPNPC_API_VERSION >= 14
253 else if ((devlist
= upnpDiscover(10000, 0, 0, 0, 0, 2, 0)) != NULL
)
255 else if ((devlist
= upnpDiscover(10000, 0, 0, 0, 0, 0)) != NULL
)
258 ret
= UPNP_GetValidIGD(devlist
, &urls
, &data
, internalIPAddress
, sizeof(internalIPAddress
));
259 allocatedUrls
= ret
!= 0; // urls is allocated on non-zero return values
263 LOGMESSAGE("Net server: upnpDiscover failed and no working cached URL.");
270 LOGMESSAGE("Net server: No IGD found");
273 LOGMESSAGE("Net server: found valid IGD = %s", urls
.controlURL
);
276 LOGMESSAGE("Net server: found a valid, not connected IGD = %s, will try to continue anyway", urls
.controlURL
);
279 LOGMESSAGE("Net server: found a UPnP device unrecognized as IGD = %s, will try to continue anyway", urls
.controlURL
);
282 debug_warn(L
"Unrecognized return value from UPNP_GetValidIGD");
285 // Try getting our external/internet facing IP. TODO: Display this on the game-setup page for conviniance.
286 ret
= UPNP_GetExternalIPAddress(urls
.controlURL
, data
.first
.servicetype
, externalIPAddress
);
287 if (ret
!= UPNPCOMMAND_SUCCESS
)
289 LOGMESSAGE("Net server: GetExternalIPAddress failed with code %d (%s)", ret
, strupnperror(ret
));
292 LOGMESSAGE("Net server: ExternalIPAddress = %s", externalIPAddress
);
294 // Try to setup port forwarding.
295 ret
= UPNP_AddPortMapping(urls
.controlURL
, data
.first
.servicetype
, psPort
, psPort
,
296 internalIPAddress
, description
, protocall
, 0, leaseDuration
);
297 if (ret
!= UPNPCOMMAND_SUCCESS
)
299 LOGMESSAGE("Net server: AddPortMapping(%s, %s, %s) failed with code %d (%s)",
300 psPort
, psPort
, internalIPAddress
, ret
, strupnperror(ret
));
304 // Check that the port was actually forwarded.
305 ret
= UPNP_GetSpecificPortMappingEntry(urls
.controlURL
,
306 data
.first
.servicetype
,
308 #if defined(MINIUPNPC_API_VERSION) && MINIUPNPC_API_VERSION >= 10
311 intClient
, intPort
, NULL
/*desc*/,
312 NULL
/*enabled*/, duration
);
314 if (ret
!= UPNPCOMMAND_SUCCESS
)
316 LOGMESSAGE("Net server: GetSpecificPortMappingEntry() failed with code %d (%s)", ret
, strupnperror(ret
));
320 LOGMESSAGE("Net server: External %s:%s %s is redirected to internal %s:%s (duration=%s)",
321 externalIPAddress
, psPort
, protocall
, intClient
, intPort
, duration
);
323 // Cache root descriptor URL to try to avoid discovery next time.
324 g_ConfigDB
.SetValueString(CFG_USER
, "network.upnprootdescurl", urls
.controlURL
);
325 g_ConfigDB
.WriteValueToFile(CFG_USER
, "network.upnprootdescurl", urls
.controlURL
);
326 LOGMESSAGE("Net server: cached UPnP root descriptor URL as %s", urls
.controlURL
);
328 // Make sure everything is properly freed.
332 freeUPNPDevlist(devlist
);
336 #endif // CONFIG2_MINIUPNPC
338 bool CNetServerWorker::SendMessage(ENetPeer
* peer
, const CNetMessage
* message
)
342 CNetServerSession
* session
= static_cast<CNetServerSession
*>(peer
->data
);
344 return CNetHost::SendMessage(message
, peer
, DebugName(session
).c_str());
347 bool CNetServerWorker::Broadcast(const CNetMessage
* message
, const std::vector
<NetServerSessionState
>& targetStates
)
353 // TODO: this does lots of repeated message serialisation if we have lots
354 // of remote peers; could do it more efficiently if that's a real problem
356 for (CNetServerSession
* session
: m_Sessions
)
357 if (std::find(targetStates
.begin(), targetStates
.end(), session
->GetCurrState()) != targetStates
.end() &&
358 !session
->SendMessage(message
))
364 void* CNetServerWorker::RunThread(void* data
)
366 debug_SetThreadName("NetServer");
368 static_cast<CNetServerWorker
*>(data
)->Run();
373 void CNetServerWorker::Run()
375 // The script runtime uses the profiler and therefore the thread must be registered before the runtime is created
376 g_Profiler2
.RegisterCurrentThread("Net server");
378 // To avoid the need for JS_SetContextThread, we create and use and destroy
379 // the script interface entirely within this network thread
380 m_ScriptInterface
= new ScriptInterface("Engine", "Net server", ScriptInterface::CreateRuntime(g_ScriptRuntime
));
381 m_GameAttributes
.init(m_ScriptInterface
->GetJSRuntime(), JS::UndefinedValue());
388 // Implement autostart mode
389 if (m_State
== SERVER_STATE_PREGAME
&& (int)m_PlayerAssignments
.size() == m_AutostartPlayers
)
392 // Update profiler stats
393 m_Stats
->LatchHostState(m_Host
);
396 // Clear roots before deleting their context
397 m_SavedCommands
.clear();
399 SAFE_DELETE(m_ScriptInterface
);
402 bool CNetServerWorker::RunStep()
404 // Check for messages from the game thread.
405 // (Do as little work as possible while the mutex is held open,
406 // to avoid performance problems and deadlocks.)
408 m_ScriptInterface
->GetRuntime()->MaybeIncrementalGC(0.5f
);
410 JSContext
* cx
= m_ScriptInterface
->GetContext();
411 JSAutoRequest
rq(cx
);
413 std::vector
<bool> newStartGame
;
414 std::vector
<std::string
> newGameAttributes
;
415 std::vector
<std::pair
<CStr
, CStr
>> newLobbyAuths
;
416 std::vector
<u32
> newTurnLength
;
419 CScopeLock
lock(m_WorkerMutex
);
424 newStartGame
.swap(m_StartGameQueue
);
425 newGameAttributes
.swap(m_GameAttributesQueue
);
426 newLobbyAuths
.swap(m_LobbyAuthQueue
);
427 newTurnLength
.swap(m_TurnLengthQueue
);
430 if (!newGameAttributes
.empty())
432 JS::RootedValue
gameAttributesVal(cx
);
433 GetScriptInterface().ParseJSON(newGameAttributes
.back(), &gameAttributesVal
);
434 UpdateGameAttributes(&gameAttributesVal
);
437 if (!newTurnLength
.empty())
438 SetTurnLength(newTurnLength
.back());
440 // Do StartGame last, so we have the most up-to-date game attributes when we start
441 if (!newStartGame
.empty())
444 while (!newLobbyAuths
.empty())
446 const std::pair
<CStr
, CStr
>& auth
= newLobbyAuths
.back();
447 ProcessLobbyAuth(auth
.first
, auth
.second
);
448 newLobbyAuths
.pop_back();
451 // Perform file transfers
452 for (CNetServerSession
* session
: m_Sessions
)
453 session
->GetFileTransferer().Poll();
455 CheckClientConnections();
457 // Process network events:
460 int status
= enet_host_service(m_Host
, &event
, HOST_SERVICE_TIMEOUT
);
463 LOGERROR("CNetServerWorker: enet_host_service failed (%d)", status
);
464 // TODO: notify game that the server has shut down
470 // Reached timeout with no events - try again
474 // Process the event:
478 case ENET_EVENT_TYPE_CONNECT
:
480 // Report the client address
481 char hostname
[256] = "(error)";
482 enet_address_get_host_ip(&event
.peer
->address
, hostname
, ARRAY_SIZE(hostname
));
483 LOGMESSAGE("Net server: Received connection from %s:%u", hostname
, (unsigned int)event
.peer
->address
.port
);
485 // Set up a session object for this peer
487 CNetServerSession
* session
= new CNetServerSession(*this, event
.peer
);
489 m_Sessions
.push_back(session
);
491 SetupSession(session
);
493 ENSURE(event
.peer
->data
== NULL
);
494 event
.peer
->data
= session
;
496 HandleConnect(session
);
501 case ENET_EVENT_TYPE_DISCONNECT
:
503 // If there is an active session with this peer, then reset and delete it
505 CNetServerSession
* session
= static_cast<CNetServerSession
*>(event
.peer
->data
);
508 LOGMESSAGE("Net server: Disconnected %s", DebugName(session
).c_str());
510 // Remove the session first, so we won't send player-update messages to it
511 // when updating the FSM
512 m_Sessions
.erase(remove(m_Sessions
.begin(), m_Sessions
.end(), session
), m_Sessions
.end());
514 session
->Update((uint
)NMT_CONNECTION_LOST
, NULL
);
517 event
.peer
->data
= NULL
;
520 if (m_State
== SERVER_STATE_LOADING
)
521 CheckGameLoadStatus(NULL
);
526 case ENET_EVENT_TYPE_RECEIVE
:
528 // If there is an active session with this peer, then process the message
530 CNetServerSession
* session
= static_cast<CNetServerSession
*>(event
.peer
->data
);
533 // Create message from raw data
534 CNetMessage
* msg
= CNetMessageFactory::CreateMessage(event
.packet
->data
, event
.packet
->dataLength
, GetScriptInterface());
537 LOGMESSAGE("Net server: Received message %s of size %lu from %s", msg
->ToString().c_str(), (unsigned long)msg
->GetSerializedLength(), DebugName(session
).c_str());
539 HandleMessageReceive(msg
, session
);
545 // Done using the packet
546 enet_packet_destroy(event
.packet
);
551 case ENET_EVENT_TYPE_NONE
:
558 void CNetServerWorker::CheckClientConnections()
560 // Send messages at most once per second
561 std::time_t now
= std::time(nullptr);
562 if (now
<= m_LastConnectionCheck
)
565 m_LastConnectionCheck
= now
;
567 for (size_t i
= 0; i
< m_Sessions
.size(); ++i
)
569 u32 lastReceived
= m_Sessions
[i
]->GetLastReceivedTime();
570 u32 meanRTT
= m_Sessions
[i
]->GetMeanRTT();
572 CNetMessage
* message
= nullptr;
574 // Report if we didn't hear from the client since few seconds
575 if (lastReceived
> NETWORK_WARNING_TIMEOUT
)
577 CClientTimeoutMessage
* msg
= new CClientTimeoutMessage();
578 msg
->m_GUID
= m_Sessions
[i
]->GetGUID();
579 msg
->m_LastReceivedTime
= lastReceived
;
582 // Report if the client has bad ping
583 else if (meanRTT
> DEFAULT_TURN_LENGTH_MP
)
585 CClientPerformanceMessage
* msg
= new CClientPerformanceMessage();
586 CClientPerformanceMessage::S_m_Clients client
;
587 client
.m_GUID
= m_Sessions
[i
]->GetGUID();
588 client
.m_MeanRTT
= meanRTT
;
589 msg
->m_Clients
.push_back(client
);
593 // Send to all clients except the affected one
594 // (since that will show the locally triggered warning instead).
595 // Also send it to clients that finished the loading screen while
596 // the game is still waiting for other clients to finish the loading screen.
598 for (size_t j
= 0; j
< m_Sessions
.size(); ++j
)
601 (m_Sessions
[j
]->GetCurrState() == NSS_PREGAME
&& m_State
== SERVER_STATE_PREGAME
) ||
602 m_Sessions
[j
]->GetCurrState() == NSS_INGAME
))
604 m_Sessions
[j
]->SendMessage(message
);
608 SAFE_DELETE(message
);
612 void CNetServerWorker::HandleMessageReceive(const CNetMessage
* message
, CNetServerSession
* session
)
614 // Handle non-FSM messages first
615 Status status
= session
->GetFileTransferer().HandleMessageReceive(message
);
616 if (status
!= INFO::SKIPPED
)
619 if (message
->GetType() == NMT_FILE_TRANSFER_REQUEST
)
621 CFileTransferRequestMessage
* reqMessage
= (CFileTransferRequestMessage
*)message
;
623 // Rejoining client got our JoinSyncStart after we received the state from
624 // another client, and has now requested that we forward it to them
626 ENSURE(!m_JoinSyncFile
.empty());
627 session
->GetFileTransferer().StartResponse(reqMessage
->m_RequestID
, m_JoinSyncFile
);
633 if (!session
->Update(message
->GetType(), (void*)message
))
634 LOGERROR("Net server: Error running FSM update (type=%d state=%d)", (int)message
->GetType(), (int)session
->GetCurrState());
637 void CNetServerWorker::SetupSession(CNetServerSession
* session
)
639 void* context
= session
;
641 // Set up transitions for session
643 session
->AddTransition(NSS_UNCONNECTED
, (uint
)NMT_CONNECTION_LOST
, NSS_UNCONNECTED
);
645 session
->AddTransition(NSS_HANDSHAKE
, (uint
)NMT_CONNECTION_LOST
, NSS_UNCONNECTED
);
646 session
->AddTransition(NSS_HANDSHAKE
, (uint
)NMT_CLIENT_HANDSHAKE
, NSS_AUTHENTICATE
, (void*)&OnClientHandshake
, context
);
648 session
->AddTransition(NSS_LOBBY_AUTHENTICATE
, (uint
)NMT_CONNECTION_LOST
, NSS_UNCONNECTED
);
649 session
->AddTransition(NSS_LOBBY_AUTHENTICATE
, (uint
)NMT_AUTHENTICATE
, NSS_PREGAME
, (void*)&OnAuthenticate
, context
);
651 session
->AddTransition(NSS_AUTHENTICATE
, (uint
)NMT_CONNECTION_LOST
, NSS_UNCONNECTED
);
652 session
->AddTransition(NSS_AUTHENTICATE
, (uint
)NMT_AUTHENTICATE
, NSS_PREGAME
, (void*)&OnAuthenticate
, context
);
654 session
->AddTransition(NSS_PREGAME
, (uint
)NMT_CONNECTION_LOST
, NSS_UNCONNECTED
, (void*)&OnDisconnect
, context
);
655 session
->AddTransition(NSS_PREGAME
, (uint
)NMT_CHAT
, NSS_PREGAME
, (void*)&OnChat
, context
);
656 session
->AddTransition(NSS_PREGAME
, (uint
)NMT_READY
, NSS_PREGAME
, (void*)&OnReady
, context
);
657 session
->AddTransition(NSS_PREGAME
, (uint
)NMT_CLEAR_ALL_READY
, NSS_PREGAME
, (void*)&OnClearAllReady
, context
);
658 session
->AddTransition(NSS_PREGAME
, (uint
)NMT_GAME_SETUP
, NSS_PREGAME
, (void*)&OnGameSetup
, context
);
659 session
->AddTransition(NSS_PREGAME
, (uint
)NMT_ASSIGN_PLAYER
, NSS_PREGAME
, (void*)&OnAssignPlayer
, context
);
660 session
->AddTransition(NSS_PREGAME
, (uint
)NMT_KICKED
, NSS_PREGAME
, (void*)&OnKickPlayer
, context
);
661 session
->AddTransition(NSS_PREGAME
, (uint
)NMT_GAME_START
, NSS_PREGAME
, (void*)&OnStartGame
, context
);
662 session
->AddTransition(NSS_PREGAME
, (uint
)NMT_LOADED_GAME
, NSS_INGAME
, (void*)&OnLoadedGame
, context
);
664 session
->AddTransition(NSS_JOIN_SYNCING
, (uint
)NMT_KICKED
, NSS_JOIN_SYNCING
, (void*)&OnKickPlayer
, context
);
665 session
->AddTransition(NSS_JOIN_SYNCING
, (uint
)NMT_CONNECTION_LOST
, NSS_UNCONNECTED
, (void*)&OnDisconnect
, context
);
666 session
->AddTransition(NSS_JOIN_SYNCING
, (uint
)NMT_LOADED_GAME
, NSS_INGAME
, (void*)&OnJoinSyncingLoadedGame
, context
);
668 session
->AddTransition(NSS_INGAME
, (uint
)NMT_REJOINED
, NSS_INGAME
, (void*)&OnRejoined
, context
);
669 session
->AddTransition(NSS_INGAME
, (uint
)NMT_KICKED
, NSS_INGAME
, (void*)&OnKickPlayer
, context
);
670 session
->AddTransition(NSS_INGAME
, (uint
)NMT_CLIENT_PAUSED
, NSS_INGAME
, (void*)&OnClientPaused
, context
);
671 session
->AddTransition(NSS_INGAME
, (uint
)NMT_CONNECTION_LOST
, NSS_UNCONNECTED
, (void*)&OnDisconnect
, context
);
672 session
->AddTransition(NSS_INGAME
, (uint
)NMT_CHAT
, NSS_INGAME
, (void*)&OnChat
, context
);
673 session
->AddTransition(NSS_INGAME
, (uint
)NMT_SIMULATION_COMMAND
, NSS_INGAME
, (void*)&OnInGame
, context
);
674 session
->AddTransition(NSS_INGAME
, (uint
)NMT_SYNC_CHECK
, NSS_INGAME
, (void*)&OnInGame
, context
);
675 session
->AddTransition(NSS_INGAME
, (uint
)NMT_END_COMMAND_BATCH
, NSS_INGAME
, (void*)&OnInGame
, context
);
678 session
->SetFirstState(NSS_HANDSHAKE
);
681 bool CNetServerWorker::HandleConnect(CNetServerSession
* session
)
683 if (std::find(m_BannedIPs
.begin(), m_BannedIPs
.end(), session
->GetIPAddress()) != m_BannedIPs
.end())
685 session
->Disconnect(NDR_BANNED
);
689 CSrvHandshakeMessage handshake
;
690 handshake
.m_Magic
= PS_PROTOCOL_MAGIC
;
691 handshake
.m_ProtocolVersion
= PS_PROTOCOL_VERSION
;
692 handshake
.m_SoftwareVersion
= PS_PROTOCOL_VERSION
;
693 return session
->SendMessage(&handshake
);
696 void CNetServerWorker::OnUserJoin(CNetServerSession
* session
)
698 AddPlayer(session
->GetGUID(), session
->GetUserName());
700 if (m_HostGUID
.empty() && session
->IsLocalClient())
701 m_HostGUID
= session
->GetGUID();
703 CGameSetupMessage
gameSetupMessage(GetScriptInterface());
704 gameSetupMessage
.m_Data
= m_GameAttributes
;
705 session
->SendMessage(&gameSetupMessage
);
707 CPlayerAssignmentMessage assignMessage
;
708 ConstructPlayerAssignmentMessage(assignMessage
);
709 session
->SendMessage(&assignMessage
);
712 void CNetServerWorker::OnUserLeave(CNetServerSession
* session
)
714 std::vector
<CStr
>::iterator pausing
= std::find(m_PausingPlayers
.begin(), m_PausingPlayers
.end(), session
->GetGUID());
715 if (pausing
!= m_PausingPlayers
.end())
716 m_PausingPlayers
.erase(pausing
);
718 RemovePlayer(session
->GetGUID());
720 if (m_ServerTurnManager
&& session
->GetCurrState() != NSS_JOIN_SYNCING
)
721 m_ServerTurnManager
->UninitialiseClient(session
->GetHostID()); // TODO: only for non-observers
723 // TODO: ought to switch the player controlled by that client
724 // back to AI control, or something?
727 void CNetServerWorker::AddPlayer(const CStr
& guid
, const CStrW
& name
)
729 // Find all player IDs in active use; we mustn't give them to a second player (excluding the unassigned ID: -1)
730 std::set
<i32
> usedIDs
;
731 for (const std::pair
<CStr
, PlayerAssignment
>& p
: m_PlayerAssignments
)
732 if (p
.second
.m_Enabled
&& p
.second
.m_PlayerID
!= -1)
733 usedIDs
.insert(p
.second
.m_PlayerID
);
735 // If the player is rejoining after disconnecting, try to give them
736 // back their old player ID
740 // Try to match GUID first
741 for (PlayerAssignmentMap::iterator it
= m_PlayerAssignments
.begin(); it
!= m_PlayerAssignments
.end(); ++it
)
743 if (!it
->second
.m_Enabled
&& it
->first
== guid
&& usedIDs
.find(it
->second
.m_PlayerID
) == usedIDs
.end())
745 playerID
= it
->second
.m_PlayerID
;
746 m_PlayerAssignments
.erase(it
); // delete the old mapping, since we've got a new one now
751 // Try to match username next
752 for (PlayerAssignmentMap::iterator it
= m_PlayerAssignments
.begin(); it
!= m_PlayerAssignments
.end(); ++it
)
754 if (!it
->second
.m_Enabled
&& it
->second
.m_Name
== name
&& usedIDs
.find(it
->second
.m_PlayerID
) == usedIDs
.end())
756 playerID
= it
->second
.m_PlayerID
;
757 m_PlayerAssignments
.erase(it
); // delete the old mapping, since we've got a new one now
762 // Otherwise leave the player ID as -1 (observer) and let gamesetup change it as needed.
765 PlayerAssignment assignment
;
766 assignment
.m_Enabled
= true;
767 assignment
.m_Name
= name
;
768 assignment
.m_PlayerID
= playerID
;
769 assignment
.m_Status
= 0;
770 m_PlayerAssignments
[guid
] = assignment
;
772 // Send the new assignments to all currently active players
773 // (which does not include the one that's just joining)
774 SendPlayerAssignments();
777 void CNetServerWorker::RemovePlayer(const CStr
& guid
)
779 m_PlayerAssignments
[guid
].m_Enabled
= false;
781 SendPlayerAssignments();
784 void CNetServerWorker::ClearAllPlayerReady()
786 for (std::pair
<const CStr
, PlayerAssignment
>& p
: m_PlayerAssignments
)
787 if (p
.second
.m_Status
!= 2)
788 p
.second
.m_Status
= 0;
790 SendPlayerAssignments();
793 void CNetServerWorker::KickPlayer(const CStrW
& playerName
, const bool ban
)
795 // Find the user with that name
796 std::vector
<CNetServerSession
*>::iterator it
= std::find_if(m_Sessions
.begin(), m_Sessions
.end(),
797 [&](CNetServerSession
* session
) { return session
->GetUserName() == playerName
; });
799 // and return if no one or the host has that name
800 if (it
== m_Sessions
.end() || (*it
)->GetGUID() == m_HostGUID
)
806 if (std::find(m_BannedPlayers
.begin(), m_BannedPlayers
.end(), playerName
) == m_BannedPlayers
.end())
807 m_BannedPlayers
.push_back(playerName
);
809 // Remember IP address
810 u32 ipAddress
= (*it
)->GetIPAddress();
811 if (std::find(m_BannedIPs
.begin(), m_BannedIPs
.end(), ipAddress
) == m_BannedIPs
.end())
812 m_BannedIPs
.push_back(ipAddress
);
815 // Disconnect that user
816 (*it
)->Disconnect(ban
? NDR_BANNED
: NDR_KICKED
);
818 // Send message notifying other clients
819 CKickedMessage kickedMessage
;
820 kickedMessage
.m_Name
= playerName
;
821 kickedMessage
.m_Ban
= ban
;
822 Broadcast(&kickedMessage
, { NSS_PREGAME
, NSS_JOIN_SYNCING
, NSS_INGAME
});
825 void CNetServerWorker::AssignPlayer(int playerID
, const CStr
& guid
)
827 // Remove anyone who's already assigned to this player
828 for (std::pair
<const CStr
, PlayerAssignment
>& p
: m_PlayerAssignments
)
830 if (p
.second
.m_PlayerID
== playerID
)
831 p
.second
.m_PlayerID
= -1;
834 // Update this host's assignment if it exists
835 if (m_PlayerAssignments
.find(guid
) != m_PlayerAssignments
.end())
836 m_PlayerAssignments
[guid
].m_PlayerID
= playerID
;
838 SendPlayerAssignments();
841 void CNetServerWorker::ConstructPlayerAssignmentMessage(CPlayerAssignmentMessage
& message
)
843 for (const std::pair
<CStr
, PlayerAssignment
>& p
: m_PlayerAssignments
)
845 if (!p
.second
.m_Enabled
)
848 CPlayerAssignmentMessage::S_m_Hosts h
;
850 h
.m_Name
= p
.second
.m_Name
;
851 h
.m_PlayerID
= p
.second
.m_PlayerID
;
852 h
.m_Status
= p
.second
.m_Status
;
853 message
.m_Hosts
.push_back(h
);
857 void CNetServerWorker::SendPlayerAssignments()
859 CPlayerAssignmentMessage message
;
860 ConstructPlayerAssignmentMessage(message
);
861 Broadcast(&message
, { NSS_PREGAME
, NSS_JOIN_SYNCING
, NSS_INGAME
});
864 const ScriptInterface
& CNetServerWorker::GetScriptInterface()
866 return *m_ScriptInterface
;
869 void CNetServerWorker::SetTurnLength(u32 msecs
)
871 if (m_ServerTurnManager
)
872 m_ServerTurnManager
->SetTurnLength(msecs
);
875 void CNetServerWorker::ProcessLobbyAuth(const CStr
& name
, const CStr
& token
)
877 LOGMESSAGE("Net Server: Received lobby auth message from %s with %s", name
, token
);
878 // Find the user with that guid
879 std::vector
<CNetServerSession
*>::iterator it
= std::find_if(m_Sessions
.begin(), m_Sessions
.end(),
880 [&](CNetServerSession
* session
)
881 { return session
->GetGUID() == token
; });
883 if (it
== m_Sessions
.end())
886 (*it
)->SetUserName(name
.FromUTF8());
887 // Send an empty message to request the authentication message from the client
888 // after its identity has been confirmed via the lobby
889 CAuthenticateMessage emptyMessage
;
890 (*it
)->SendMessage(&emptyMessage
);
893 bool CNetServerWorker::OnClientHandshake(void* context
, CFsmEvent
* event
)
895 ENSURE(event
->GetType() == (uint
)NMT_CLIENT_HANDSHAKE
);
897 CNetServerSession
* session
= (CNetServerSession
*)context
;
898 CNetServerWorker
& server
= session
->GetServer();
900 CCliHandshakeMessage
* message
= (CCliHandshakeMessage
*)event
->GetParamRef();
901 if (message
->m_ProtocolVersion
!= PS_PROTOCOL_VERSION
)
903 session
->Disconnect(NDR_INCORRECT_PROTOCOL_VERSION
);
907 CStr guid
= ps_generate_guid();
909 // Ensure unique GUID
911 server
.m_Sessions
.begin(), server
.m_Sessions
.end(),
912 [&guid
] (const CNetServerSession
* session
)
913 { return session
->GetGUID() == guid
; }) != server
.m_Sessions
.end())
917 session
->Disconnect(NDR_UNKNOWN
);
920 guid
= ps_generate_guid();
923 session
->SetGUID(guid
);
925 CSrvHandshakeResponseMessage handshakeResponse
;
926 handshakeResponse
.m_UseProtocolVersion
= PS_PROTOCOL_VERSION
;
927 handshakeResponse
.m_GUID
= guid
;
928 handshakeResponse
.m_Flags
= 0;
930 if (server
.m_LobbyAuth
)
932 handshakeResponse
.m_Flags
|= PS_NETWORK_FLAG_REQUIRE_LOBBYAUTH
;
933 session
->SetNextState(NSS_LOBBY_AUTHENTICATE
);
936 session
->SendMessage(&handshakeResponse
);
941 bool CNetServerWorker::OnAuthenticate(void* context
, CFsmEvent
* event
)
943 ENSURE(event
->GetType() == (uint
)NMT_AUTHENTICATE
);
945 CNetServerSession
* session
= (CNetServerSession
*)context
;
946 CNetServerWorker
& server
= session
->GetServer();
948 // Prohibit joins while the game is loading
949 if (server
.m_State
== SERVER_STATE_LOADING
)
951 LOGMESSAGE("Refused connection while the game is loading");
952 session
->Disconnect(NDR_SERVER_LOADING
);
956 CAuthenticateMessage
* message
= (CAuthenticateMessage
*)event
->GetParamRef();
957 CStrW username
= SanitisePlayerName(message
->m_Name
);
958 CStrW
usernameWithoutRating(username
.substr(0, username
.find(L
" (")));
960 // Compare the lowercase names as specified by https://xmpp.org/extensions/xep-0029.html#sect-idm139493404168176
961 // "[...] comparisons will be made in case-normalized canonical form."
962 if (server
.m_LobbyAuth
&& usernameWithoutRating
.LowerCase() != session
->GetUserName().LowerCase())
964 LOGERROR("Net server: lobby auth: %s tried joining as %s",
965 session
->GetUserName().ToUTF8(),
966 usernameWithoutRating
.ToUTF8());
967 session
->Disconnect(NDR_LOBBY_AUTH_FAILED
);
971 // Either deduplicate or prohibit join if name is in use
972 bool duplicatePlayernames
= false;
973 CFG_GET_VAL("network.duplicateplayernames", duplicatePlayernames
);
974 if (duplicatePlayernames
)
975 username
= server
.DeduplicatePlayerName(username
);
978 std::vector
<CNetServerSession
*>::iterator it
= std::find_if(
979 server
.m_Sessions
.begin(), server
.m_Sessions
.end(),
980 [&username
] (const CNetServerSession
* session
)
981 { return session
->GetUserName() == username
; });
983 if (it
!= server
.m_Sessions
.end() && (*it
) != session
)
985 session
->Disconnect(NDR_PLAYERNAME_IN_USE
);
990 // Disconnect banned usernames
991 if (std::find(server
.m_BannedPlayers
.begin(), server
.m_BannedPlayers
.end(), username
) != server
.m_BannedPlayers
.end())
993 session
->Disconnect(NDR_BANNED
);
997 int maxObservers
= 0;
998 CFG_GET_VAL("network.observerlimit", maxObservers
);
1000 bool isRejoining
= false;
1001 bool serverFull
= false;
1002 if (server
.m_State
== SERVER_STATE_PREGAME
)
1004 // Don't check for maxObservers in the gamesetup, as we don't know yet who will be assigned
1005 serverFull
= server
.m_Sessions
.size() >= MAX_CLIENTS
;
1009 bool isObserver
= true;
1010 int disconnectedPlayers
= 0;
1011 int connectedPlayers
= 0;
1012 // (TODO: if GUIDs were stable, we should use them instead)
1013 for (const std::pair
<CStr
, PlayerAssignment
>& p
: server
.m_PlayerAssignments
)
1015 const PlayerAssignment
& assignment
= p
.second
;
1017 if (!assignment
.m_Enabled
&& assignment
.m_Name
== username
)
1019 isObserver
= assignment
.m_PlayerID
== -1;
1023 if (assignment
.m_PlayerID
== -1)
1026 if (assignment
.m_Enabled
)
1029 ++disconnectedPlayers
;
1032 // Optionally allow everyone or only buddies to join after the game has started
1035 CStr observerLateJoin
;
1036 CFG_GET_VAL("network.lateobservers", observerLateJoin
);
1038 if (observerLateJoin
== "everyone")
1042 else if (observerLateJoin
== "buddies")
1045 CFG_GET_VAL("lobby.buddies", buddies
);
1046 std::wstringstream
buddiesStream(wstring_from_utf8(buddies
));
1048 while (std::getline(buddiesStream
, buddy
, L
','))
1050 if (buddy
== usernameWithoutRating
)
1061 LOGMESSAGE("Refused connection after game start from not-previously-known user \"%s\"", utf8_from_wstring(username
));
1062 session
->Disconnect(NDR_SERVER_ALREADY_IN_GAME
);
1066 // Ensure all players will be able to rejoin
1067 serverFull
= isObserver
&& (
1068 (int) server
.m_Sessions
.size() - connectedPlayers
> maxObservers
||
1069 (int) server
.m_Sessions
.size() + disconnectedPlayers
>= MAX_CLIENTS
);
1074 session
->Disconnect(NDR_SERVER_FULL
);
1078 // TODO: check server password etc?
1080 u32 newHostID
= server
.m_NextHostID
++;
1082 session
->SetUserName(username
);
1083 session
->SetHostID(newHostID
);
1084 session
->SetLocalClient(message
->m_IsLocalClient
);
1086 CAuthenticateResultMessage authenticateResult
;
1087 authenticateResult
.m_Code
= isRejoining
? ARC_OK_REJOINING
: ARC_OK
;
1088 authenticateResult
.m_HostID
= newHostID
;
1089 authenticateResult
.m_Message
= L
"Logged in";
1090 session
->SendMessage(&authenticateResult
);
1092 server
.OnUserJoin(session
);
1096 // Request a copy of the current game state from an existing player,
1097 // so we can send it on to the new player
1099 // Assume session 0 is most likely the local player, so they're
1100 // the most efficient client to request a copy from
1101 CNetServerSession
* sourceSession
= server
.m_Sessions
.at(0);
1103 session
->SetLongTimeout(true);
1105 sourceSession
->GetFileTransferer().StartTask(
1106 shared_ptr
<CNetFileReceiveTask
>(new CNetFileReceiveTask_ServerRejoin(server
, newHostID
))
1109 session
->SetNextState(NSS_JOIN_SYNCING
);
1115 bool CNetServerWorker::OnInGame(void* context
, CFsmEvent
* event
)
1117 // TODO: should split each of these cases into a separate method
1119 CNetServerSession
* session
= (CNetServerSession
*)context
;
1120 CNetServerWorker
& server
= session
->GetServer();
1122 CNetMessage
* message
= (CNetMessage
*)event
->GetParamRef();
1123 if (message
->GetType() == (uint
)NMT_SIMULATION_COMMAND
)
1125 CSimulationMessage
* simMessage
= static_cast<CSimulationMessage
*> (message
);
1127 // Ignore messages sent by one player on behalf of another player
1128 // unless cheating is enabled
1129 bool cheatsEnabled
= false;
1130 const ScriptInterface
& scriptInterface
= server
.GetScriptInterface();
1131 JSContext
* cx
= scriptInterface
.GetContext();
1132 JSAutoRequest
rq(cx
);
1133 JS::RootedValue
settings(cx
);
1134 scriptInterface
.GetProperty(server
.m_GameAttributes
, "settings", &settings
);
1135 if (scriptInterface
.HasProperty(settings
, "CheatsEnabled"))
1136 scriptInterface
.GetProperty(settings
, "CheatsEnabled", cheatsEnabled
);
1138 PlayerAssignmentMap::iterator it
= server
.m_PlayerAssignments
.find(session
->GetGUID());
1139 // When cheating is disabled, fail if the player the message claims to
1140 // represent does not exist or does not match the sender's player name
1141 if (!cheatsEnabled
&& (it
== server
.m_PlayerAssignments
.end() || it
->second
.m_PlayerID
!= simMessage
->m_Player
))
1144 // Send it back to all clients that have finished
1145 // the loading screen (and the synchronization when rejoining)
1146 server
.Broadcast(simMessage
, { NSS_INGAME
});
1148 // Save all the received commands
1149 if (server
.m_SavedCommands
.size() < simMessage
->m_Turn
+ 1)
1150 server
.m_SavedCommands
.resize(simMessage
->m_Turn
+ 1);
1151 server
.m_SavedCommands
[simMessage
->m_Turn
].push_back(*simMessage
);
1153 // TODO: we shouldn't send the message back to the client that first sent it
1155 else if (message
->GetType() == (uint
)NMT_SYNC_CHECK
)
1157 CSyncCheckMessage
* syncMessage
= static_cast<CSyncCheckMessage
*> (message
);
1158 server
.m_ServerTurnManager
->NotifyFinishedClientUpdate(*session
, syncMessage
->m_Turn
, syncMessage
->m_Hash
);
1160 else if (message
->GetType() == (uint
)NMT_END_COMMAND_BATCH
)
1162 // The turn-length field is ignored
1163 CEndCommandBatchMessage
* endMessage
= static_cast<CEndCommandBatchMessage
*> (message
);
1164 server
.m_ServerTurnManager
->NotifyFinishedClientCommands(*session
, endMessage
->m_Turn
);
1170 bool CNetServerWorker::OnChat(void* context
, CFsmEvent
* event
)
1172 ENSURE(event
->GetType() == (uint
)NMT_CHAT
);
1174 CNetServerSession
* session
= (CNetServerSession
*)context
;
1175 CNetServerWorker
& server
= session
->GetServer();
1177 CChatMessage
* message
= (CChatMessage
*)event
->GetParamRef();
1179 message
->m_GUID
= session
->GetGUID();
1181 server
.Broadcast(message
, { NSS_PREGAME
, NSS_INGAME
});
1186 bool CNetServerWorker::OnReady(void* context
, CFsmEvent
* event
)
1188 ENSURE(event
->GetType() == (uint
)NMT_READY
);
1190 CNetServerSession
* session
= (CNetServerSession
*)context
;
1191 CNetServerWorker
& server
= session
->GetServer();
1193 // Occurs if a client presses not-ready
1194 // in the very last moment before the hosts starts the game
1195 if (server
.m_State
== SERVER_STATE_LOADING
)
1198 CReadyMessage
* message
= (CReadyMessage
*)event
->GetParamRef();
1199 message
->m_GUID
= session
->GetGUID();
1200 server
.Broadcast(message
, { NSS_PREGAME
});
1202 server
.m_PlayerAssignments
[message
->m_GUID
].m_Status
= message
->m_Status
;
1207 bool CNetServerWorker::OnClearAllReady(void* context
, CFsmEvent
* event
)
1209 ENSURE(event
->GetType() == (uint
)NMT_CLEAR_ALL_READY
);
1211 CNetServerSession
* session
= (CNetServerSession
*)context
;
1212 CNetServerWorker
& server
= session
->GetServer();
1214 if (session
->GetGUID() == server
.m_HostGUID
)
1215 server
.ClearAllPlayerReady();
1220 bool CNetServerWorker::OnGameSetup(void* context
, CFsmEvent
* event
)
1222 ENSURE(event
->GetType() == (uint
)NMT_GAME_SETUP
);
1224 CNetServerSession
* session
= (CNetServerSession
*)context
;
1225 CNetServerWorker
& server
= session
->GetServer();
1227 // Changing the settings after gamestart is not implemented and would cause an Out-of-sync error.
1228 // This happened when doubleclicking on the startgame button.
1229 if (server
.m_State
!= SERVER_STATE_PREGAME
)
1232 if (session
->GetGUID() == server
.m_HostGUID
)
1234 CGameSetupMessage
* message
= (CGameSetupMessage
*)event
->GetParamRef();
1235 server
.UpdateGameAttributes(&(message
->m_Data
));
1240 bool CNetServerWorker::OnAssignPlayer(void* context
, CFsmEvent
* event
)
1242 ENSURE(event
->GetType() == (uint
)NMT_ASSIGN_PLAYER
);
1243 CNetServerSession
* session
= (CNetServerSession
*)context
;
1244 CNetServerWorker
& server
= session
->GetServer();
1246 if (session
->GetGUID() == server
.m_HostGUID
)
1248 CAssignPlayerMessage
* message
= (CAssignPlayerMessage
*)event
->GetParamRef();
1249 server
.AssignPlayer(message
->m_PlayerID
, message
->m_GUID
);
1254 bool CNetServerWorker::OnStartGame(void* context
, CFsmEvent
* event
)
1256 ENSURE(event
->GetType() == (uint
)NMT_GAME_START
);
1257 CNetServerSession
* session
= (CNetServerSession
*)context
;
1258 CNetServerWorker
& server
= session
->GetServer();
1260 if (session
->GetGUID() == server
.m_HostGUID
)
1266 bool CNetServerWorker::OnLoadedGame(void* context
, CFsmEvent
* event
)
1268 ENSURE(event
->GetType() == (uint
)NMT_LOADED_GAME
);
1270 CNetServerSession
* loadedSession
= (CNetServerSession
*)context
;
1271 CNetServerWorker
& server
= loadedSession
->GetServer();
1273 loadedSession
->SetLongTimeout(false);
1275 // We're in the loading state, so wait until every client has loaded
1276 // before starting the game
1277 ENSURE(server
.m_State
== SERVER_STATE_LOADING
);
1278 if (server
.CheckGameLoadStatus(loadedSession
))
1281 CClientsLoadingMessage message
;
1282 // We always send all GUIDs of clients in the loading state
1283 // so that we don't have to bother about switching GUI pages
1284 for (CNetServerSession
* session
: server
.m_Sessions
)
1285 if (session
->GetCurrState() != NSS_INGAME
&& loadedSession
->GetGUID() != session
->GetGUID())
1287 CClientsLoadingMessage::S_m_Clients client
;
1288 client
.m_GUID
= session
->GetGUID();
1289 message
.m_Clients
.push_back(client
);
1292 // Send to the client who has loaded the game but did not reach the NSS_INGAME state yet
1293 loadedSession
->SendMessage(&message
);
1294 server
.Broadcast(&message
, { NSS_INGAME
});
1299 bool CNetServerWorker::OnJoinSyncingLoadedGame(void* context
, CFsmEvent
* event
)
1301 // A client rejoining an in-progress game has now finished loading the
1302 // map and deserialized the initial state.
1303 // The simulation may have progressed since then, so send any subsequent
1304 // commands to them and set them as an active player so they can participate
1305 // in all future turns.
1307 // (TODO: if it takes a long time for them to receive and execute all these
1308 // commands, the other players will get frozen for that time and may be unhappy;
1309 // we could try repeating this process a few times until the client converges
1310 // on the up-to-date state, before setting them as active.)
1312 ENSURE(event
->GetType() == (uint
)NMT_LOADED_GAME
);
1314 CNetServerSession
* session
= (CNetServerSession
*)context
;
1315 CNetServerWorker
& server
= session
->GetServer();
1317 CLoadedGameMessage
* message
= (CLoadedGameMessage
*)event
->GetParamRef();
1319 u32 turn
= message
->m_CurrentTurn
;
1320 u32 readyTurn
= server
.m_ServerTurnManager
->GetReadyTurn();
1322 // Send them all commands received since their saved state,
1323 // and turn-ended messages for any turns that have already been processed
1324 for (size_t i
= turn
+ 1; i
< std::max(readyTurn
+1, (u32
)server
.m_SavedCommands
.size()); ++i
)
1326 if (i
< server
.m_SavedCommands
.size())
1327 for (size_t j
= 0; j
< server
.m_SavedCommands
[i
].size(); ++j
)
1328 session
->SendMessage(&server
.m_SavedCommands
[i
][j
]);
1332 CEndCommandBatchMessage endMessage
;
1333 endMessage
.m_Turn
= i
;
1334 endMessage
.m_TurnLength
= server
.m_ServerTurnManager
->GetSavedTurnLength(i
);
1335 session
->SendMessage(&endMessage
);
1339 // Tell the turn manager to expect commands from this new client
1340 server
.m_ServerTurnManager
->InitialiseClient(session
->GetHostID(), readyTurn
);
1342 // Tell the client that everything has finished loading and it should start now
1343 CLoadedGameMessage loaded
;
1344 loaded
.m_CurrentTurn
= readyTurn
;
1345 session
->SendMessage(&loaded
);
1350 bool CNetServerWorker::OnRejoined(void* context
, CFsmEvent
* event
)
1352 // A client has finished rejoining and the loading screen disappeared.
1353 ENSURE(event
->GetType() == (uint
)NMT_REJOINED
);
1355 CNetServerSession
* session
= (CNetServerSession
*)context
;
1356 CNetServerWorker
& server
= session
->GetServer();
1358 // Inform everyone of the client having rejoined
1359 CRejoinedMessage
* message
= (CRejoinedMessage
*)event
->GetParamRef();
1360 message
->m_GUID
= session
->GetGUID();
1361 server
.Broadcast(message
, { NSS_INGAME
});
1363 // Send all pausing players to the rejoined client.
1364 for (const CStr
& guid
: server
.m_PausingPlayers
)
1366 CClientPausedMessage pausedMessage
;
1367 pausedMessage
.m_GUID
= guid
;
1368 pausedMessage
.m_Pause
= true;
1369 session
->SendMessage(&pausedMessage
);
1372 session
->SetLongTimeout(false);
1377 bool CNetServerWorker::OnKickPlayer(void* context
, CFsmEvent
* event
)
1379 ENSURE(event
->GetType() == (uint
)NMT_KICKED
);
1381 CNetServerSession
* session
= (CNetServerSession
*)context
;
1382 CNetServerWorker
& server
= session
->GetServer();
1384 if (session
->GetGUID() == server
.m_HostGUID
)
1386 CKickedMessage
* message
= (CKickedMessage
*)event
->GetParamRef();
1387 server
.KickPlayer(message
->m_Name
, message
->m_Ban
);
1392 bool CNetServerWorker::OnDisconnect(void* context
, CFsmEvent
* event
)
1394 ENSURE(event
->GetType() == (uint
)NMT_CONNECTION_LOST
);
1396 CNetServerSession
* session
= (CNetServerSession
*)context
;
1397 CNetServerWorker
& server
= session
->GetServer();
1399 server
.OnUserLeave(session
);
1404 bool CNetServerWorker::OnClientPaused(void* context
, CFsmEvent
* event
)
1406 ENSURE(event
->GetType() == (uint
)NMT_CLIENT_PAUSED
);
1408 CNetServerSession
* session
= (CNetServerSession
*)context
;
1409 CNetServerWorker
& server
= session
->GetServer();
1411 CClientPausedMessage
* message
= (CClientPausedMessage
*)event
->GetParamRef();
1413 message
->m_GUID
= session
->GetGUID();
1415 // Update the list of pausing players.
1416 std::vector
<CStr
>::iterator player
= std::find(server
.m_PausingPlayers
.begin(), server
.m_PausingPlayers
.end(), session
->GetGUID());
1418 if (message
->m_Pause
)
1420 if (player
!= server
.m_PausingPlayers
.end())
1423 server
.m_PausingPlayers
.push_back(session
->GetGUID());
1427 if (player
== server
.m_PausingPlayers
.end())
1430 server
.m_PausingPlayers
.erase(player
);
1433 // Send messages to clients that are in game, and are not the client who paused.
1434 for (CNetServerSession
* session
: server
.m_Sessions
)
1436 if (session
->GetCurrState() == NSS_INGAME
&& message
->m_GUID
!= session
->GetGUID())
1437 session
->SendMessage(message
);
1443 bool CNetServerWorker::CheckGameLoadStatus(CNetServerSession
* changedSession
)
1445 for (const CNetServerSession
* session
: m_Sessions
)
1446 if (session
!= changedSession
&& session
->GetCurrState() != NSS_INGAME
)
1449 // Inform clients that everyone has loaded the map and that the game can start
1450 CLoadedGameMessage loaded
;
1451 loaded
.m_CurrentTurn
= 0;
1453 // Notice the changedSession is still in the NSS_PREGAME state
1454 Broadcast(&loaded
, { NSS_PREGAME
, NSS_INGAME
});
1456 m_State
= SERVER_STATE_INGAME
;
1460 void CNetServerWorker::StartGame()
1462 for (std::pair
<const CStr
, PlayerAssignment
>& player
: m_PlayerAssignments
)
1463 if (player
.second
.m_Enabled
&& player
.second
.m_PlayerID
!= -1 && player
.second
.m_Status
== 0)
1465 LOGERROR("Tried to start the game without player \"%s\" being ready!", utf8_from_wstring(player
.second
.m_Name
).c_str());
1469 m_ServerTurnManager
= new CNetServerTurnManager(*this);
1471 for (CNetServerSession
* session
: m_Sessions
)
1473 m_ServerTurnManager
->InitialiseClient(session
->GetHostID(), 0); // TODO: only for non-observers
1474 session
->SetLongTimeout(true);
1477 m_State
= SERVER_STATE_LOADING
;
1479 // Send the final setup state to all clients
1480 UpdateGameAttributes(&m_GameAttributes
);
1482 // Remove players and observers that are not present when the game starts
1483 for (PlayerAssignmentMap::iterator it
= m_PlayerAssignments
.begin(); it
!= m_PlayerAssignments
.end();)
1484 if (it
->second
.m_Enabled
)
1487 it
= m_PlayerAssignments
.erase(it
);
1489 SendPlayerAssignments();
1491 CGameStartMessage gameStart
;
1492 Broadcast(&gameStart
, { NSS_PREGAME
});
1495 void CNetServerWorker::UpdateGameAttributes(JS::MutableHandleValue attrs
)
1497 m_GameAttributes
= attrs
;
1502 CGameSetupMessage
gameSetupMessage(GetScriptInterface());
1503 gameSetupMessage
.m_Data
= m_GameAttributes
;
1504 Broadcast(&gameSetupMessage
, { NSS_PREGAME
});
1507 CStrW
CNetServerWorker::SanitisePlayerName(const CStrW
& original
)
1509 const size_t MAX_LENGTH
= 32;
1511 CStrW name
= original
;
1512 name
.Replace(L
"[", L
"{"); // remove GUI tags
1513 name
.Replace(L
"]", L
"}"); // remove for symmetry
1515 // Restrict the length
1516 if (name
.length() > MAX_LENGTH
)
1517 name
= name
.Left(MAX_LENGTH
);
1519 // Don't allow surrounding whitespace
1520 name
.Trim(PS_TRIM_BOTH
);
1522 // Don't allow empty name
1524 name
= L
"Anonymous";
1529 CStrW
CNetServerWorker::DeduplicatePlayerName(const CStrW
& original
)
1531 CStrW name
= original
;
1533 // Try names "Foo", "Foo (2)", "Foo (3)", etc
1538 for (const CNetServerSession
* session
: m_Sessions
)
1540 if (session
->GetUserName() == name
)
1550 name
= original
+ L
" (" + CStrW::FromUInt(id
++) + L
")";
1554 void CNetServerWorker::SendHolePunchingMessage(const CStr
& ipStr
, u16 port
)
1556 StunClient::SendHolePunchingMessages(m_Host
, ipStr
.c_str(), port
);
1562 CNetServer::CNetServer(bool useLobbyAuth
, int autostartPlayers
) :
1563 m_Worker(new CNetServerWorker(useLobbyAuth
, autostartPlayers
)),
1564 m_LobbyAuth(useLobbyAuth
)
1568 CNetServer::~CNetServer()
1573 bool CNetServer::UseLobbyAuth() const
1578 bool CNetServer::SetupConnection(const u16 port
)
1580 return m_Worker
->SetupConnection(port
);
1583 void CNetServer::StartGame()
1585 CScopeLock
lock(m_Worker
->m_WorkerMutex
);
1586 m_Worker
->m_StartGameQueue
.push_back(true);
1589 void CNetServer::UpdateGameAttributes(JS::MutableHandleValue attrs
, const ScriptInterface
& scriptInterface
)
1591 // Pass the attributes as JSON, since that's the easiest safe
1592 // cross-thread way of passing script data
1593 std::string attrsJSON
= scriptInterface
.StringifyJSON(attrs
, false);
1595 CScopeLock
lock(m_Worker
->m_WorkerMutex
);
1596 m_Worker
->m_GameAttributesQueue
.push_back(attrsJSON
);
1599 void CNetServer::OnLobbyAuth(const CStr
& name
, const CStr
& token
)
1601 CScopeLock
lock(m_Worker
->m_WorkerMutex
);
1602 m_Worker
->m_LobbyAuthQueue
.push_back(std::make_pair(name
, token
));
1605 void CNetServer::SetTurnLength(u32 msecs
)
1607 CScopeLock
lock(m_Worker
->m_WorkerMutex
);
1608 m_Worker
->m_TurnLengthQueue
.push_back(msecs
);
1611 void CNetServer::SendHolePunchingMessage(const CStr
& ip
, u16 port
)
1613 m_Worker
->SendHolePunchingMessage(ip
, port
);