/* 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 . */ #include "precompiled.h" #include "NetServer.h" #include "NetClient.h" #include "NetMessage.h" #include "NetSession.h" #include "NetStats.h" #include "NetTurnManager.h" #include "lib/external_libraries/enet.h" #include "ps/CLogger.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/Simulation2.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", "Net server")), 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(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(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(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 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; } void CNetServer::SetTurnLength(u32 msecs) { if (m_ServerTurnManager) m_ServerTurnManager->SetTurnLength(msecs); } 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 (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 (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 (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_GUID = session->GetGUID(); 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((unsigned long)id++) + L")"; } }