0ad/source/network/Server.cpp
Matei 92578ae553 # Some initial work on networking, fixing session setup, game startup, and command queueing.
- Fixed outdated / buggy networking code in the GUI scripts.
- Finished the API to CNetClient so that it's possible to start a CGame
from it.
- Some enhancements for debugging networking: Enabled updates while the
game is minimized/out-of-focus if it's in a network session. Also
reduced the turn length to something slightly more manageable but still
unplayable (1 sec versus 3 sec).
- Fixed a bug where IssueCommand used to access the order it creates
after queueing it, which is bad if the order gets deleted while being
queued (e.g. in CNetClient).

This was SVN commit r5139.
2007-06-04 07:41:05 +00:00

433 lines
11 KiB
C++

#include "precompiled.h"
#include <algorithm>
#include "Server.h"
#include "ServerSession.h"
#include "Network.h"
#include "JSEvents.h"
#include "scripting/ScriptableObject.h"
#include "ps/Game.h"
#include "simulation/Simulation.h"
#include "ps/Player.h"
#include "ps/CLogger.h"
#include "ps/CConsole.h"
#define LOG_CAT_NET "net"
CNetServer *g_NetServer=NULL;
using namespace std;
// NOTE: Called in network thread
CNetServerSession *CNetServer::CreateSession(CSocketInternal *pInt)
{
CNetServerSession *pRet=new CNetServerSession(this, pInt);
CServerHandshake *pMsg=new CServerHandshake();
pMsg->m_Magic=PS_PROTOCOL_MAGIC;
pMsg->m_ProtocolVersion=PS_PROTOCOL_VERSION;
pMsg->m_SoftwareVersion=PS_PROTOCOL_VERSION;
pRet->Push(pMsg);
return pRet;
}
// NOTE: Called in network thread
void CNetServer::OnAccept(const CSocketAddress &addr)
{
LOG(NORMAL, LOG_CAT_NET, "CNetServer::OnAccept(): Accepted connection from %s port %d", addr.GetString().c_str(), addr.GetPort());
CSocketInternal *pInt=Accept();
CNetServerSession *pSession=CreateSession(pInt);
UNUSED2(pSession);
}
CNetServer::CNetServer(CGame *pGame, CGameAttributes *pGameAttribs):
m_JSI_Sessions(&m_Sessions),
m_pGame(pGame),
m_pGameAttributes(pGameAttribs),
m_MaxObservers(5),
m_LastSessionID(1),
m_ServerPlayerName(L"Noname Server Player"),
m_ServerName(L"Noname Server"),
m_WelcomeMessage(L"Noname Server Welcome Message"),
m_Port(-1)
{
ONCE(
ScriptingInit();
);
m_pGameAttributes->SetUpdateCallback(AttributeUpdate, this);
m_pGameAttributes->SetPlayerUpdateCallback(PlayerAttributeUpdate, this);
m_pGameAttributes->SetPlayerSlotAssignmentCallback(PlayerSlotAssignmentCallback, this);
m_pGame->GetSimulation()->SetTurnManager(this);
// Set an incredibly long turn length for debugging - less command batch spam that way
for (int i=0;i<3;i++)
CTurnManager::SetTurnLength(i, 1000);
g_ScriptingHost.SetGlobal("g_NetServer", OBJECT_TO_JSVAL(GetScript()));
}
CNetServer::~CNetServer()
{
g_ScriptingHost.SetGlobal("g_NetServer", JSVAL_NULL);
while (m_Sessions.size() > 0)
{
SessionMap::iterator it=m_Sessions.begin();
delete it->second;
m_Sessions.erase(it);
}
}
void CNetServer::ScriptingInit()
{
CJSMap<SessionMap>::ScriptingInit("NetServer_SessionMap");
AddMethod<bool, &CNetServer::JSI_Open>("open", 0);
AddProperty(L"sessions", &CNetServer::m_JSI_Sessions);
AddProperty(L"serverPlayerName", &CNetServer::m_ServerPlayerName);
AddProperty(L"serverName", &CNetServer::m_ServerName);
AddProperty(L"welcomeMessage", &CNetServer::m_WelcomeMessage);
AddProperty(L"port", &CNetServer::m_Port);
AddProperty(L"onChat", &CNetServer::m_OnChat);
AddProperty(L"onClientConnect", &CNetServer::m_OnClientConnect);
AddProperty(L"onClientDisconnect", &CNetServer::m_OnClientDisconnect);
CJSObject<CNetServer>::ScriptingInit("NetServer");
}
bool CNetServer::JSI_Open(JSContext* UNUSED(cx), uintN UNUSED(argc), jsval* UNUSED(argv))
{
CSocketAddress addr;
if (m_Port == -1)
GetDefaultListenAddress(addr);
else
addr=CSocketAddress(m_Port, /* m_UseIPv6 ? IPv6 : */ IPv4);
PS_RESULT res=Bind(addr);
if (res != PS_OK)
{
LOG(ERROR, LOG_CAT_NET, "CNetServer::JSI_Open(): Bind error: %s", res);
return false;
}
return true;
}
PS_RESULT CNetServer::Bind(const CSocketAddress &address)
{
PS_RESULT res=CServerSocket::Bind(address);
if (res==PS_OK)
m_ServerState=NSS_PreGame;
return res;
}
void FillSetGameConfigCB(const CStrW& name, ISynchedJSProperty *prop, void *userdata)
{
CSetGameConfig *pMsg=(CSetGameConfig *)userdata;
size_t size=pMsg->m_Values.size();
pMsg->m_Values.resize(size+1);
pMsg->m_Values[size].m_Name=name;
pMsg->m_Values[size].m_Value=prop->ToString();
}
void CNetServer::FillSetGameConfig(CSetGameConfig *pMsg)
{
m_pGameAttributes->IterateSynchedProperties(FillSetGameConfigCB, pMsg);
}
void FillSetPlayerConfigCB(const CStrW& name, ISynchedJSProperty *prop, void *userdata)
{
CSetPlayerConfig *pMsg=(CSetPlayerConfig *)userdata;
size_t size=pMsg->m_Values.size();
pMsg->m_Values.resize(size+1);
pMsg->m_Values[size].m_Name=name;
pMsg->m_Values[size].m_Value=prop->ToString();
}
void CNetServer::FillSetPlayerConfig(CSetPlayerConfig *pMsg, CPlayer *pPlayer)
{
pMsg->m_PlayerID=pPlayer->GetPlayerID();
pPlayer->IterateSynchedProperties(FillSetPlayerConfigCB, pMsg);
}
void CNetServer::AssignSessionID(CNetServerSession *pSession)
{
int newID=++m_LastSessionID;
pSession->SetID(newID);
m_Sessions[newID]=pSession;
}
void CNetServer::AddSession(CNetServerSession *pSession)
{
{
CSetGameConfig *pMsg=new CSetGameConfig();
FillSetGameConfig(pMsg);
pSession->Push(pMsg);
}
// Broadcast a message for the newly added player session
CClientConnect *pMsg=new CClientConnect();
pMsg->m_Clients.resize(1);
pMsg->m_Clients[0].m_SessionID=pSession->GetID();
pMsg->m_Clients[0].m_Name=pSession->GetName();
Broadcast(pMsg);
pMsg=new CClientConnect();
// Server "client"
pMsg->m_Clients.resize(1);
pMsg->m_Clients.back().m_SessionID=1; // Server is always 1
pMsg->m_Clients.back().m_Name=m_ServerPlayerName;
// All the other clients
SessionMap::iterator it=m_Sessions.begin();
for (;it!=m_Sessions.end();++it)
{
pMsg->m_Clients.push_back(CClientConnect::S_m_Clients());
pMsg->m_Clients.back().m_SessionID=it->second->GetID();
pMsg->m_Clients.back().m_Name=it->second->GetName();
}
pSession->Push(pMsg);
// Sync player slot assignments and player attributes
for (uint i=0;i<m_pGameAttributes->GetSlotCount();i++)
{
CPlayerSlot *pSlot=m_pGameAttributes->GetSlot(i);
pSession->Push(CreatePlayerSlotAssignmentMessage(pSlot));
if (pSlot->GetAssignment() == SLOT_SESSION)
{
CSetPlayerConfig *pMsg=new CSetPlayerConfig();
FillSetPlayerConfig(pMsg, pSlot->GetPlayer());
pSession->Push(pMsg);
}
}
OnClientConnect(pSession);
}
void CNetServer::AttributeUpdate(const CStrW& name, const CStrW& newValue, void *userdata)
{
CNetServer *pServer=(CNetServer *)userdata;
g_Console->InsertMessage(L"AttributeUpdate: %ls = \"%ls\"", name.c_str(), newValue.c_str());
CSetGameConfig *pMsg=new CSetGameConfig;
pMsg->m_Values.resize(1);
pMsg->m_Values[0].m_Name=name;
pMsg->m_Values[0].m_Value=newValue;
pServer->Broadcast(pMsg);
}
void CNetServer::PlayerAttributeUpdate(const CStrW& name, const CStrW& newValue, CPlayer *pPlayer, void *userdata)
{
CNetServer *pServer=(CNetServer *)userdata;
g_Console->InsertMessage(L"PlayerAttributeUpdate(%d): %ls = \"%ls\"", pPlayer->GetPlayerID(), name.c_str(), newValue.c_str());
CSetPlayerConfig *pMsg=new CSetPlayerConfig;
pMsg->m_PlayerID=pPlayer->GetPlayerID();
pMsg->m_Values.resize(1);
pMsg->m_Values[0].m_Name=name;
pMsg->m_Values[0].m_Value=newValue;
pServer->Broadcast(pMsg);
}
CNetMessage *CNetServer::CreatePlayerSlotAssignmentMessage(CPlayerSlot *pSlot)
{
CAssignPlayerSlot *pMsg=new CAssignPlayerSlot();
pMsg->m_SlotID=pSlot->GetSlotID();
pMsg->m_SessionID=pSlot->GetSessionID();
switch (pSlot->GetAssignment())
{
#define CASE(_a, _b) case _a: pMsg->m_Assignment=_b; break;
CASE(SLOT_CLOSED, PS_ASSIGN_CLOSED)
CASE(SLOT_OPEN, PS_ASSIGN_OPEN)
CASE(SLOT_SESSION, PS_ASSIGN_SESSION)
//CASE(SLOT_AI, PS_ASSIGN_AI)
}
return pMsg;
}
void CNetServer::PlayerSlotAssignmentCallback(void *userdata, CPlayerSlot *pSlot)
{
CNetServer *pInstance=(CNetServer *)userdata;
if (pSlot->GetAssignment() == SLOT_SESSION)
pSlot->GetSession()->SetPlayerSlot(pSlot);
CNetMessage *pMsg=CreatePlayerSlotAssignmentMessage(pSlot);
g_Console->InsertMessage(L"Player Slot Assignment: %hs\n", pMsg->GetString().c_str());
pInstance->Broadcast(pMsg);
}
bool CNetServer::AllowObserver(CNetServerSession* UNUSED(pSession))
{
return m_Observers.size() < m_MaxObservers;
}
void CNetServer::RemoveSession(CNetServerSession *pSession)
{
SessionMap::iterator it=m_Sessions.find(pSession->GetID());
if (it != m_Sessions.end())
m_Sessions.erase(it);
/*
* Player sessions require some extra care:
*
* Pre-Game: dissociate the slot that was used by the session and
* synchronize the disconnection of the client.
*
* In-Game: Revert all player's entities to Gaia control, awaiting the
* client's reconnect attempts [if/when we implement that]
*
* Post-Game: Just sync disconnection - we don't have any players anymore
* and all is fine.
*
* After this is done, call the JS callback if it's been set.
*/
if (pSession->GetPlayer())
{
if (m_ServerState == NSS_PreGame)
{
pSession->GetPlayerSlot()->AssignClosed();
}
else if (m_ServerState == NSS_InGame)
{
// TODO Reassign entities to Gaia control
// TODO Set everything up for re-connect and resume
SetClientPipe(pSession->GetPlayerSlot()->GetSlotID(), NULL);
pSession->GetPlayerSlot()->AssignClosed();
}
}
CClientDisconnect *pMsg=new CClientDisconnect();
pMsg->m_SessionID=pSession->GetID();
Broadcast(pMsg);
OnClientDisconnect(pSession);
// TODO Correct handling of observers
}
// Unfortunately, the message queueing model is made so that each message has
// to be copied once for each socket its sent over, messages are deleted when
// sent by CMessageSocket. We could ref-count, but that requires a lot of
// thread safety stuff => hard work
void CNetServer::Broadcast(CNetMessage *pMsg)
{
if (m_Sessions.empty())
{
delete pMsg;
return;
}
SessionMap::iterator it=m_Sessions.begin();
// Skip one session
++it;
// Send a *copy* to all remaining sessions
for (;it != m_Sessions.end();++it)
{
it->second->Push(pMsg->Copy());
}
// Now send to the first session, *not* copying the message
m_Sessions.begin()->second->Push(pMsg);
}
int CNetServer::StartGame()
{
Broadcast(new CStartGame());
if (m_pGame->StartGame(m_pGameAttributes) != PSRETURN_OK)
{
return -1;
}
else
{
CTurnManager::Initialize(m_pGameAttributes->GetSlotCount());
for (uint i=0;i<m_pGameAttributes->GetSlotCount();i++)
{
CPlayerSlot *pSlot=m_pGameAttributes->GetSlot(i);
if (pSlot->GetAssignment() == SLOT_SESSION)
CTurnManager::SetClientPipe(i, pSlot->GetSession());
}
m_ServerState=NSS_InGame;
SessionMap::iterator it=m_Sessions.begin();
while (it != m_Sessions.end())
{
it->second->StartGame();
++it;
}
// This is the signal for everyone to start their simulations.
SendBatch(1);
return 0;
}
}
void CNetServer::GetDefaultListenAddress(CSocketAddress &address)
{
address=CSocketAddress(PS_DEFAULT_PORT, IPv4);
}
void CNetServer::NewTurn()
{
RecordBatch(2);
RotateBatches();
ClearBatch(2);
IterateBatch(1, CSimulation::GetMessageMask, m_pGame->GetSimulation());
SendBatch(1);
//IterateBatch(1, SendToObservers, this);
}
void CNetServer::QueueLocalCommand(CNetMessage *pMsg)
{
QueueIncomingCommand(pMsg);
}
void CNetServer::QueueIncomingCommand(CNetMessage *pMsg)
{
LOG(NORMAL, LOG_CAT_NET, "CNetServer::QueueIncomingCommand(): %s.", pMsg->GetString().c_str());
QueueMessage(2, pMsg);
}
void CNetServer::OnChat(const CStrW& from, const CStrW& message)
{
if (m_OnChat.Defined())
{
CChatEvent evt(from, message);
m_OnChat.DispatchEvent(GetScript(), &evt);
}
}
void CNetServer::OnClientConnect(CNetServerSession *pSession)
{
if (m_OnClientConnect.Defined())
{
CClientConnectEvent evt(pSession);
m_OnClientConnect.DispatchEvent(GetScript(), &evt);
}
}
void CNetServer::OnClientDisconnect(CNetServerSession *pSession)
{
if (m_OnClientDisconnect.Defined())
{
CClientDisconnectEvent evt(pSession->GetID(), pSession->GetName());
m_OnClientDisconnect.DispatchEvent(GetScript(), &evt);
}
}