mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-06-16 05:13:58 -07:00
Thus remove the peculiarity to broadcast to clients that are in the gamesetup, loading screen or ingame, but not rejoining ones. Fix "unknown player" errors in the GUI by broadcasting player assignments to rejoining players too. Fixes #4036. Differential Revision: D17 Reviewed By: Imarok This was SVN commit r19171.
1458 lines
43 KiB
C++
1458 lines
43 KiB
C++
/* Copyright (C) 2017 Wildfire Games.
|
|
* This file is part of 0 A.D.
|
|
*
|
|
* 0 A.D. is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* 0 A.D. is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "precompiled.h"
|
|
|
|
#include "NetServer.h"
|
|
|
|
#include "NetClient.h"
|
|
#include "NetMessage.h"
|
|
#include "NetSession.h"
|
|
#include "NetServerTurnManager.h"
|
|
#include "NetStats.h"
|
|
|
|
#include "lib/external_libraries/enet.h"
|
|
#include "ps/CLogger.h"
|
|
#include "ps/ConfigDB.h"
|
|
#include "ps/Profile.h"
|
|
#include "scriptinterface/ScriptInterface.h"
|
|
#include "scriptinterface/ScriptRuntime.h"
|
|
#include "simulation2/Simulation2.h"
|
|
#include "simulation2/system/TurnManager.h"
|
|
|
|
#if CONFIG2_MINIUPNPC
|
|
#include <miniupnpc/miniwget.h>
|
|
#include <miniupnpc/miniupnpc.h>
|
|
#include <miniupnpc/upnpcommands.h>
|
|
#include <miniupnpc/upnperrors.h>
|
|
#endif
|
|
|
|
/**
|
|
* Number of peers to allocate for the enet host.
|
|
* Limited by ENET_PROTOCOL_MAXIMUM_PEER_ID (4096).
|
|
*
|
|
* At most 8 players, 32 observers and 1 temporary connection to send the "server full" disconnect-reason.
|
|
*/
|
|
#define MAX_CLIENTS 41
|
|
|
|
#define DEFAULT_SERVER_NAME L"Unnamed Server"
|
|
#define DEFAULT_WELCOME_MESSAGE L"Welcome"
|
|
|
|
static const int CHANNEL_COUNT = 1;
|
|
|
|
/**
|
|
* enet_host_service timeout (msecs).
|
|
* Smaller numbers may hurt performance; larger numbers will
|
|
* hurt latency responding to messages from game thread.
|
|
*/
|
|
static const int HOST_SERVICE_TIMEOUT = 50;
|
|
|
|
CNetServer* g_NetServer = NULL;
|
|
|
|
static CStr DebugName(CNetServerSession* session)
|
|
{
|
|
if (session == NULL)
|
|
return "[unknown host]";
|
|
if (session->GetGUID().empty())
|
|
return "[unauthed host]";
|
|
return "[" + session->GetGUID().substr(0, 8) + "...]";
|
|
}
|
|
|
|
/**
|
|
* Async task for receiving the initial game state to be forwarded to another
|
|
* client that is rejoining an in-progress network game.
|
|
*/
|
|
class CNetFileReceiveTask_ServerRejoin : public CNetFileReceiveTask
|
|
{
|
|
NONCOPYABLE(CNetFileReceiveTask_ServerRejoin);
|
|
public:
|
|
CNetFileReceiveTask_ServerRejoin(CNetServerWorker& server, u32 hostID)
|
|
: m_Server(server), m_RejoinerHostID(hostID)
|
|
{
|
|
}
|
|
|
|
virtual void OnComplete()
|
|
{
|
|
// We've received the game state from an existing player - now
|
|
// we need to send it onwards to the newly rejoining player
|
|
|
|
// Find the session corresponding to the rejoining host (if any)
|
|
CNetServerSession* session = NULL;
|
|
for (CNetServerSession* serverSession : m_Server.m_Sessions)
|
|
{
|
|
if (serverSession->GetHostID() == m_RejoinerHostID)
|
|
{
|
|
session = serverSession;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!session)
|
|
{
|
|
LOGMESSAGE("Net server: rejoining client disconnected before we sent to it");
|
|
return;
|
|
}
|
|
|
|
// Store the received state file, and tell the client to start downloading it from us
|
|
// TODO: this will get kind of confused if there's multiple clients downloading in parallel;
|
|
// they'll race and get whichever happens to be the latest received by the server,
|
|
// which should still work but isn't great
|
|
m_Server.m_JoinSyncFile = m_Buffer;
|
|
CJoinSyncStartMessage message;
|
|
session->SendMessage(&message);
|
|
}
|
|
|
|
private:
|
|
CNetServerWorker& m_Server;
|
|
u32 m_RejoinerHostID;
|
|
};
|
|
|
|
/*
|
|
* XXX: We use some non-threadsafe functions from the worker thread.
|
|
* See http://trac.wildfiregames.com/ticket/654
|
|
*/
|
|
|
|
CNetServerWorker::CNetServerWorker(int autostartPlayers) :
|
|
m_AutostartPlayers(autostartPlayers),
|
|
m_Shutdown(false),
|
|
m_ScriptInterface(NULL),
|
|
m_NextHostID(1), m_Host(NULL), m_HostGUID(), m_Stats(NULL),
|
|
m_LastConnectionCheck(0)
|
|
{
|
|
m_State = SERVER_STATE_UNCONNECTED;
|
|
|
|
m_ServerTurnManager = NULL;
|
|
|
|
m_ServerName = DEFAULT_SERVER_NAME;
|
|
m_WelcomeMessage = DEFAULT_WELCOME_MESSAGE;
|
|
}
|
|
|
|
CNetServerWorker::~CNetServerWorker()
|
|
{
|
|
if (m_State != SERVER_STATE_UNCONNECTED)
|
|
{
|
|
// Tell the thread to shut down
|
|
{
|
|
CScopeLock lock(m_WorkerMutex);
|
|
m_Shutdown = true;
|
|
}
|
|
|
|
// Wait for it to shut down cleanly
|
|
pthread_join(m_WorkerThread, NULL);
|
|
}
|
|
|
|
// Clean up resources
|
|
|
|
delete m_Stats;
|
|
|
|
for (CNetServerSession* session : m_Sessions)
|
|
{
|
|
session->DisconnectNow(NDR_SERVER_SHUTDOWN);
|
|
delete session;
|
|
}
|
|
|
|
if (m_Host)
|
|
enet_host_destroy(m_Host);
|
|
|
|
delete m_ServerTurnManager;
|
|
}
|
|
|
|
bool CNetServerWorker::SetupConnection(const u16 port)
|
|
{
|
|
ENSURE(m_State == SERVER_STATE_UNCONNECTED);
|
|
ENSURE(!m_Host);
|
|
|
|
// Bind to default host
|
|
ENetAddress addr;
|
|
addr.host = ENET_HOST_ANY;
|
|
addr.port = port;
|
|
|
|
// Create ENet server
|
|
m_Host = enet_host_create(&addr, MAX_CLIENTS, CHANNEL_COUNT, 0, 0);
|
|
if (!m_Host)
|
|
{
|
|
LOGERROR("Net server: enet_host_create failed");
|
|
return false;
|
|
}
|
|
|
|
m_Stats = new CNetStatsTable();
|
|
if (CProfileViewer::IsInitialised())
|
|
g_ProfileViewer.AddRootTable(m_Stats);
|
|
|
|
m_State = SERVER_STATE_PREGAME;
|
|
|
|
// Launch the worker thread
|
|
int ret = pthread_create(&m_WorkerThread, NULL, &RunThread, this);
|
|
ENSURE(ret == 0);
|
|
|
|
#if CONFIG2_MINIUPNPC
|
|
// Launch the UPnP thread
|
|
ret = pthread_create(&m_UPnPThread, NULL, &SetupUPnP, NULL);
|
|
ENSURE(ret == 0);
|
|
#endif
|
|
|
|
return true;
|
|
}
|
|
|
|
#if CONFIG2_MINIUPNPC
|
|
void* CNetServerWorker::SetupUPnP(void*)
|
|
{
|
|
// Values we want to set.
|
|
char psPort[6];
|
|
sprintf_s(psPort, ARRAY_SIZE(psPort), "%d", PS_DEFAULT_PORT);
|
|
const char* leaseDuration = "0"; // Indefinite/permanent lease duration.
|
|
const char* description = "0AD Multiplayer";
|
|
const char* protocall = "UDP";
|
|
char internalIPAddress[64];
|
|
char externalIPAddress[40];
|
|
// Variables to hold the values that actually get set.
|
|
char intClient[40];
|
|
char intPort[6];
|
|
char duration[16];
|
|
// Intermediate variables.
|
|
struct UPNPUrls urls;
|
|
struct IGDdatas data;
|
|
struct UPNPDev* devlist = NULL;
|
|
|
|
// Cached root descriptor URL.
|
|
std::string rootDescURL;
|
|
CFG_GET_VAL("network.upnprootdescurl", rootDescURL);
|
|
if (!rootDescURL.empty())
|
|
LOGMESSAGE("Net server: attempting to use cached root descriptor URL: %s", rootDescURL.c_str());
|
|
|
|
int ret = 0;
|
|
bool allocatedUrls = false;
|
|
|
|
// Try a cached URL first
|
|
if (!rootDescURL.empty() && UPNP_GetIGDFromUrl(rootDescURL.c_str(), &urls, &data, internalIPAddress, sizeof(internalIPAddress)))
|
|
{
|
|
LOGMESSAGE("Net server: using cached IGD = %s", urls.controlURL);
|
|
ret = 1;
|
|
}
|
|
// No cached URL, or it did not respond. Try getting a valid UPnP device for 10 seconds.
|
|
#if defined(MINIUPNPC_API_VERSION) && MINIUPNPC_API_VERSION >= 14
|
|
else if ((devlist = upnpDiscover(10000, 0, 0, 0, 0, 2, 0)) != NULL)
|
|
#else
|
|
else if ((devlist = upnpDiscover(10000, 0, 0, 0, 0, 0)) != NULL)
|
|
#endif
|
|
{
|
|
ret = UPNP_GetValidIGD(devlist, &urls, &data, internalIPAddress, sizeof(internalIPAddress));
|
|
allocatedUrls = ret != 0; // urls is allocated on non-zero return values
|
|
}
|
|
else
|
|
{
|
|
LOGMESSAGE("Net server: upnpDiscover failed and no working cached URL.");
|
|
return NULL;
|
|
}
|
|
|
|
switch (ret)
|
|
{
|
|
case 0:
|
|
LOGMESSAGE("Net server: No IGD found");
|
|
break;
|
|
case 1:
|
|
LOGMESSAGE("Net server: found valid IGD = %s", urls.controlURL);
|
|
break;
|
|
case 2:
|
|
LOGMESSAGE("Net server: found a valid, not connected IGD = %s, will try to continue anyway", urls.controlURL);
|
|
break;
|
|
case 3:
|
|
LOGMESSAGE("Net server: found a UPnP device unrecognized as IGD = %s, will try to continue anyway", urls.controlURL);
|
|
break;
|
|
default:
|
|
debug_warn(L"Unrecognized return value from UPNP_GetValidIGD");
|
|
}
|
|
|
|
// Try getting our external/internet facing IP. TODO: Display this on the game-setup page for conviniance.
|
|
ret = UPNP_GetExternalIPAddress(urls.controlURL, data.first.servicetype, externalIPAddress);
|
|
if (ret != UPNPCOMMAND_SUCCESS)
|
|
{
|
|
LOGMESSAGE("Net server: GetExternalIPAddress failed with code %d (%s)", ret, strupnperror(ret));
|
|
return NULL;
|
|
}
|
|
LOGMESSAGE("Net server: ExternalIPAddress = %s", externalIPAddress);
|
|
|
|
// Try to setup port forwarding.
|
|
ret = UPNP_AddPortMapping(urls.controlURL, data.first.servicetype, psPort, psPort,
|
|
internalIPAddress, description, protocall, 0, leaseDuration);
|
|
if (ret != UPNPCOMMAND_SUCCESS)
|
|
{
|
|
LOGMESSAGE("Net server: AddPortMapping(%s, %s, %s) failed with code %d (%s)",
|
|
psPort, psPort, internalIPAddress, ret, strupnperror(ret));
|
|
return NULL;
|
|
}
|
|
|
|
// Check that the port was actually forwarded.
|
|
ret = UPNP_GetSpecificPortMappingEntry(urls.controlURL,
|
|
data.first.servicetype,
|
|
psPort, protocall,
|
|
#if defined(MINIUPNPC_API_VERSION) && MINIUPNPC_API_VERSION >= 10
|
|
NULL/*remoteHost*/,
|
|
#endif
|
|
intClient, intPort, NULL/*desc*/,
|
|
NULL/*enabled*/, duration);
|
|
|
|
if (ret != UPNPCOMMAND_SUCCESS)
|
|
{
|
|
LOGMESSAGE("Net server: GetSpecificPortMappingEntry() failed with code %d (%s)", ret, strupnperror(ret));
|
|
return NULL;
|
|
}
|
|
|
|
LOGMESSAGE("Net server: External %s:%s %s is redirected to internal %s:%s (duration=%s)",
|
|
externalIPAddress, psPort, protocall, intClient, intPort, duration);
|
|
|
|
// Cache root descriptor URL to try to avoid discovery next time.
|
|
g_ConfigDB.SetValueString(CFG_USER, "network.upnprootdescurl", urls.controlURL);
|
|
g_ConfigDB.WriteValueToFile(CFG_USER, "network.upnprootdescurl", urls.controlURL);
|
|
LOGMESSAGE("Net server: cached UPnP root descriptor URL as %s", urls.controlURL);
|
|
|
|
// Make sure everything is properly freed.
|
|
if (allocatedUrls)
|
|
FreeUPNPUrls(&urls);
|
|
|
|
freeUPNPDevlist(devlist);
|
|
|
|
return NULL;
|
|
}
|
|
#endif // CONFIG2_MINIUPNPC
|
|
|
|
bool CNetServerWorker::SendMessage(ENetPeer* peer, const CNetMessage* message)
|
|
{
|
|
ENSURE(m_Host);
|
|
|
|
CNetServerSession* session = static_cast<CNetServerSession*>(peer->data);
|
|
|
|
return CNetHost::SendMessage(message, peer, DebugName(session).c_str());
|
|
}
|
|
|
|
bool CNetServerWorker::Broadcast(const CNetMessage* message, const std::vector<NetServerSessionState>& targetStates)
|
|
{
|
|
ENSURE(m_Host);
|
|
|
|
bool ok = true;
|
|
|
|
// TODO: this does lots of repeated message serialisation if we have lots
|
|
// of remote peers; could do it more efficiently if that's a real problem
|
|
|
|
for (CNetServerSession* session : m_Sessions)
|
|
if (std::find(targetStates.begin(), targetStates.end(), session->GetCurrState()) != targetStates.end() &&
|
|
!session->SendMessage(message))
|
|
ok = false;
|
|
|
|
return ok;
|
|
}
|
|
|
|
void* CNetServerWorker::RunThread(void* data)
|
|
{
|
|
debug_SetThreadName("NetServer");
|
|
|
|
static_cast<CNetServerWorker*>(data)->Run();
|
|
|
|
return NULL;
|
|
}
|
|
|
|
void CNetServerWorker::Run()
|
|
{
|
|
// The script runtime uses the profiler and therefore the thread must be registered before the runtime is created
|
|
g_Profiler2.RegisterCurrentThread("Net server");
|
|
|
|
// To avoid the need for JS_SetContextThread, we create and use and destroy
|
|
// the script interface entirely within this network thread
|
|
m_ScriptInterface = new ScriptInterface("Engine", "Net server", ScriptInterface::CreateRuntime(g_ScriptRuntime));
|
|
m_GameAttributes.init(m_ScriptInterface->GetJSRuntime(), JS::UndefinedValue());
|
|
|
|
while (true)
|
|
{
|
|
if (!RunStep())
|
|
break;
|
|
|
|
// Implement autostart mode
|
|
if (m_State == SERVER_STATE_PREGAME && (int)m_PlayerAssignments.size() == m_AutostartPlayers)
|
|
StartGame();
|
|
|
|
// Update profiler stats
|
|
m_Stats->LatchHostState(m_Host);
|
|
}
|
|
|
|
// Clear roots before deleting their context
|
|
m_SavedCommands.clear();
|
|
|
|
SAFE_DELETE(m_ScriptInterface);
|
|
}
|
|
|
|
bool CNetServerWorker::RunStep()
|
|
{
|
|
// Check for messages from the game thread.
|
|
// (Do as little work as possible while the mutex is held open,
|
|
// to avoid performance problems and deadlocks.)
|
|
|
|
m_ScriptInterface->GetRuntime()->MaybeIncrementalGC(0.5f);
|
|
|
|
JSContext* cx = m_ScriptInterface->GetContext();
|
|
JSAutoRequest rq(cx);
|
|
|
|
std::vector<bool> newStartGame;
|
|
std::vector<std::string> newGameAttributes;
|
|
std::vector<u32> newTurnLength;
|
|
|
|
{
|
|
CScopeLock lock(m_WorkerMutex);
|
|
|
|
if (m_Shutdown)
|
|
return false;
|
|
|
|
newStartGame.swap(m_StartGameQueue);
|
|
newGameAttributes.swap(m_GameAttributesQueue);
|
|
newTurnLength.swap(m_TurnLengthQueue);
|
|
}
|
|
|
|
if (!newGameAttributes.empty())
|
|
{
|
|
JS::RootedValue gameAttributesVal(cx);
|
|
GetScriptInterface().ParseJSON(newGameAttributes.back(), &gameAttributesVal);
|
|
UpdateGameAttributes(&gameAttributesVal);
|
|
}
|
|
|
|
if (!newTurnLength.empty())
|
|
SetTurnLength(newTurnLength.back());
|
|
|
|
// Do StartGame last, so we have the most up-to-date game attributes when we start
|
|
if (!newStartGame.empty())
|
|
StartGame();
|
|
|
|
// Perform file transfers
|
|
for (CNetServerSession* session : m_Sessions)
|
|
session->GetFileTransferer().Poll();
|
|
|
|
CheckClientConnections();
|
|
|
|
// Process network events:
|
|
|
|
ENetEvent event;
|
|
int status = enet_host_service(m_Host, &event, HOST_SERVICE_TIMEOUT);
|
|
if (status < 0)
|
|
{
|
|
LOGERROR("CNetServerWorker: enet_host_service failed (%d)", status);
|
|
// TODO: notify game that the server has shut down
|
|
return false;
|
|
}
|
|
|
|
if (status == 0)
|
|
{
|
|
// Reached timeout with no events - try again
|
|
return true;
|
|
}
|
|
|
|
// Process the event:
|
|
|
|
switch (event.type)
|
|
{
|
|
case ENET_EVENT_TYPE_CONNECT:
|
|
{
|
|
// Report the client address
|
|
char hostname[256] = "(error)";
|
|
enet_address_get_host_ip(&event.peer->address, hostname, ARRAY_SIZE(hostname));
|
|
LOGMESSAGE("Net server: Received connection from %s:%u", hostname, (unsigned int)event.peer->address.port);
|
|
|
|
// Set up a session object for this peer
|
|
|
|
CNetServerSession* session = new CNetServerSession(*this, event.peer);
|
|
|
|
m_Sessions.push_back(session);
|
|
|
|
SetupSession(session);
|
|
|
|
ENSURE(event.peer->data == NULL);
|
|
event.peer->data = session;
|
|
|
|
HandleConnect(session);
|
|
|
|
break;
|
|
}
|
|
|
|
case ENET_EVENT_TYPE_DISCONNECT:
|
|
{
|
|
// If there is an active session with this peer, then reset and delete it
|
|
|
|
CNetServerSession* session = static_cast<CNetServerSession*>(event.peer->data);
|
|
if (session)
|
|
{
|
|
LOGMESSAGE("Net server: Disconnected %s", DebugName(session).c_str());
|
|
|
|
// Remove the session first, so we won't send player-update messages to it
|
|
// when updating the FSM
|
|
m_Sessions.erase(remove(m_Sessions.begin(), m_Sessions.end(), session), m_Sessions.end());
|
|
|
|
session->Update((uint)NMT_CONNECTION_LOST, NULL);
|
|
|
|
delete session;
|
|
event.peer->data = NULL;
|
|
}
|
|
|
|
if (m_State == SERVER_STATE_LOADING)
|
|
CheckGameLoadStatus(NULL);
|
|
|
|
break;
|
|
}
|
|
|
|
case ENET_EVENT_TYPE_RECEIVE:
|
|
{
|
|
// If there is an active session with this peer, then process the message
|
|
|
|
CNetServerSession* session = static_cast<CNetServerSession*>(event.peer->data);
|
|
if (session)
|
|
{
|
|
// Create message from raw data
|
|
CNetMessage* msg = CNetMessageFactory::CreateMessage(event.packet->data, event.packet->dataLength, GetScriptInterface());
|
|
if (msg)
|
|
{
|
|
LOGMESSAGE("Net server: Received message %s of size %lu from %s", msg->ToString().c_str(), (unsigned long)msg->GetSerializedLength(), DebugName(session).c_str());
|
|
|
|
HandleMessageReceive(msg, session);
|
|
|
|
delete msg;
|
|
}
|
|
}
|
|
|
|
// Done using the packet
|
|
enet_packet_destroy(event.packet);
|
|
|
|
break;
|
|
}
|
|
|
|
case ENET_EVENT_TYPE_NONE:
|
|
break;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void CNetServerWorker::CheckClientConnections()
|
|
{
|
|
if (m_State == SERVER_STATE_LOADING)
|
|
return;
|
|
|
|
// Send messages at most once per second
|
|
std::time_t now = std::time(nullptr);
|
|
if (now <= m_LastConnectionCheck)
|
|
return;
|
|
|
|
m_LastConnectionCheck = now;
|
|
|
|
for (size_t i = 0; i < m_Sessions.size(); ++i)
|
|
{
|
|
u32 lastReceived = m_Sessions[i]->GetLastReceivedTime();
|
|
u32 meanRTT = m_Sessions[i]->GetMeanRTT();
|
|
|
|
CNetMessage* message = nullptr;
|
|
|
|
// Report if we didn't hear from the client since few seconds
|
|
if (lastReceived > NETWORK_WARNING_TIMEOUT)
|
|
{
|
|
CClientTimeoutMessage* msg = new CClientTimeoutMessage();
|
|
msg->m_GUID = m_Sessions[i]->GetGUID();
|
|
msg->m_LastReceivedTime = lastReceived;
|
|
message = msg;
|
|
}
|
|
// Report if the client has bad ping
|
|
else if (meanRTT > DEFAULT_TURN_LENGTH_MP)
|
|
{
|
|
CClientPerformanceMessage* msg = new CClientPerformanceMessage();
|
|
CClientPerformanceMessage::S_m_Clients client;
|
|
client.m_GUID = m_Sessions[i]->GetGUID();
|
|
client.m_MeanRTT = meanRTT;
|
|
msg->m_Clients.push_back(client);
|
|
message = msg;
|
|
}
|
|
|
|
// Send to all clients except the affected one
|
|
// (since that will show the locally triggered warning instead)
|
|
if (message)
|
|
for (size_t j = 0; j < m_Sessions.size(); ++j)
|
|
{
|
|
if (i != j && (
|
|
m_Sessions[j]->GetCurrState() == NSS_PREGAME ||
|
|
m_Sessions[j]->GetCurrState() == NSS_INGAME))
|
|
{
|
|
m_Sessions[j]->SendMessage(message);
|
|
}
|
|
}
|
|
|
|
SAFE_DELETE(message);
|
|
}
|
|
}
|
|
|
|
void CNetServerWorker::HandleMessageReceive(const CNetMessage* message, CNetServerSession* session)
|
|
{
|
|
// Handle non-FSM messages first
|
|
Status status = session->GetFileTransferer().HandleMessageReceive(message);
|
|
if (status != INFO::SKIPPED)
|
|
return;
|
|
|
|
if (message->GetType() == NMT_FILE_TRANSFER_REQUEST)
|
|
{
|
|
CFileTransferRequestMessage* reqMessage = (CFileTransferRequestMessage*)message;
|
|
|
|
// Rejoining client got our JoinSyncStart after we received the state from
|
|
// another client, and has now requested that we forward it to them
|
|
|
|
ENSURE(!m_JoinSyncFile.empty());
|
|
session->GetFileTransferer().StartResponse(reqMessage->m_RequestID, m_JoinSyncFile);
|
|
|
|
return;
|
|
}
|
|
|
|
// Update FSM
|
|
if (!session->Update(message->GetType(), (void*)message))
|
|
LOGERROR("Net server: Error running FSM update (type=%d state=%d)", (int)message->GetType(), (int)session->GetCurrState());
|
|
}
|
|
|
|
void CNetServerWorker::SetupSession(CNetServerSession* session)
|
|
{
|
|
void* context = session;
|
|
|
|
// Set up transitions for session
|
|
|
|
session->AddTransition(NSS_UNCONNECTED, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
|
|
|
|
session->AddTransition(NSS_HANDSHAKE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
|
|
session->AddTransition(NSS_HANDSHAKE, (uint)NMT_CLIENT_HANDSHAKE, NSS_AUTHENTICATE, (void*)&OnClientHandshake, context);
|
|
|
|
session->AddTransition(NSS_AUTHENTICATE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
|
|
session->AddTransition(NSS_AUTHENTICATE, (uint)NMT_AUTHENTICATE, NSS_PREGAME, (void*)&OnAuthenticate, context);
|
|
|
|
session->AddTransition(NSS_PREGAME, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, (void*)&OnDisconnect, context);
|
|
session->AddTransition(NSS_PREGAME, (uint)NMT_CHAT, NSS_PREGAME, (void*)&OnChat, context);
|
|
session->AddTransition(NSS_PREGAME, (uint)NMT_READY, NSS_PREGAME, (void*)&OnReady, context);
|
|
session->AddTransition(NSS_PREGAME, (uint)NMT_CLEAR_ALL_READY, NSS_PREGAME, (void*)&OnClearAllReady, context);
|
|
session->AddTransition(NSS_PREGAME, (uint)NMT_GAME_SETUP, NSS_PREGAME, (void*)&OnGameSetup, context);
|
|
session->AddTransition(NSS_PREGAME, (uint)NMT_ASSIGN_PLAYER, NSS_PREGAME, (void*)&OnAssignPlayer, context);
|
|
session->AddTransition(NSS_PREGAME, (uint)NMT_KICKED, NSS_PREGAME, (void*)&OnKickPlayer, context);
|
|
session->AddTransition(NSS_PREGAME, (uint)NMT_GAME_START, NSS_PREGAME, (void*)&OnStartGame, context);
|
|
session->AddTransition(NSS_PREGAME, (uint)NMT_LOADED_GAME, NSS_INGAME, (void*)&OnLoadedGame, context);
|
|
|
|
session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_KICKED, NSS_JOIN_SYNCING, (void*)&OnKickPlayer, context);
|
|
session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, (void*)&OnDisconnect, context);
|
|
session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_LOADED_GAME, NSS_INGAME, (void*)&OnJoinSyncingLoadedGame, context);
|
|
|
|
session->AddTransition(NSS_INGAME, (uint)NMT_REJOINED, NSS_INGAME, (void*)&OnRejoined, context);
|
|
session->AddTransition(NSS_INGAME, (uint)NMT_KICKED, NSS_INGAME, (void*)&OnKickPlayer, context);
|
|
session->AddTransition(NSS_INGAME, (uint)NMT_CLIENT_PAUSED, NSS_INGAME, (void*)&OnClientPaused, context);
|
|
session->AddTransition(NSS_INGAME, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, (void*)&OnDisconnect, context);
|
|
session->AddTransition(NSS_INGAME, (uint)NMT_CHAT, NSS_INGAME, (void*)&OnChat, context);
|
|
session->AddTransition(NSS_INGAME, (uint)NMT_SIMULATION_COMMAND, NSS_INGAME, (void*)&OnInGame, context);
|
|
session->AddTransition(NSS_INGAME, (uint)NMT_SYNC_CHECK, NSS_INGAME, (void*)&OnInGame, context);
|
|
session->AddTransition(NSS_INGAME, (uint)NMT_END_COMMAND_BATCH, NSS_INGAME, (void*)&OnInGame, context);
|
|
|
|
// Set first state
|
|
session->SetFirstState(NSS_HANDSHAKE);
|
|
}
|
|
|
|
bool CNetServerWorker::HandleConnect(CNetServerSession* session)
|
|
{
|
|
if (std::find(m_BannedIPs.begin(), m_BannedIPs.end(), session->GetIPAddress()) != m_BannedIPs.end())
|
|
{
|
|
session->Disconnect(NDR_BANNED);
|
|
return false;
|
|
}
|
|
|
|
CSrvHandshakeMessage handshake;
|
|
handshake.m_Magic = PS_PROTOCOL_MAGIC;
|
|
handshake.m_ProtocolVersion = PS_PROTOCOL_VERSION;
|
|
handshake.m_SoftwareVersion = PS_PROTOCOL_VERSION;
|
|
return session->SendMessage(&handshake);
|
|
}
|
|
|
|
void CNetServerWorker::OnUserJoin(CNetServerSession* session)
|
|
{
|
|
AddPlayer(session->GetGUID(), session->GetUserName());
|
|
|
|
if (m_HostGUID.empty() && session->IsLocalClient())
|
|
m_HostGUID = session->GetGUID();
|
|
|
|
CGameSetupMessage gameSetupMessage(GetScriptInterface());
|
|
gameSetupMessage.m_Data = m_GameAttributes;
|
|
session->SendMessage(&gameSetupMessage);
|
|
|
|
CPlayerAssignmentMessage assignMessage;
|
|
ConstructPlayerAssignmentMessage(assignMessage);
|
|
session->SendMessage(&assignMessage);
|
|
}
|
|
|
|
void CNetServerWorker::OnUserLeave(CNetServerSession* session)
|
|
{
|
|
std::vector<CStr>::iterator pausing = std::find(m_PausingPlayers.begin(), m_PausingPlayers.end(), session->GetGUID());
|
|
if (pausing != m_PausingPlayers.end())
|
|
m_PausingPlayers.erase(pausing);
|
|
|
|
RemovePlayer(session->GetGUID());
|
|
|
|
if (m_ServerTurnManager && session->GetCurrState() != NSS_JOIN_SYNCING)
|
|
m_ServerTurnManager->UninitialiseClient(session->GetHostID()); // TODO: only for non-observers
|
|
|
|
// TODO: ought to switch the player controlled by that client
|
|
// back to AI control, or something?
|
|
}
|
|
|
|
void CNetServerWorker::AddPlayer(const CStr& guid, const CStrW& name)
|
|
{
|
|
// Find all player IDs in active use; we mustn't give them to a second player (excluding the unassigned ID: -1)
|
|
std::set<i32> usedIDs;
|
|
for (const std::pair<CStr, PlayerAssignment>& p : m_PlayerAssignments)
|
|
if (p.second.m_Enabled && p.second.m_PlayerID != -1)
|
|
usedIDs.insert(p.second.m_PlayerID);
|
|
|
|
// If the player is rejoining after disconnecting, try to give them
|
|
// back their old player ID
|
|
|
|
i32 playerID = -1;
|
|
|
|
// Try to match GUID first
|
|
for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it)
|
|
{
|
|
if (!it->second.m_Enabled && it->first == guid && usedIDs.find(it->second.m_PlayerID) == usedIDs.end())
|
|
{
|
|
playerID = it->second.m_PlayerID;
|
|
m_PlayerAssignments.erase(it); // delete the old mapping, since we've got a new one now
|
|
goto found;
|
|
}
|
|
}
|
|
|
|
// Try to match username next
|
|
for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it)
|
|
{
|
|
if (!it->second.m_Enabled && it->second.m_Name == name && usedIDs.find(it->second.m_PlayerID) == usedIDs.end())
|
|
{
|
|
playerID = it->second.m_PlayerID;
|
|
m_PlayerAssignments.erase(it); // delete the old mapping, since we've got a new one now
|
|
goto found;
|
|
}
|
|
}
|
|
|
|
// Otherwise leave the player ID as -1 (observer) and let gamesetup change it as needed.
|
|
|
|
found:
|
|
PlayerAssignment assignment;
|
|
assignment.m_Enabled = true;
|
|
assignment.m_Name = name;
|
|
assignment.m_PlayerID = playerID;
|
|
assignment.m_Status = 0;
|
|
m_PlayerAssignments[guid] = assignment;
|
|
|
|
// Send the new assignments to all currently active players
|
|
// (which does not include the one that's just joining)
|
|
SendPlayerAssignments();
|
|
}
|
|
|
|
void CNetServerWorker::RemovePlayer(const CStr& guid)
|
|
{
|
|
m_PlayerAssignments[guid].m_Enabled = false;
|
|
|
|
SendPlayerAssignments();
|
|
}
|
|
|
|
void CNetServerWorker::SetPlayerReady(const CStr& guid, const int ready)
|
|
{
|
|
m_PlayerAssignments[guid].m_Status = ready;
|
|
|
|
SendPlayerAssignments();
|
|
}
|
|
|
|
void CNetServerWorker::ClearAllPlayerReady()
|
|
{
|
|
for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it)
|
|
it->second.m_Status = 0;
|
|
|
|
SendPlayerAssignments();
|
|
}
|
|
|
|
void CNetServerWorker::KickPlayer(const CStrW& playerName, const bool ban)
|
|
{
|
|
// Find the user with that name
|
|
std::vector<CNetServerSession*>::iterator it = std::find_if(m_Sessions.begin(), m_Sessions.end(),
|
|
[&](CNetServerSession* session) { return session->GetUserName() == playerName; });
|
|
|
|
// and return if no one or the host has that name
|
|
if (it == m_Sessions.end() || (*it)->GetGUID() == m_HostGUID)
|
|
return;
|
|
|
|
if (ban)
|
|
{
|
|
// Remember name
|
|
if (std::find(m_BannedPlayers.begin(), m_BannedPlayers.end(), playerName) == m_BannedPlayers.end())
|
|
m_BannedPlayers.push_back(playerName);
|
|
|
|
// Remember IP address
|
|
u32 ipAddress = (*it)->GetIPAddress();
|
|
if (std::find(m_BannedIPs.begin(), m_BannedIPs.end(), ipAddress) == m_BannedIPs.end())
|
|
m_BannedIPs.push_back(ipAddress);
|
|
}
|
|
|
|
// Disconnect that user
|
|
(*it)->Disconnect(ban ? NDR_BANNED : NDR_KICKED);
|
|
|
|
// Send message notifying other clients
|
|
CKickedMessage kickedMessage;
|
|
kickedMessage.m_Name = playerName;
|
|
kickedMessage.m_Ban = ban;
|
|
Broadcast(&kickedMessage, { NSS_PREGAME, NSS_JOIN_SYNCING, NSS_INGAME });
|
|
}
|
|
|
|
void CNetServerWorker::AssignPlayer(int playerID, const CStr& guid)
|
|
{
|
|
// Remove anyone who's already assigned to this player
|
|
for (std::pair<const CStr, PlayerAssignment>& p : m_PlayerAssignments)
|
|
{
|
|
if (p.second.m_PlayerID == playerID)
|
|
p.second.m_PlayerID = -1;
|
|
}
|
|
|
|
// Update this host's assignment if it exists
|
|
if (m_PlayerAssignments.find(guid) != m_PlayerAssignments.end())
|
|
m_PlayerAssignments[guid].m_PlayerID = playerID;
|
|
|
|
SendPlayerAssignments();
|
|
}
|
|
|
|
void CNetServerWorker::ConstructPlayerAssignmentMessage(CPlayerAssignmentMessage& message)
|
|
{
|
|
for (const std::pair<CStr, PlayerAssignment>& p : m_PlayerAssignments)
|
|
{
|
|
if (!p.second.m_Enabled)
|
|
continue;
|
|
|
|
CPlayerAssignmentMessage::S_m_Hosts h;
|
|
h.m_GUID = p.first;
|
|
h.m_Name = p.second.m_Name;
|
|
h.m_PlayerID = p.second.m_PlayerID;
|
|
h.m_Status = p.second.m_Status;
|
|
message.m_Hosts.push_back(h);
|
|
}
|
|
}
|
|
|
|
void CNetServerWorker::SendPlayerAssignments()
|
|
{
|
|
CPlayerAssignmentMessage message;
|
|
ConstructPlayerAssignmentMessage(message);
|
|
Broadcast(&message, { NSS_PREGAME, NSS_JOIN_SYNCING, NSS_INGAME });
|
|
}
|
|
|
|
ScriptInterface& CNetServerWorker::GetScriptInterface()
|
|
{
|
|
return *m_ScriptInterface;
|
|
}
|
|
|
|
void CNetServerWorker::SetTurnLength(u32 msecs)
|
|
{
|
|
if (m_ServerTurnManager)
|
|
m_ServerTurnManager->SetTurnLength(msecs);
|
|
}
|
|
|
|
bool CNetServerWorker::OnClientHandshake(void* context, CFsmEvent* event)
|
|
{
|
|
ENSURE(event->GetType() == (uint)NMT_CLIENT_HANDSHAKE);
|
|
|
|
CNetServerSession* session = (CNetServerSession*)context;
|
|
CNetServerWorker& server = session->GetServer();
|
|
|
|
CCliHandshakeMessage* message = (CCliHandshakeMessage*)event->GetParamRef();
|
|
if (message->m_ProtocolVersion != PS_PROTOCOL_VERSION)
|
|
{
|
|
session->Disconnect(NDR_INCORRECT_PROTOCOL_VERSION);
|
|
return false;
|
|
}
|
|
|
|
CSrvHandshakeResponseMessage handshakeResponse;
|
|
handshakeResponse.m_UseProtocolVersion = PS_PROTOCOL_VERSION;
|
|
handshakeResponse.m_Message = server.m_WelcomeMessage;
|
|
handshakeResponse.m_Flags = 0;
|
|
session->SendMessage(&handshakeResponse);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool CNetServerWorker::OnAuthenticate(void* context, CFsmEvent* event)
|
|
{
|
|
ENSURE(event->GetType() == (uint)NMT_AUTHENTICATE);
|
|
|
|
CNetServerSession* session = (CNetServerSession*)context;
|
|
CNetServerWorker& server = session->GetServer();
|
|
|
|
// Prohibit joins while the game is loading
|
|
if (server.m_State == SERVER_STATE_LOADING)
|
|
{
|
|
LOGMESSAGE("Refused connection while the game is loading");
|
|
session->Disconnect(NDR_SERVER_LOADING);
|
|
return true;
|
|
}
|
|
|
|
CAuthenticateMessage* message = (CAuthenticateMessage*)event->GetParamRef();
|
|
CStrW username = SanitisePlayerName(message->m_Name);
|
|
|
|
// Either deduplicate or prohibit join if name is in use
|
|
bool duplicatePlayernames = false;
|
|
CFG_GET_VAL("network.duplicateplayernames", duplicatePlayernames);
|
|
if (duplicatePlayernames)
|
|
username = server.DeduplicatePlayerName(username);
|
|
else if (std::find_if(
|
|
server.m_Sessions.begin(), server.m_Sessions.end(),
|
|
[&username] (const CNetServerSession* session)
|
|
{ return session->GetUserName() == username; })
|
|
!= server.m_Sessions.end())
|
|
{
|
|
session->Disconnect(NDR_PLAYERNAME_IN_USE);
|
|
return true;
|
|
}
|
|
|
|
// Disconnect banned usernames
|
|
if (std::find(server.m_BannedPlayers.begin(), server.m_BannedPlayers.end(), username) != server.m_BannedPlayers.end())
|
|
{
|
|
session->Disconnect(NDR_BANNED);
|
|
return true;
|
|
}
|
|
|
|
// Optionally allow observers to join after the game has started
|
|
bool observerLateJoin = false;
|
|
CFG_GET_VAL("network.lateobserverjoins", observerLateJoin);
|
|
|
|
int maxObservers = 0;
|
|
CFG_GET_VAL("network.observerlimit", maxObservers);
|
|
|
|
bool isRejoining = false;
|
|
bool serverFull = false;
|
|
if (server.m_State == SERVER_STATE_PREGAME)
|
|
{
|
|
// Don't check for maxObservers in the gamesetup, as we don't know yet who will be assigned
|
|
serverFull = server.m_Sessions.size() >= MAX_CLIENTS;
|
|
}
|
|
else
|
|
{
|
|
isRejoining = observerLateJoin;
|
|
bool isObserver = true;
|
|
int disconnectedPlayers = 0;
|
|
int connectedPlayers = 0;
|
|
// (TODO: if GUIDs were stable, we should use them instead)
|
|
for (const std::pair<CStr, PlayerAssignment>& p : server.m_PlayerAssignments)
|
|
{
|
|
const PlayerAssignment& assignment = p.second;
|
|
|
|
if (!assignment.m_Enabled && assignment.m_Name == username)
|
|
{
|
|
isObserver = assignment.m_PlayerID == -1;
|
|
isRejoining = true;
|
|
}
|
|
|
|
if (assignment.m_PlayerID == -1)
|
|
continue;
|
|
|
|
if (assignment.m_Enabled)
|
|
++connectedPlayers;
|
|
else
|
|
++disconnectedPlayers;
|
|
}
|
|
|
|
// Players who weren't already in the game are not allowed to join now that it's started
|
|
if (!isRejoining)
|
|
{
|
|
LOGMESSAGE("Refused connection after game start from not-previously-known user \"%s\"", utf8_from_wstring(username));
|
|
session->Disconnect(NDR_SERVER_ALREADY_IN_GAME);
|
|
return true;
|
|
}
|
|
|
|
// Ensure all players will be able to rejoin
|
|
serverFull = isObserver && (
|
|
(int) server.m_Sessions.size() - connectedPlayers > maxObservers ||
|
|
(int) server.m_Sessions.size() + disconnectedPlayers >= MAX_CLIENTS);
|
|
}
|
|
|
|
if (serverFull)
|
|
{
|
|
session->Disconnect(NDR_SERVER_FULL);
|
|
return true;
|
|
}
|
|
|
|
// TODO: check server password etc?
|
|
|
|
u32 newHostID = server.m_NextHostID++;
|
|
|
|
session->SetUserName(username);
|
|
session->SetGUID(message->m_GUID);
|
|
session->SetHostID(newHostID);
|
|
session->SetLocalClient(message->m_IsLocalClient);
|
|
|
|
CAuthenticateResultMessage authenticateResult;
|
|
authenticateResult.m_Code = isRejoining ? ARC_OK_REJOINING : ARC_OK;
|
|
authenticateResult.m_HostID = newHostID;
|
|
authenticateResult.m_Message = L"Logged in";
|
|
session->SendMessage(&authenticateResult);
|
|
|
|
server.OnUserJoin(session);
|
|
|
|
if (isRejoining)
|
|
{
|
|
// Request a copy of the current game state from an existing player,
|
|
// so we can send it on to the new player
|
|
|
|
// Assume session 0 is most likely the local player, so they're
|
|
// the most efficient client to request a copy from
|
|
CNetServerSession* sourceSession = server.m_Sessions.at(0);
|
|
sourceSession->GetFileTransferer().StartTask(
|
|
shared_ptr<CNetFileReceiveTask>(new CNetFileReceiveTask_ServerRejoin(server, newHostID))
|
|
);
|
|
|
|
session->SetNextState(NSS_JOIN_SYNCING);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool CNetServerWorker::OnInGame(void* context, CFsmEvent* event)
|
|
{
|
|
// TODO: should split each of these cases into a separate method
|
|
|
|
CNetServerSession* session = (CNetServerSession*)context;
|
|
CNetServerWorker& server = session->GetServer();
|
|
|
|
CNetMessage* message = (CNetMessage*)event->GetParamRef();
|
|
if (message->GetType() == (uint)NMT_SIMULATION_COMMAND)
|
|
{
|
|
CSimulationMessage* simMessage = static_cast<CSimulationMessage*> (message);
|
|
|
|
// Ignore messages sent by one player on behalf of another player
|
|
// unless cheating is enabled
|
|
bool cheatsEnabled = false;
|
|
ScriptInterface& scriptInterface = server.GetScriptInterface();
|
|
JSContext* cx = scriptInterface.GetContext();
|
|
JSAutoRequest rq(cx);
|
|
JS::RootedValue settings(cx);
|
|
scriptInterface.GetProperty(server.m_GameAttributes, "settings", &settings);
|
|
if (scriptInterface.HasProperty(settings, "CheatsEnabled"))
|
|
scriptInterface.GetProperty(settings, "CheatsEnabled", cheatsEnabled);
|
|
|
|
PlayerAssignmentMap::iterator it = server.m_PlayerAssignments.find(session->GetGUID());
|
|
// When cheating is disabled, fail if the player the message claims to
|
|
// represent does not exist or does not match the sender's player name
|
|
if (!cheatsEnabled && (it == server.m_PlayerAssignments.end() || it->second.m_PlayerID != simMessage->m_Player))
|
|
return true;
|
|
|
|
// Send it back to all clients that have finished
|
|
// the loading screen (and the synchronization when rejoining)
|
|
server.Broadcast(simMessage, { NSS_INGAME });
|
|
|
|
// Save all the received commands
|
|
if (server.m_SavedCommands.size() < simMessage->m_Turn + 1)
|
|
server.m_SavedCommands.resize(simMessage->m_Turn + 1);
|
|
server.m_SavedCommands[simMessage->m_Turn].push_back(*simMessage);
|
|
|
|
// TODO: we shouldn't send the message back to the client that first sent it
|
|
}
|
|
else if (message->GetType() == (uint)NMT_SYNC_CHECK)
|
|
{
|
|
CSyncCheckMessage* syncMessage = static_cast<CSyncCheckMessage*> (message);
|
|
server.m_ServerTurnManager->NotifyFinishedClientUpdate(session->GetHostID(), session->GetUserName(), syncMessage->m_Turn, syncMessage->m_Hash);
|
|
}
|
|
else if (message->GetType() == (uint)NMT_END_COMMAND_BATCH)
|
|
{
|
|
// The turn-length field is ignored
|
|
CEndCommandBatchMessage* endMessage = static_cast<CEndCommandBatchMessage*> (message);
|
|
server.m_ServerTurnManager->NotifyFinishedClientCommands(session->GetHostID(), endMessage->m_Turn);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool CNetServerWorker::OnChat(void* context, CFsmEvent* event)
|
|
{
|
|
ENSURE(event->GetType() == (uint)NMT_CHAT);
|
|
|
|
CNetServerSession* session = (CNetServerSession*)context;
|
|
CNetServerWorker& server = session->GetServer();
|
|
|
|
CChatMessage* message = (CChatMessage*)event->GetParamRef();
|
|
|
|
message->m_GUID = session->GetGUID();
|
|
|
|
server.Broadcast(message, { NSS_PREGAME, NSS_INGAME });
|
|
|
|
return true;
|
|
}
|
|
|
|
bool CNetServerWorker::OnReady(void* context, CFsmEvent* event)
|
|
{
|
|
ENSURE(event->GetType() == (uint)NMT_READY);
|
|
|
|
CNetServerSession* session = (CNetServerSession*)context;
|
|
CNetServerWorker& server = session->GetServer();
|
|
|
|
CReadyMessage* message = (CReadyMessage*)event->GetParamRef();
|
|
|
|
message->m_GUID = session->GetGUID();
|
|
|
|
server.Broadcast(message, { NSS_PREGAME });
|
|
server.SetPlayerReady(message->m_GUID, message->m_Status);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool CNetServerWorker::OnClearAllReady(void* context, CFsmEvent* event)
|
|
{
|
|
ENSURE(event->GetType() == (uint)NMT_CLEAR_ALL_READY);
|
|
|
|
CNetServerSession* session = (CNetServerSession*)context;
|
|
CNetServerWorker& server = session->GetServer();
|
|
|
|
if (session->GetGUID() == server.m_HostGUID)
|
|
server.ClearAllPlayerReady();
|
|
|
|
return true;
|
|
}
|
|
|
|
bool CNetServerWorker::OnGameSetup(void* context, CFsmEvent* event)
|
|
{
|
|
ENSURE(event->GetType() == (uint)NMT_GAME_SETUP);
|
|
|
|
CNetServerSession* session = (CNetServerSession*)context;
|
|
CNetServerWorker& server = session->GetServer();
|
|
|
|
if (session->GetGUID() == server.m_HostGUID)
|
|
{
|
|
CGameSetupMessage* message = (CGameSetupMessage*)event->GetParamRef();
|
|
server.UpdateGameAttributes(&(message->m_Data));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool CNetServerWorker::OnAssignPlayer(void* context, CFsmEvent* event)
|
|
{
|
|
ENSURE(event->GetType() == (uint)NMT_ASSIGN_PLAYER);
|
|
CNetServerSession* session = (CNetServerSession*)context;
|
|
CNetServerWorker& server = session->GetServer();
|
|
|
|
if (session->GetGUID() == server.m_HostGUID)
|
|
{
|
|
CAssignPlayerMessage* message = (CAssignPlayerMessage*)event->GetParamRef();
|
|
server.AssignPlayer(message->m_PlayerID, message->m_GUID);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool CNetServerWorker::OnStartGame(void* context, CFsmEvent* event)
|
|
{
|
|
ENSURE(event->GetType() == (uint)NMT_GAME_START);
|
|
CNetServerSession* session = (CNetServerSession*)context;
|
|
CNetServerWorker& server = session->GetServer();
|
|
|
|
if (session->GetGUID() == server.m_HostGUID)
|
|
server.StartGame();
|
|
|
|
return true;
|
|
}
|
|
|
|
bool CNetServerWorker::OnLoadedGame(void* context, CFsmEvent* event)
|
|
{
|
|
ENSURE(event->GetType() == (uint)NMT_LOADED_GAME);
|
|
|
|
CNetServerSession* session = (CNetServerSession*)context;
|
|
CNetServerWorker& server = session->GetServer();
|
|
|
|
// We're in the loading state, so wait until every player has loaded before
|
|
// starting the game
|
|
ENSURE(server.m_State == SERVER_STATE_LOADING);
|
|
server.CheckGameLoadStatus(session);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool CNetServerWorker::OnJoinSyncingLoadedGame(void* context, CFsmEvent* event)
|
|
{
|
|
// A client rejoining an in-progress game has now finished loading the
|
|
// map and deserialized the initial state.
|
|
// The simulation may have progressed since then, so send any subsequent
|
|
// commands to them and set them as an active player so they can participate
|
|
// in all future turns.
|
|
//
|
|
// (TODO: if it takes a long time for them to receive and execute all these
|
|
// commands, the other players will get frozen for that time and may be unhappy;
|
|
// we could try repeating this process a few times until the client converges
|
|
// on the up-to-date state, before setting them as active.)
|
|
|
|
ENSURE(event->GetType() == (uint)NMT_LOADED_GAME);
|
|
|
|
CNetServerSession* session = (CNetServerSession*)context;
|
|
CNetServerWorker& server = session->GetServer();
|
|
|
|
CLoadedGameMessage* message = (CLoadedGameMessage*)event->GetParamRef();
|
|
|
|
u32 turn = message->m_CurrentTurn;
|
|
u32 readyTurn = server.m_ServerTurnManager->GetReadyTurn();
|
|
|
|
// Send them all commands received since their saved state,
|
|
// and turn-ended messages for any turns that have already been processed
|
|
for (size_t i = turn + 1; i < std::max(readyTurn+1, (u32)server.m_SavedCommands.size()); ++i)
|
|
{
|
|
if (i < server.m_SavedCommands.size())
|
|
for (size_t j = 0; j < server.m_SavedCommands[i].size(); ++j)
|
|
session->SendMessage(&server.m_SavedCommands[i][j]);
|
|
|
|
if (i <= readyTurn)
|
|
{
|
|
CEndCommandBatchMessage endMessage;
|
|
endMessage.m_Turn = i;
|
|
endMessage.m_TurnLength = server.m_ServerTurnManager->GetSavedTurnLength(i);
|
|
session->SendMessage(&endMessage);
|
|
}
|
|
}
|
|
|
|
// Tell the turn manager to expect commands from this new client
|
|
server.m_ServerTurnManager->InitialiseClient(session->GetHostID(), readyTurn);
|
|
|
|
// Tell the client that everything has finished loading and it should start now
|
|
CLoadedGameMessage loaded;
|
|
loaded.m_CurrentTurn = readyTurn;
|
|
session->SendMessage(&loaded);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool CNetServerWorker::OnRejoined(void* context, CFsmEvent* event)
|
|
{
|
|
// A client has finished rejoining and the loading screen disappeared.
|
|
ENSURE(event->GetType() == (uint)NMT_REJOINED);
|
|
|
|
CNetServerSession* session = (CNetServerSession*)context;
|
|
CNetServerWorker& server = session->GetServer();
|
|
|
|
// Inform everyone of the client having rejoined
|
|
CRejoinedMessage* message = (CRejoinedMessage*)event->GetParamRef();
|
|
message->m_GUID = session->GetGUID();
|
|
server.Broadcast(message, { NSS_INGAME });
|
|
|
|
// Send all pausing players to the rejoined client.
|
|
for (const CStr& guid : server.m_PausingPlayers)
|
|
{
|
|
CClientPausedMessage pausedMessage;
|
|
pausedMessage.m_GUID = guid;
|
|
pausedMessage.m_Pause = true;
|
|
session->SendMessage(&pausedMessage);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool CNetServerWorker::OnKickPlayer(void* context, CFsmEvent* event)
|
|
{
|
|
ENSURE(event->GetType() == (uint)NMT_KICKED);
|
|
|
|
CNetServerSession* session = (CNetServerSession*)context;
|
|
CNetServerWorker& server = session->GetServer();
|
|
|
|
if (session->GetGUID() == server.m_HostGUID)
|
|
{
|
|
CKickedMessage* message = (CKickedMessage*)event->GetParamRef();
|
|
server.KickPlayer(message->m_Name, message->m_Ban);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool CNetServerWorker::OnDisconnect(void* context, CFsmEvent* event)
|
|
{
|
|
ENSURE(event->GetType() == (uint)NMT_CONNECTION_LOST);
|
|
|
|
CNetServerSession* session = (CNetServerSession*)context;
|
|
CNetServerWorker& server = session->GetServer();
|
|
|
|
server.OnUserLeave(session);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool CNetServerWorker::OnClientPaused(void* context, CFsmEvent* event)
|
|
{
|
|
ENSURE(event->GetType() == (uint)NMT_CLIENT_PAUSED);
|
|
|
|
CNetServerSession* session = (CNetServerSession*)context;
|
|
CNetServerWorker& server = session->GetServer();
|
|
|
|
CClientPausedMessage* message = (CClientPausedMessage*)event->GetParamRef();
|
|
|
|
message->m_GUID = session->GetGUID();
|
|
|
|
// Update the list of pausing players.
|
|
std::vector<CStr>::iterator player = std::find(server.m_PausingPlayers.begin(), server.m_PausingPlayers.end(), session->GetGUID());
|
|
|
|
if (message->m_Pause)
|
|
{
|
|
if (player != server.m_PausingPlayers.end())
|
|
return true;
|
|
|
|
server.m_PausingPlayers.push_back(session->GetGUID());
|
|
}
|
|
else
|
|
{
|
|
if (player == server.m_PausingPlayers.end())
|
|
return true;
|
|
|
|
server.m_PausingPlayers.erase(player);
|
|
}
|
|
|
|
// Send messages to clients that are in game, and are not the client who paused.
|
|
for (CNetServerSession* session : server.m_Sessions)
|
|
{
|
|
if (session->GetCurrState() == NSS_INGAME && message->m_GUID != session->GetGUID())
|
|
session->SendMessage(message);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void CNetServerWorker::CheckGameLoadStatus(CNetServerSession* changedSession)
|
|
{
|
|
for (const CNetServerSession* session : m_Sessions)
|
|
if (session != changedSession && session->GetCurrState() != NSS_INGAME)
|
|
return;
|
|
|
|
// Inform clients that everyone has loaded the map and that the game can start
|
|
CLoadedGameMessage loaded;
|
|
loaded.m_CurrentTurn = 0;
|
|
Broadcast(&loaded, { NSS_PREGAME });
|
|
|
|
m_State = SERVER_STATE_INGAME;
|
|
}
|
|
|
|
void CNetServerWorker::StartGame()
|
|
{
|
|
m_ServerTurnManager = new CNetServerTurnManager(*this);
|
|
|
|
for (const CNetServerSession* session : m_Sessions)
|
|
m_ServerTurnManager->InitialiseClient(session->GetHostID(), 0); // TODO: only for non-observers
|
|
|
|
m_State = SERVER_STATE_LOADING;
|
|
|
|
// Send the final setup state to all clients
|
|
UpdateGameAttributes(&m_GameAttributes);
|
|
|
|
// Remove players and observers that are not present when the game starts
|
|
for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end();)
|
|
if (it->second.m_Enabled)
|
|
++it;
|
|
else
|
|
it = m_PlayerAssignments.erase(it);
|
|
|
|
SendPlayerAssignments();
|
|
|
|
CGameStartMessage gameStart;
|
|
Broadcast(&gameStart, { NSS_PREGAME });
|
|
}
|
|
|
|
void CNetServerWorker::UpdateGameAttributes(JS::MutableHandleValue attrs)
|
|
{
|
|
m_GameAttributes = attrs;
|
|
|
|
if (!m_Host)
|
|
return;
|
|
|
|
CGameSetupMessage gameSetupMessage(GetScriptInterface());
|
|
gameSetupMessage.m_Data = m_GameAttributes;
|
|
Broadcast(&gameSetupMessage, { NSS_PREGAME });
|
|
}
|
|
|
|
CStrW CNetServerWorker::SanitisePlayerName(const CStrW& original)
|
|
{
|
|
const size_t MAX_LENGTH = 32;
|
|
|
|
CStrW name = original;
|
|
name.Replace(L"[", L"{"); // remove GUI tags
|
|
name.Replace(L"]", L"}"); // remove for symmetry
|
|
|
|
// Restrict the length
|
|
if (name.length() > MAX_LENGTH)
|
|
name = name.Left(MAX_LENGTH);
|
|
|
|
// Don't allow surrounding whitespace
|
|
name.Trim(PS_TRIM_BOTH);
|
|
|
|
// Don't allow empty name
|
|
if (name.empty())
|
|
name = L"Anonymous";
|
|
|
|
return name;
|
|
}
|
|
|
|
CStrW CNetServerWorker::DeduplicatePlayerName(const CStrW& original)
|
|
{
|
|
CStrW name = original;
|
|
|
|
// Try names "Foo", "Foo (2)", "Foo (3)", etc
|
|
size_t id = 2;
|
|
while (true)
|
|
{
|
|
bool unique = true;
|
|
for (const CNetServerSession* session : m_Sessions)
|
|
{
|
|
if (session->GetUserName() == name)
|
|
{
|
|
unique = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (unique)
|
|
return name;
|
|
|
|
name = original + L" (" + CStrW::FromUInt(id++) + L")";
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
CNetServer::CNetServer(int autostartPlayers) :
|
|
m_Worker(new CNetServerWorker(autostartPlayers))
|
|
{
|
|
}
|
|
|
|
CNetServer::~CNetServer()
|
|
{
|
|
delete m_Worker;
|
|
}
|
|
|
|
bool CNetServer::SetupConnection(const u16 port)
|
|
{
|
|
return m_Worker->SetupConnection(port);
|
|
}
|
|
|
|
void CNetServer::StartGame()
|
|
{
|
|
CScopeLock lock(m_Worker->m_WorkerMutex);
|
|
m_Worker->m_StartGameQueue.push_back(true);
|
|
}
|
|
|
|
void CNetServer::UpdateGameAttributes(JS::MutableHandleValue attrs, ScriptInterface& scriptInterface)
|
|
{
|
|
// Pass the attributes as JSON, since that's the easiest safe
|
|
// cross-thread way of passing script data
|
|
std::string attrsJSON = scriptInterface.StringifyJSON(attrs, false);
|
|
|
|
CScopeLock lock(m_Worker->m_WorkerMutex);
|
|
m_Worker->m_GameAttributesQueue.push_back(attrsJSON);
|
|
}
|
|
|
|
void CNetServer::SetTurnLength(u32 msecs)
|
|
{
|
|
CScopeLock lock(m_Worker->m_WorkerMutex);
|
|
m_Worker->m_TurnLengthQueue.push_back(msecs);
|
|
}
|