0ad/source/network/NetServer.cpp
Ykkrosh 31699e830d Handle disconnections better.
Remove local sessions (just use ENet for everything instead) because
they add far too much complexity.
Fix memory leaks.

This was SVN commit r7706.
2010-07-06 19:54:17 +00:00

602 lines
16 KiB
C++

/* Copyright (C) 2010 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 "NetStats.h"
#include "NetTurnManager.h"
#include "ps/CLogger.h"
#include "scriptinterface/ScriptInterface.h"
#include "simulation2/Simulation2.h"
#include <enet/enet.h>
#define DEFAULT_SERVER_NAME L"Unnamed Server"
#define DEFAULT_WELCOME_MESSAGE L"Welcome"
#define MAX_CLIENTS 8
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) + "...]";
}
CNetServer::CNetServer() :
m_ScriptInterface(new ScriptInterface("Engine")), m_NextHostID(1), m_Host(NULL), m_Stats(NULL)
{
m_State = SERVER_STATE_UNCONNECTED;
m_ServerTurnManager = NULL;
m_ServerName = DEFAULT_SERVER_NAME;
m_WelcomeMessage = DEFAULT_WELCOME_MESSAGE;
}
CNetServer::~CNetServer()
{
delete m_Stats;
for (size_t i = 0; i < m_Sessions.size(); ++i)
{
m_Sessions[i]->DisconnectNow(NDR_UNEXPECTED_SHUTDOWN);
delete m_Sessions[i];
}
if (m_Host)
{
enet_host_destroy(m_Host);
}
delete m_ServerTurnManager;
m_GameAttributes = CScriptValRooted(); // clear root before deleting its context
delete m_ScriptInterface;
}
bool CNetServer::SetupConnection()
{
debug_assert(m_State == SERVER_STATE_UNCONNECTED);
debug_assert(!m_Host);
// Bind to default host
ENetAddress addr;
addr.host = ENET_HOST_ANY;
addr.port = PS_DEFAULT_PORT;
// Create ENet server
m_Host = enet_host_create(&addr, MAX_CLIENTS, 0, 0);
if (!m_Host)
{
LOGERROR(L"Net server: enet_host_create failed");
return false;
}
m_Stats = new CNetStatsTable(m_Host);
if (CProfileViewer::IsInitialised())
g_ProfileViewer.AddRootTable(m_Stats);
m_State = SERVER_STATE_PREGAME;
return true;
}
bool CNetServer::SendMessage(ENetPeer* peer, const CNetMessage* message)
{
debug_assert(m_Host);
CNetServerSession* session = static_cast<CNetServerSession*>(peer->data);
return CNetHost::SendMessage(message, peer, DebugName(session).c_str());
}
bool CNetServer::Broadcast(const CNetMessage* message)
{
debug_assert(m_Host);
bool ok = true;
// Send to all sessions that are active and has finished authentication
for (size_t i = 0; i < m_Sessions.size(); ++i)
{
if (m_Sessions[i]->GetCurrState() == NSS_PREGAME || m_Sessions[i]->GetCurrState() == NSS_INGAME)
{
if (!m_Sessions[i]->SendMessage(message))
ok = false;
// 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
}
}
return ok;
}
void CNetServer::Poll()
{
debug_assert(m_Host);
// Poll host for events
ENetEvent event;
while (enet_host_service(m_Host, &event, 0) > 0)
{
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(L"Net server: Received connection from %hs:%u", hostname, event.peer->address.port);
if (m_State != SERVER_STATE_PREGAME)
{
enet_peer_disconnect(event.peer, NDR_SERVER_ALREADY_IN_GAME);
break;
}
// Set up a session object for this peer
CNetServerSession* session = new CNetServerSession(*this, event.peer);
m_Sessions.push_back(session);
SetupSession(session);
debug_assert(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(L"Net server: Disconnected %hs", 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;
}
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(L"Net server: Received message %hs of size %lu from %hs", 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;
}
}
}
}
void CNetServer::Flush()
{
debug_assert(m_Host);
enet_host_flush(m_Host);
}
void CNetServer::HandleMessageReceive(const CNetMessage* message, CNetServerSession* session)
{
// Update FSM
bool ok = session->Update(message->GetType(), (void*)message);
if (!ok)
LOGERROR(L"Net server: Error running FSM update (type=%d state=%d)", (int)message->GetType(), (int)session->GetCurrState());
}
void CNetServer::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_LOADED_GAME, NSS_INGAME, (void*)&OnLoadedGame, 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 CNetServer::HandleConnect(CNetServerSession* session)
{
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 CNetServer::OnUserJoin(CNetServerSession* session)
{
AddPlayer(session->GetGUID(), session->GetUserName());
CGameSetupMessage gameSetupMessage(GetScriptInterface());
gameSetupMessage.m_Data = m_GameAttributes;
session->SendMessage(&gameSetupMessage);
CPlayerAssignmentMessage assignMessage;
ConstructPlayerAssignmentMessage(assignMessage);
session->SendMessage(&assignMessage);
OnAddPlayer();
}
void CNetServer::OnUserLeave(CNetServerSession* session)
{
RemovePlayer(session->GetGUID());
OnRemovePlayer();
}
void CNetServer::AddPlayer(const CStr& guid, const CStrW& name)
{
// Find the first free player ID
std::set<i32> usedIDs;
for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it)
usedIDs.insert(it->second.m_PlayerID);
i32 playerID;
for (playerID = 1; usedIDs.find(playerID) != usedIDs.end(); ++playerID)
{
// (do nothing)
}
PlayerAssignment assignment;
assignment.m_Name = name;
assignment.m_PlayerID = playerID;
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 CNetServer::RemovePlayer(const CStr& guid)
{
m_PlayerAssignments.erase(guid);
SendPlayerAssignments();
OnRemovePlayer();
}
void CNetServer::AssignPlayer(int playerID, const CStr& guid)
{
// Remove anyone who's already assigned to this player
for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it)
{
if (it->second.m_PlayerID == playerID)
it->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 CNetServer::ConstructPlayerAssignmentMessage(CPlayerAssignmentMessage& message)
{
for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it)
{
CPlayerAssignmentMessage::S_m_Hosts h;
h.m_GUID = it->first;
h.m_Name = it->second.m_Name;
h.m_PlayerID = it->second.m_PlayerID;
message.m_Hosts.push_back(h);
}
}
void CNetServer::SendPlayerAssignments()
{
CPlayerAssignmentMessage message;
ConstructPlayerAssignmentMessage(message);
Broadcast(&message);
}
ScriptInterface& CNetServer::GetScriptInterface()
{
return *m_ScriptInterface;
}
bool CNetServer::OnClientHandshake(void* context, CFsmEvent* event)
{
debug_assert(event->GetType() == (uint)NMT_CLIENT_HANDSHAKE);
CNetServerSession* session = (CNetServerSession*)context;
CNetServer& server = session->GetServer();
if (server.m_State != SERVER_STATE_PREGAME)
{
session->Disconnect(NDR_SERVER_ALREADY_IN_GAME);
return false;
}
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 CNetServer::OnAuthenticate(void* context, CFsmEvent* event)
{
debug_assert(event->GetType() == (uint)NMT_AUTHENTICATE);
CNetServerSession* session = (CNetServerSession*)context;
CNetServer& server = session->GetServer();
if (server.m_State != SERVER_STATE_PREGAME)
{
session->Disconnect(NDR_SERVER_ALREADY_IN_GAME);
return false;
}
CAuthenticateMessage* message = (CAuthenticateMessage*)event->GetParamRef();
// TODO: check server password etc?
u32 newHostID = server.m_NextHostID++;
CStrW username = server.DeduplicatePlayerName(SanitisePlayerName(message->m_Name));
session->SetUserName(username);
session->SetGUID(message->m_GUID);
session->SetHostID(newHostID);
CAuthenticateResultMessage authenticateResult;
authenticateResult.m_Code = ARC_OK;
authenticateResult.m_HostID = newHostID;
authenticateResult.m_Message = L"Logged in";
session->SendMessage(&authenticateResult);
server.OnUserJoin(session);
return true;
}
bool CNetServer::OnInGame(void* context, CFsmEvent* event)
{
// TODO: should split each of these cases into a separate method
CNetServerSession* session = (CNetServerSession*)context;
CNetServer& server = session->GetServer();
CNetMessage* message = (CNetMessage*)event->GetParamRef();
if (message->GetType() == (uint)NMT_SIMULATION_COMMAND)
{
CSimulationMessage* simMessage = static_cast<CSimulationMessage*> (message);
// Send it back to all clients immediately
server.Broadcast(simMessage);
// TODO: we should do some validation of ownership (clients can't send commands on behalf of opposing players)
// 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(), syncMessage->m_Turn, syncMessage->m_Hash);
}
else if (message->GetType() == (uint)NMT_END_COMMAND_BATCH)
{
CEndCommandBatchMessage* endMessage = static_cast<CEndCommandBatchMessage*> (message);
server.m_ServerTurnManager->NotifyFinishedClientCommands(session->GetHostID(), endMessage->m_Turn);
}
return true;
}
bool CNetServer::OnChat(void* context, CFsmEvent* event)
{
debug_assert(event->GetType() == (uint)NMT_CHAT);
CNetServerSession* session = (CNetServerSession*)context;
CNetServer& server = session->GetServer();
CChatMessage* message = (CChatMessage*)event->GetParamRef();
message->m_Sender = session->GetUserName();
server.Broadcast(message);
return true;
}
bool CNetServer::OnLoadedGame(void* context, CFsmEvent* event)
{
debug_assert(event->GetType() == (uint)NMT_LOADED_GAME);
CNetServerSession* session = (CNetServerSession*)context;
CNetServer& server = session->GetServer();
server.CheckGameLoadStatus(session);
return true;
}
bool CNetServer::OnDisconnect(void* context, CFsmEvent* event)
{
debug_assert(event->GetType() == (uint)NMT_CONNECTION_LOST);
CNetServerSession* session = (CNetServerSession*)context;
CNetServer& server = session->GetServer();
server.OnUserLeave(session);
return true;
}
void CNetServer::CheckGameLoadStatus(CNetServerSession* changedSession)
{
for (size_t i = 0; i < m_Sessions.size(); ++i)
{
if (m_Sessions[i] != changedSession && m_Sessions[i]->GetCurrState() != NSS_INGAME)
return;
}
CLoadedGameMessage loaded;
Broadcast(&loaded);
m_State = SERVER_STATE_INGAME;
}
void CNetServer::StartGame()
{
m_ServerTurnManager = new CNetServerTurnManager(*this);
for (size_t i = 0; i < m_Sessions.size(); ++i)
m_ServerTurnManager->InitialiseClient(m_Sessions[i]->GetHostID()); // TODO: only for non-observers
m_State = SERVER_STATE_LOADING;
// Send the final setup state to all clients
UpdateGameAttributes(m_GameAttributes);
SendPlayerAssignments();
CGameStartMessage gameStart;
Broadcast(&gameStart);
}
void CNetServer::UpdateGameAttributes(const CScriptValRooted& attrs)
{
m_GameAttributes = attrs;
if (!m_Host)
return;
CGameSetupMessage gameSetupMessage(GetScriptInterface());
gameSetupMessage.m_Data = m_GameAttributes;
Broadcast(&gameSetupMessage);
}
CStrW CNetServer::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 CNetServer::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 (size_t i = 0; i < m_Sessions.size(); ++i)
{
if (m_Sessions[i]->GetUserName() == name)
{
unique = false;
break;
}
}
if (unique)
return name;
name = original + L" (" + CStrW(id++) + L")";
}
}