# Support rejoining multiplayer games after disconnection.

This was SVN commit r10437.
This commit is contained in:
Ykkrosh
2011-10-27 16:46:48 +00:00
parent 6d071123cc
commit bfe2126a17
16 changed files with 1038 additions and 81 deletions
+133 -2
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2010 Wildfire Games.
/* Copyright (C) 2011 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -23,16 +23,48 @@
#include "NetSession.h"
#include "NetTurnManager.h"
#include "lib/byte_order.h"
#include "lib/sysdep/sysdep.h"
#include "ps/CConsole.h"
#include "ps/CLogger.h"
#include "ps/Compress.h"
#include "ps/CStr.h"
#include "ps/Game.h"
#include "ps/Loader.h"
#include "scriptinterface/ScriptInterface.h"
#include "simulation2/Simulation2.h"
CNetClient *g_NetClient = NULL;
/**
* Async task for receiving the initial game state when rejoining an
* in-progress network game.
*/
class CNetFileReceiveTask_ClientRejoin : public CNetFileReceiveTask
{
NONCOPYABLE(CNetFileReceiveTask_ClientRejoin);
public:
CNetFileReceiveTask_ClientRejoin(CNetClient& client)
: m_Client(client)
{
}
virtual void OnComplete()
{
// We've received the game state from the server
// Save it so we can use it after the map has finished loading
m_Client.m_JoinSyncBuffer = m_Buffer;
// Pretend the server told us to start the game
CGameStartMessage start;
m_Client.HandleMessage(&start);
}
private:
CNetClient& m_Client;
};
CNetClient::CNetClient(CGame* game) :
m_Session(NULL),
m_UserName(L"anonymous"),
@@ -57,6 +89,15 @@ CNetClient::CNetClient(CGame* game) :
AddTransition(NCS_PREGAME, (uint)NMT_GAME_SETUP, NCS_PREGAME, (void*)&OnGameSetup, context);
AddTransition(NCS_PREGAME, (uint)NMT_PLAYER_ASSIGNMENT, NCS_PREGAME, (void*)&OnPlayerAssignment, context);
AddTransition(NCS_PREGAME, (uint)NMT_GAME_START, NCS_LOADING, (void*)&OnGameStart, context);
AddTransition(NCS_PREGAME, (uint)NMT_JOIN_SYNC_START, NCS_JOIN_SYNCING, (void*)&OnJoinSyncStart, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CHAT, NCS_JOIN_SYNCING, (void*)&OnChat, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_GAME_SETUP, NCS_JOIN_SYNCING, (void*)&OnGameSetup, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_PLAYER_ASSIGNMENT, NCS_JOIN_SYNCING, (void*)&OnPlayerAssignment, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_GAME_START, NCS_JOIN_SYNCING, (void*)&OnGameStart, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_SIMULATION_COMMAND, NCS_JOIN_SYNCING, (void*)&OnInGame, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_END_COMMAND_BATCH, NCS_JOIN_SYNCING, (void*)&OnJoinSyncEndCommandBatch, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_LOADED_GAME, NCS_INGAME, (void*)&OnLoadedGame, context);
AddTransition(NCS_LOADING, (uint)NMT_CHAT, NCS_LOADING, (void*)&OnChat, context);
AddTransition(NCS_LOADING, (uint)NMT_GAME_SETUP, NCS_LOADING, (void*)&OnGameSetup, context);
@@ -76,7 +117,7 @@ CNetClient::CNetClient(CGame* game) :
CNetClient::~CNetClient()
{
delete m_Session;
DestroyConnection();
}
void CNetClient::SetUserName(const CStrW& username)
@@ -100,6 +141,11 @@ void CNetClient::SetAndOwnSession(CNetClientSession* session)
m_Session = session;
}
void CNetClient::DestroyConnection()
{
SAFE_DELETE(m_Session);
}
void CNetClient::Poll()
{
if (m_Session)
@@ -203,6 +249,40 @@ void CNetClient::SendChatMessage(const std::wstring& text)
bool CNetClient::HandleMessage(CNetMessage* message)
{
// Handle non-FSM messages first
Status status = m_Session->GetFileTransferer().HandleMessageReceive(message);
if (status == INFO::OK)
return true;
if (status != INFO::SKIPPED)
return false;
if (message->GetType() == NMT_FILE_TRANSFER_REQUEST)
{
CFileTransferRequestMessage* reqMessage = (CFileTransferRequestMessage*)message;
// TODO: we should support different transfer request types, instead of assuming
// it's always requesting the simulation state
std::stringstream stream;
LOGMESSAGERENDER(L"Serializing game at turn %d for rejoining player", m_ClientTurnManager->GetCurrentTurn());
u32 turn = to_le32(m_ClientTurnManager->GetCurrentTurn());
stream.write((char*)&turn, sizeof(turn));
bool ok = m_Game->GetSimulation2()->SerializeState(stream);
ENSURE(ok);
// Compress the content with zlib to save bandwidth
// (TODO: if this is still too large, compressing with e.g. LZMA works much better)
std::string compressed;
CompressZLib(stream.str(), compressed, true);
m_Session->GetFileTransferer().StartResponse(reqMessage->m_RequestID, compressed);
return true;
}
// Update FSM
bool ok = Update(message->GetType(), message);
if (!ok)
@@ -212,11 +292,31 @@ bool CNetClient::HandleMessage(CNetMessage* message)
void CNetClient::LoadFinished()
{
if (!m_JoinSyncBuffer.empty())
{
std::string state;
DecompressZLib(m_JoinSyncBuffer, state, true);
std::stringstream stream(state);
u32 turn;
stream.read((char*)&turn, sizeof(turn));
turn = to_le32(turn);
LOGMESSAGE(L"Rejoining client deserializing state at turn %d\n", turn);
bool ok = m_Game->GetSimulation2()->DeserializeState(stream);
ENSURE(ok);
m_ClientTurnManager->ResetState(turn, turn);
}
CScriptValRooted msg;
GetScriptInterface().Eval("({'type':'netstatus','status':'waiting_for_players'})", msg);
PushGuiMessage(msg);
CLoadedGameMessage loaded;
loaded.m_CurrentTurn = m_ClientTurnManager->GetCurrentTurn();
SendMessage(&loaded);
}
@@ -330,6 +430,7 @@ bool CNetClient::OnPlayerAssignment(void* context, CFsmEvent* event)
for (size_t i = 0; i < message->m_Hosts.size(); ++i)
{
PlayerAssignment assignment;
assignment.m_Enabled = true;
assignment.m_Name = message->m_Hosts[i].m_Name;
assignment.m_PlayerID = message->m_Hosts[i].m_PlayerID;
newPlayerAssignments[message->m_Hosts[i].m_GUID] = assignment;
@@ -366,6 +467,36 @@ bool CNetClient::OnGameStart(void* context, CFsmEvent* event)
return true;
}
bool CNetClient::OnJoinSyncStart(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_JOIN_SYNC_START);
CNetClient* client = (CNetClient*)context;
// The server wants us to start downloading the game state from it, so do so
client->m_Session->GetFileTransferer().StartTask(
shared_ptr<CNetFileReceiveTask>(new CNetFileReceiveTask_ClientRejoin(*client))
);
return true;
}
bool CNetClient::OnJoinSyncEndCommandBatch(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_END_COMMAND_BATCH);
CNetClient* client = (CNetClient*)context;
CEndCommandBatchMessage* endMessage = (CEndCommandBatchMessage*)event->GetParamRef();
client->m_ClientTurnManager->FinishedAllCommands(endMessage->m_Turn, endMessage->m_TurnLength);
// Execute all the received commands for the latest turn
client->m_ClientTurnManager->UpdateFastForward();
return true;
}
bool CNetClient::OnLoadedGame(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_LOADED_GAME);
+16 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2010 Wildfire Games.
/* Copyright (C) 2011 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -19,6 +19,7 @@
#define NETCLIENT_H
#include "network/fsm.h"
#include "network/NetFileTransfer.h"
#include "network/NetHost.h"
#include "scriptinterface/ScriptVal.h"
@@ -42,6 +43,7 @@ enum
NCS_INITIAL_GAMESETUP,
NCS_PREGAME,
NCS_LOADING,
NCS_JOIN_SYNCING,
NCS_INGAME
};
@@ -56,6 +58,8 @@ class CNetClient : public CFsm
{
NONCOPYABLE(CNetClient);
friend class CNetFileReceiveTask_ClientRejoin;
public:
/**
* Construct a client associated with the given game object.
@@ -78,6 +82,12 @@ public:
*/
bool SetupConnection(const CStr& server);
/**
* Destroy the connection to the server.
* This client probably cannot be used again.
*/
void DestroyConnection();
/**
* Poll the connection for messages from the server and process them, and send
* any queued messages.
@@ -166,6 +176,8 @@ private:
static bool OnPlayerAssignment(void* context, CFsmEvent* event);
static bool OnInGame(void* context, CFsmEvent* event);
static bool OnGameStart(void* context, CFsmEvent* event);
static bool OnJoinSyncStart(void* context, CFsmEvent* event);
static bool OnJoinSyncEndCommandBatch(void* context, CFsmEvent* event);
static bool OnLoadedGame(void* context, CFsmEvent* event);
/**
@@ -203,6 +215,9 @@ private:
/// Queue of messages for GuiPoll
std::deque<CScriptValRooted> m_GuiMessageQueue;
/// Serialized game state received when joining an in-progress game
std::string m_JoinSyncBuffer;
};
/// Global network client for the standard game
+159
View File
@@ -0,0 +1,159 @@
#include "precompiled.h"
#include "NetFileTransfer.h"
#include "lib/timer.h"
#include "network/NetMessage.h"
#include "network/NetSession.h"
#include "ps/CLogger.h"
Status CNetFileTransferer::HandleMessageReceive(const CNetMessage* message)
{
if (message->GetType() == NMT_FILE_TRANSFER_RESPONSE)
{
CFileTransferResponseMessage* respMessage = (CFileTransferResponseMessage*)message;
if (m_FileReceiveTasks.find(respMessage->m_RequestID) == m_FileReceiveTasks.end())
{
LOGERROR(L"Net transfer: Unsolicited file transfer response (id=%d)", (int)respMessage->m_RequestID);
return ERR::FAIL;
}
if (respMessage->m_Length == 0 || respMessage->m_Length > MAX_FILE_TRANSFER_SIZE)
{
LOGERROR(L"Net transfer: Invalid size for file transfer response (length=%d)", (int)respMessage->m_Length);
return ERR::FAIL;
}
shared_ptr<CNetFileReceiveTask> task = m_FileReceiveTasks[respMessage->m_RequestID];
task->m_Length = respMessage->m_Length;
task->m_Buffer.reserve(respMessage->m_Length);
LOGMESSAGERENDER(L"Downloading data over network (%d KB) - please wait...", task->m_Length/1024);
m_LastProgressReportTime = timer_Time();
return INFO::OK;
}
else if (message->GetType() == NMT_FILE_TRANSFER_DATA)
{
CFileTransferDataMessage* dataMessage = (CFileTransferDataMessage*)message;
if (m_FileReceiveTasks.find(dataMessage->m_RequestID) == m_FileReceiveTasks.end())
{
LOGERROR(L"Net transfer: Unsolicited file transfer data (id=%d)", (int)dataMessage->m_RequestID);
return ERR::FAIL;
}
shared_ptr<CNetFileReceiveTask> task = m_FileReceiveTasks[dataMessage->m_RequestID];
task->m_Buffer += dataMessage->m_Data;
if (task->m_Buffer.size() > task->m_Length)
{
LOGERROR(L"Net transfer: Invalid size for file transfer data (length=%d actual=%d)", (int)task->m_Length, (int)task->m_Buffer.size());
return ERR::FAIL;
}
CFileTransferAckMessage ackMessage;
ackMessage.m_RequestID = task->m_RequestID;
ackMessage.m_NumPackets = 1; // TODO: would be nice to send a single ack for multiple packets at once
m_Session->SendMessage(&ackMessage);
if (task->m_Buffer.size() == task->m_Length)
{
LOGMESSAGERENDER(L"Download completed");
task->OnComplete();
m_FileReceiveTasks.erase(dataMessage->m_RequestID);
return INFO::OK;
}
// TODO: should report progress using proper GUI
// Report the download status occassionally
double t = timer_Time();
if (t > m_LastProgressReportTime + 0.5)
{
LOGMESSAGERENDER(L"Downloading data: %.1f%% of %d KB", 100.f*task->m_Buffer.size()/task->m_Length, task->m_Length/1024);
m_LastProgressReportTime = t;
}
return INFO::OK;
}
else if (message->GetType() == NMT_FILE_TRANSFER_ACK)
{
CFileTransferAckMessage* ackMessage = (CFileTransferAckMessage*)message;
if (m_FileSendTasks.find(ackMessage->m_RequestID) == m_FileSendTasks.end())
{
LOGERROR(L"Net transfer: Unsolicited file transfer ack (id=%d)", (int)ackMessage->m_RequestID);
return ERR::FAIL;
}
CNetFileSendTask& task = m_FileSendTasks[ackMessage->m_RequestID];
if (ackMessage->m_NumPackets > task.packetsInFlight)
{
LOGERROR(L"Net transfer: Invalid num packets for file transfer ack (num=%d inflight=%d)",
(int)ackMessage->m_NumPackets, (int)task.packetsInFlight);
return ERR::FAIL;
}
task.packetsInFlight -= ackMessage->m_NumPackets;
return INFO::OK;
}
return INFO::SKIPPED;
}
void CNetFileTransferer::StartTask(const shared_ptr<CNetFileReceiveTask>& task)
{
u32 requestID = m_NextRequestID++;
task->m_RequestID = requestID;
m_FileReceiveTasks[requestID] = task;
CFileTransferRequestMessage request;
request.m_RequestID = requestID;
m_Session->SendMessage(&request);
}
void CNetFileTransferer::StartResponse(u32 requestID, const std::string& data)
{
CNetFileSendTask task;
task.requestID = requestID;
task.buffer = data;
task.offset = 0;
task.packetsInFlight = 0;
task.maxWindowSize = DEFAULT_FILE_TRANSFER_WINDOW_SIZE;
m_FileSendTasks[task.requestID] = task;
CFileTransferResponseMessage respMessage;
respMessage.m_RequestID = requestID;
respMessage.m_Length = task.buffer.size();
m_Session->SendMessage(&respMessage);
}
void CNetFileTransferer::Poll()
{
// Find tasks which have fewer packets in flight than their window size,
// and send more packets
for (FileSendTasksMap::iterator it = m_FileSendTasks.begin(); it != m_FileSendTasks.end(); ++it)
{
while (it->second.packetsInFlight < it->second.maxWindowSize && it->second.offset < it->second.buffer.size())
{
CFileTransferDataMessage dataMessage;
dataMessage.m_RequestID = it->second.requestID;
ssize_t packetSize = std::min(DEFAULT_FILE_TRANSFER_PACKET_SIZE, it->second.buffer.size() - it->second.offset);
dataMessage.m_Data = it->second.buffer.substr(it->second.offset, packetSize);
it->second.offset += packetSize;
it->second.packetsInFlight++;
m_Session->SendMessage(&dataMessage);
}
}
// TODO: need to garbage-collect finished tasks
}
+131
View File
@@ -0,0 +1,131 @@
/* Copyright (C) 2011 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/>.
*/
#ifndef NETFILETRANSFER_H
#define NETFILETRANSFER_H
class CNetMessage;
class CNetClientSession;
class CNetServerSession;
class INetSession;
// Assume this is sufficiently less than MTU that packets won't get
// fragmented or dropped.
static const size_t DEFAULT_FILE_TRANSFER_PACKET_SIZE = 1024;
// To improve performance without flooding ENet's internal buffers,
// maintain a small number of in-flight packets.
// Pick numbers so that with e.g. 200ms round-trip latency
// we can hopefully get windowSize*packetSize*1000/200 = 160KB/s bandwidth
static const size_t DEFAULT_FILE_TRANSFER_WINDOW_SIZE = 32;
// Some arbitrary limit to make it slightly harder to use up all of someone's RAM
static const size_t MAX_FILE_TRANSFER_SIZE = 8*MiB;
/**
* Asynchronous file-receiving task.
* Other code should subclass this, implement OnComplete(),
* then pass it to CNetFileTransferer::StartTask.
*/
class CNetFileReceiveTask
{
public:
CNetFileReceiveTask() : m_RequestID(0), m_Length(0) { }
virtual ~CNetFileReceiveTask() {}
/**
* Called when m_Buffer contains the full received data.
*/
virtual void OnComplete() = 0;
// TODO: Ought to have an OnFailure, e.g. when the session drops or there's another error
/**
* Uniquely identifies the request within the scope of its CNetFileTransferer.
* Set automatically by StartTask.
*/
u32 m_RequestID;
size_t m_Length;
std::string m_Buffer;
};
/**
* Handles transferring files between clients and servers.
*/
class CNetFileTransferer
{
public:
CNetFileTransferer(INetSession* session)
: m_Session(session), m_NextRequestID(1), m_LastProgressReportTime(0)
{
}
/**
* Should be called when a message is received from the network.
* Returns INFO::SKIPPED if the message is not one that this class handles.
* Returns INFO::OK if the message is handled successfully,
* or ERR::FAIL if handled unsuccessfully.
*/
Status HandleMessageReceive(const CNetMessage* message);
/**
* Registers a file-receiving task.
*/
void StartTask(const shared_ptr<CNetFileReceiveTask>& task);
/**
* Registers data to be sent in response to a request.
* (Callers are expected to have their own mechanism for receiving
* requests and deciding what to respond with.)
*/
void StartResponse(u32 requestID, const std::string& data);
/**
* Call frequently (e.g. once per frame) to trigger any necessary
* packet processing.
*/
void Poll();
private:
/**
* Asynchronous file-sending task.
*/
struct CNetFileSendTask
{
u32 requestID;
std::string buffer;
size_t offset;
size_t maxWindowSize;
size_t packetsInFlight;
};
INetSession* m_Session;
u32 m_NextRequestID;
typedef std::map<u32, shared_ptr<CNetFileReceiveTask> > FileReceiveTasksMap;
FileReceiveTasksMap m_FileReceiveTasks;
typedef std::map<u32, CNetFileSendTask> FileSendTasksMap;
FileSendTasksMap m_FileSendTasks;
double m_LastProgressReportTime;
};
#endif // NETFILETRANSFER_H
+13 -3
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2010 Wildfire Games.
/* Copyright (C) 2011 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -34,8 +34,18 @@ class CNetMessage;
struct PlayerAssignment
{
CStrW m_Name; // player name
i32 m_PlayerID; // the player that the given host controls, or -1 if none (observer)
/**
* Whether the player is currently connected and active.
* (We retain information on disconnected players to support rejoining,
* but don't transmit these to other clients.)
*/
bool m_Enabled;
/// Player name
CStrW m_Name;
/// The player that the given host controls, or -1 if none (observer)
i32 m_PlayerID;
};
typedef std::map<CStr, PlayerAssignment> PlayerAssignmentMap; // map from GUID -> assignment
+21 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2010 Wildfire Games.
/* Copyright (C) 2011 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -111,6 +111,26 @@ CNetMessage* CNetMessageFactory::CreateMessage(const void* pData,
pNewMessage = new CPlayerAssignmentMessage;
break;
case NMT_FILE_TRANSFER_REQUEST:
pNewMessage = new CFileTransferRequestMessage;
break;
case NMT_FILE_TRANSFER_RESPONSE:
pNewMessage = new CFileTransferResponseMessage;
break;
case NMT_FILE_TRANSFER_DATA:
pNewMessage = new CFileTransferDataMessage;
break;
case NMT_FILE_TRANSFER_ACK:
pNewMessage = new CFileTransferAckMessage;
break;
case NMT_JOIN_SYNC_START:
pNewMessage = new CJoinSyncStartMessage;
break;
case NMT_LOADED_GAME:
pNewMessage = new CLoadedGameMessage;
break;
+2 -3
View File
@@ -31,8 +31,6 @@
*/
class CNetMessage : public ISerializable
{
NONCOPYABLE(CNetMessage);
friend class CNetSession;
public:
@@ -127,7 +125,7 @@ public:
u32 m_Turn;
CScriptValRooted m_Data;
private:
ScriptInterface& m_ScriptInterface;
ScriptInterface* m_ScriptInterface;
};
/**
@@ -135,6 +133,7 @@ private:
*/
class CGameSetupMessage : public CNetMessage
{
NONCOPYABLE(CGameSetupMessage);
public:
CGameSetupMessage(ScriptInterface& scriptInterface);
CGameSetupMessage(ScriptInterface& scriptInterface, jsval data);
+7 -7
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2010 Wildfire Games.
/* Copyright (C) 2011 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -94,12 +94,12 @@ public:
};
CSimulationMessage::CSimulationMessage(ScriptInterface& scriptInterface) :
CNetMessage(NMT_SIMULATION_COMMAND), m_ScriptInterface(scriptInterface)
CNetMessage(NMT_SIMULATION_COMMAND), m_ScriptInterface(&scriptInterface)
{
}
CSimulationMessage::CSimulationMessage(ScriptInterface& scriptInterface, u32 client, i32 player, u32 turn, jsval data) :
CNetMessage(NMT_SIMULATION_COMMAND), m_ScriptInterface(scriptInterface),
CNetMessage(NMT_SIMULATION_COMMAND), m_ScriptInterface(&scriptInterface),
m_Client(client), m_Player(player), m_Turn(turn), m_Data(scriptInterface.GetContext(), data)
{
}
@@ -110,7 +110,7 @@ u8* CSimulationMessage::Serialize(u8* pBuffer) const
// TODO: ought to represent common commands more efficiently
u8* pos = CNetMessage::Serialize(pBuffer);
CBufferBinarySerializer serializer(m_ScriptInterface, pos);
CBufferBinarySerializer serializer(*m_ScriptInterface, pos);
serializer.NumberU32_Unbounded("client", m_Client);
serializer.NumberI32_Unbounded("player", m_Player);
serializer.NumberU32_Unbounded("turn", m_Turn);
@@ -125,7 +125,7 @@ const u8* CSimulationMessage::Deserialize(const u8* pStart, const u8* pEnd)
const u8* pos = CNetMessage::Deserialize(pStart, pEnd);
std::istringstream stream(std::string(pos, pEnd));
CStdDeserializer deserializer(m_ScriptInterface, stream);
CStdDeserializer deserializer(*m_ScriptInterface, stream);
deserializer.NumberU32_Unbounded("client", m_Client);
deserializer.NumberI32_Unbounded("player", m_Player);
deserializer.NumberU32_Unbounded("turn", m_Turn);
@@ -138,7 +138,7 @@ size_t CSimulationMessage::GetSerializedLength() const
// TODO: serializing twice is stupidly inefficient - we should just
// do it once, store the result, and use it here and in Serialize
CLengthBinarySerializer serializer(m_ScriptInterface);
CLengthBinarySerializer serializer(*m_ScriptInterface);
serializer.NumberU32_Unbounded("client", m_Client);
serializer.NumberI32_Unbounded("player", m_Player);
serializer.NumberU32_Unbounded("turn", m_Turn);
@@ -148,7 +148,7 @@ size_t CSimulationMessage::GetSerializedLength() const
CStr CSimulationMessage::ToString() const
{
std::string source = utf8_from_wstring(m_ScriptInterface.ToString(m_Data.get()));
std::string source = utf8_from_wstring(m_ScriptInterface->ToString(m_Data.get()));
std::stringstream stream;
stream << "CSimulationMessage { m_Client: " << m_Client << ", m_Player: " << m_Player << ", m_Turn: " << m_Turn << ", m_Data: " << source << " }";
+35 -4
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2010 Wildfire Games.
/* Copyright (C) 2011 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -28,7 +28,7 @@
#define PS_PROTOCOL_MAGIC 0x5073013f // 'P', 's', 0x01, '?'
#define PS_PROTOCOL_MAGIC_RESPONSE 0x50630121 // 'P', 'c', 0x01, '!'
#define PS_PROTOCOL_VERSION 0x01010004 // Arbitrary protocol
#define PS_PROTOCOL_VERSION 0x01010005 // Arbitrary protocol
#define PS_DEFAULT_PORT 0x5073 // 'P', 's'
// Defines the list of message types. The order of the list must not change.
@@ -48,11 +48,19 @@ enum NetMessageType
NMT_CHAT, // Common chat message
NMT_GAME_SETUP,
NMT_PLAYER_ASSIGNMENT,
NMT_FILE_TRANSFER_REQUEST,
NMT_FILE_TRANSFER_RESPONSE,
NMT_FILE_TRANSFER_DATA,
NMT_FILE_TRANSFER_ACK,
NMT_JOIN_SYNC_START,
NMT_LOADED_GAME,
NMT_GAME_START,
NMT_END_COMMAND_BATCH,
NMT_SYNC_CHECK,
NMT_SYNC_ERROR,
NMT_SYNC_CHECK, // OOS-detection hash checking
NMT_SYNC_ERROR, // OOS-detection error
NMT_SIMULATION_COMMAND,
NMT_LAST // Last message in the list
};
@@ -119,7 +127,30 @@ START_NMT_CLASS_(PlayerAssignment, NMT_PLAYER_ASSIGNMENT)
NMT_END_ARRAY()
END_NMT_CLASS()
START_NMT_CLASS_(FileTransferRequest, NMT_FILE_TRANSFER_REQUEST)
NMT_FIELD_INT(m_RequestID, u32, 4)
END_NMT_CLASS()
START_NMT_CLASS_(FileTransferResponse, NMT_FILE_TRANSFER_RESPONSE)
NMT_FIELD_INT(m_RequestID, u32, 4)
NMT_FIELD_INT(m_Length, u32, 4)
END_NMT_CLASS()
START_NMT_CLASS_(FileTransferData, NMT_FILE_TRANSFER_DATA)
NMT_FIELD_INT(m_RequestID, u32, 4)
NMT_FIELD(CStr8, m_Data)
END_NMT_CLASS()
START_NMT_CLASS_(FileTransferAck, NMT_FILE_TRANSFER_ACK)
NMT_FIELD_INT(m_RequestID, u32, 4)
NMT_FIELD_INT(m_NumPackets, u32, 4)
END_NMT_CLASS()
START_NMT_CLASS_(JoinSyncStart, NMT_JOIN_SYNC_START)
END_NMT_CLASS()
START_NMT_CLASS_(LoadedGame, NMT_LOADED_GAME)
NMT_FIELD_INT(m_CurrentTurn, u32, 4)
END_NMT_CLASS()
START_NMT_CLASS_(GameStart, NMT_GAME_START)
+226 -28
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2010 Wildfire Games.
/* Copyright (C) 2011 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -54,6 +54,55 @@ static CStr DebugName(CNetServerSession* session)
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 (size_t i = 0; i < m_Server.m_Sessions.size(); ++i)
{
if (m_Server.m_Sessions[i]->GetHostID() == m_RejoinerHostID)
{
session = m_Server.m_Sessions[i];
break;
}
}
if (!session)
{
LOGMESSAGE(L"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
@@ -195,8 +244,9 @@ void CNetServerWorker::Run()
m_Stats->LatchHostState(m_Host);
}
// Clear root before deleting its context
// Clear roots before deleting their context
m_GameAttributes = CScriptValRooted();
m_SavedCommands.clear();
SAFE_DELETE(m_ScriptInterface);
}
@@ -237,6 +287,10 @@ bool CNetServerWorker::RunStep()
if (!newStartGame.empty())
StartGame();
// Perform file transfers
for (size_t i = 0; i < m_Sessions.size(); ++i)
m_Sessions[i]->GetFileTransferer().Poll();
// Process network events:
ENetEvent event;
@@ -265,12 +319,6 @@ bool CNetServerWorker::RunStep()
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);
@@ -343,6 +391,24 @@ bool CNetServerWorker::RunStep()
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
bool ok = session->Update(message->GetType(), (void*)message);
if (!ok)
@@ -367,6 +433,8 @@ void CNetServerWorker::SetupSession(CNetServerSession* session)
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_JOIN_SYNCING, (uint)NMT_LOADED_GAME, NSS_INGAME, (void*)&OnJoinSyncingLoadedGame, 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);
@@ -412,19 +480,57 @@ void CNetServerWorker::OnUserLeave(CNetServerSession* session)
void CNetServerWorker::AddPlayer(const CStr& guid, const CStrW& name)
{
// Find the first free player ID
// Find all player IDs in active use; we mustn't give them to a second player
std::set<i32> usedIDs;
for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it)
usedIDs.insert(it->second.m_PlayerID);
if (it->second.m_Enabled)
usedIDs.insert(it->second.m_PlayerID);
i32 playerID;
for (playerID = 1; usedIDs.find(playerID) != usedIDs.end(); ++playerID)
// If the player is rejoining after disconnecting, try to give them
// back their old player ID
i32 playerID = -1;
bool foundPlayerID = false;
// Try to match GUID first
for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it)
{
// (do nothing)
if (!it->second.m_Enabled && it->first == guid && usedIDs.find(it->second.m_PlayerID) == usedIDs.end())
{
playerID = it->second.m_PlayerID;
foundPlayerID = true;
m_PlayerAssignments.erase(it); // delete the old mapping, since we've got a new one now
break;
}
}
// Try to match username next
if (!foundPlayerID)
{
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;
foundPlayerID = true;
m_PlayerAssignments.erase(it); // delete the old mapping, since we've got a new one now
break;
}
}
}
// Otherwise pick the first free player ID
if (!foundPlayerID)
{
for (playerID = 1; usedIDs.find(playerID) != usedIDs.end(); ++playerID)
{
// (do nothing)
}
foundPlayerID = true;
}
PlayerAssignment assignment;
assignment.m_Enabled = true;
assignment.m_Name = name;
assignment.m_PlayerID = playerID;
m_PlayerAssignments[guid] = assignment;
@@ -436,7 +542,7 @@ void CNetServerWorker::AddPlayer(const CStr& guid, const CStrW& name)
void CNetServerWorker::RemovePlayer(const CStr& guid)
{
m_PlayerAssignments.erase(guid);
m_PlayerAssignments[guid].m_Enabled = false;
SendPlayerAssignments();
}
@@ -461,6 +567,9 @@ void CNetServerWorker::ConstructPlayerAssignmentMessage(CPlayerAssignmentMessage
{
for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it)
{
if (!it->second.m_Enabled)
continue;
CPlayerAssignmentMessage::S_m_Hosts h;
h.m_GUID = it->first;
h.m_Name = it->second.m_Name;
@@ -494,12 +603,6 @@ bool CNetServerWorker::OnClientHandshake(void* context, CFsmEvent* event)
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& 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)
{
@@ -523,20 +626,40 @@ bool CNetServerWorker::OnAuthenticate(void* context, CFsmEvent* event)
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CAuthenticateMessage* message = (CAuthenticateMessage*)event->GetParamRef();
CStrW username = server.DeduplicatePlayerName(SanitisePlayerName(message->m_Name));
bool isRejoining = false;
if (server.m_State != SERVER_STATE_PREGAME)
{
session->Disconnect(NDR_SERVER_ALREADY_IN_GAME);
return false;
}
// isRejoining = true; // uncomment this to test rejoining even if the player wasn't connected previously
CAuthenticateMessage* message = (CAuthenticateMessage*)event->GetParamRef();
// Search for an old disconnected player of the same name
// (TODO: if GUIDs were stable, we should use them instead)
for (PlayerAssignmentMap::iterator it = server.m_PlayerAssignments.begin(); it != server.m_PlayerAssignments.end(); ++it)
{
if (!it->second.m_Enabled && it->second.m_Name == username)
{
isRejoining = true;
break;
}
}
// Players who weren't already in the game are not allowed to join now that it's started
if (!isRejoining)
{
LOGMESSAGE(L"Refused connection after game start from not-previously-known user \"%ls\"", username.c_str());
session->Disconnect(NDR_SERVER_ALREADY_IN_GAME);
return true;
}
}
// 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);
@@ -549,6 +672,21 @@ bool CNetServerWorker::OnAuthenticate(void* context, CFsmEvent* event)
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;
}
@@ -567,6 +705,11 @@ bool CNetServerWorker::OnInGame(void* context, CFsmEvent* event)
// Send it back to all clients immediately
server.Broadcast(simMessage);
// 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 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
@@ -608,11 +751,65 @@ bool CNetServerWorker::OnLoadedGame(void* context, CFsmEvent* event)
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::OnDisconnect(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_CONNECTION_LOST);
@@ -634,6 +831,7 @@ void CNetServerWorker::CheckGameLoadStatus(CNetServerSession* changedSession)
}
CLoadedGameMessage loaded;
loaded.m_CurrentTurn = 0;
Broadcast(&loaded);
m_State = SERVER_STATE_INGAME;
@@ -644,7 +842,7 @@ void CNetServerWorker::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_ServerTurnManager->InitialiseClient(m_Sessions[i]->GetHostID(), 0); // TODO: only for non-observers
m_State = SERVER_STATE_LOADING;
+23 -2
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2010 Wildfire Games.
/* Copyright (C) 2011 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -18,6 +18,7 @@
#ifndef NETSERVER_H
#define NETSERVER_H
#include "NetFileTransfer.h"
#include "NetHost.h"
#include "ps/ThreadUtil.h"
@@ -31,6 +32,7 @@ class CFsmEvent;
class ScriptInterface;
class CPlayerAssignmentMessage;
class CNetStatsTable;
class CSimulationMessage;
class CNetServerWorker;
@@ -77,6 +79,10 @@ enum NetServerSessionState
// Server must be in SERVER_STATE_PREGAME or SERVER_STATE_LOADING.
NSS_PREGAME,
// The client has authenticated but the game was already started,
// so it's synchronising with the game state from other clients
NSS_JOIN_SYNCING,
// The client is running the game.
// Server must be in SERVER_STATE_LOADING or SERVER_STATE_INGAME.
NSS_INGAME
@@ -171,6 +177,7 @@ public:
private:
friend class CNetServer;
friend class CNetFileReceiveTask_ServerRejoin;
CNetServerWorker(int autostartPlayers);
~CNetServerWorker();
@@ -219,7 +226,7 @@ private:
/**
* Set the turn length to a fixed value.
* TODO: we should replace this with some adapative lag-dependent computation.
* TODO: we should replace this with some adaptive lag-dependent computation.
*/
void SetTurnLength(u32 msecs);
@@ -238,6 +245,7 @@ private:
static bool OnInGame(void* context, CFsmEvent* event);
static bool OnChat(void* context, CFsmEvent* event);
static bool OnLoadedGame(void* context, CFsmEvent* event);
static bool OnJoinSyncingLoadedGame(void* context, CFsmEvent* event);
static bool OnDisconnect(void* context, CFsmEvent* event);
void CheckGameLoadStatus(CNetServerSession* changedSession);
@@ -275,6 +283,19 @@ private:
CNetServerTurnManager* m_ServerTurnManager;
/**
* A copy of all simulation commands received so far, indexed by
* turn number, to simplify support for rejoining etc.
* TODO: verify this doesn't use too much RAM.
*/
std::vector<std::vector<CSimulationMessage> > m_SavedCommands;
/**
* The latest copy of the simulation state, received from an existing
* client when a new client has asked to rejoin the game.
*/
std::string m_JoinSyncFile;
private:
// Thread-related stuff:
+5 -3
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2010 Wildfire Games.
/* Copyright (C) 2011 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -28,7 +28,7 @@
static const int CHANNEL_COUNT = 1;
CNetClientSession::CNetClientSession(CNetClient& client) :
m_Client(client), m_Host(NULL), m_Server(NULL), m_Stats(NULL)
m_Client(client), m_FileTransferer(this), m_Host(NULL), m_Server(NULL), m_Stats(NULL)
{
}
@@ -96,6 +96,8 @@ void CNetClientSession::Poll()
{
ENSURE(m_Host && m_Server);
m_FileTransferer.Poll();
ENetEvent event;
while (enet_host_service(m_Host, &event, 0) > 0)
{
@@ -165,7 +167,7 @@ bool CNetClientSession::SendMessage(const CNetMessage* message)
CNetServerSession::CNetServerSession(CNetServerWorker& server, ENetPeer* peer) :
m_Server(server), m_Peer(peer)
m_Server(server), m_FileTransferer(this), m_Peer(peer)
{
}
+25 -5
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2010 Wildfire Games.
/* Copyright (C) 2011 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -19,6 +19,7 @@
#define NETSESSION_H
#include "network/fsm.h"
#include "network/NetFileTransfer.h"
#include "network/NetHost.h"
#include "ps/CStr.h"
#include "scriptinterface/ScriptVal.h"
@@ -37,11 +38,21 @@ class CNetStatsTable;
* A client runs one session at once; a server typically runs many.
*/
/**
* Interface for sessions to which messages can be sent.
*/
class INetSession
{
public:
virtual ~INetSession() {}
virtual bool SendMessage(const CNetMessage* message) = 0;
};
/**
* The client end of a network session.
* Provides an abstraction of the network interface, allowing communication with the server.
*/
class CNetClientSession
class CNetClientSession : public INetSession
{
NONCOPYABLE(CNetClientSession);
@@ -70,10 +81,15 @@ public:
/**
* Send a message to the server.
*/
bool SendMessage(const CNetMessage* message);
virtual bool SendMessage(const CNetMessage* message);
CNetFileTransferer& GetFileTransferer() { return m_FileTransferer; }
private:
CNetClient& m_Client;
CNetFileTransferer m_FileTransferer;
ENetHost* m_Host;
ENetPeer* m_Server;
CNetStatsTable* m_Stats;
@@ -88,7 +104,7 @@ private:
* Thread-safety:
* - This is constructed and used by CNetServerWorker in the network server thread.
*/
class CNetServerSession : public CFsm
class CNetServerSession : public CFsm, public INetSession
{
NONCOPYABLE(CNetServerSession);
@@ -124,11 +140,15 @@ public:
/**
* Send a message to the client.
*/
bool SendMessage(const CNetMessage* message);
virtual bool SendMessage(const CNetMessage* message);
CNetFileTransferer& GetFileTransferer() { return m_FileTransferer; }
private:
CNetServerWorker& m_Server;
CNetFileTransferer m_FileTransferer;
ENetPeer* m_Peer;
CStr m_GUID;
+74 -18
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2010 Wildfire Games.
/* Copyright (C) 2011 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -70,6 +70,16 @@ CNetTurnManager::CNetTurnManager(CSimulation2& simulation, u32 defaultTurnLength
m_QueuedCommands.resize(COMMAND_DELAY + 1);
}
void CNetTurnManager::ResetState(u32 newCurrentTurn, u32 newReadyTurn)
{
m_CurrentTurn = newCurrentTurn;
m_ReadyTurn = newReadyTurn;
m_DeltaTime = 0;
size_t queuedCommandsSize = m_QueuedCommands.size();
m_QueuedCommands.clear();
m_QueuedCommands.resize(queuedCommandsSize);
}
void CNetTurnManager::SetPlayerID(int playerId)
{
m_PlayerId = playerId;
@@ -168,6 +178,45 @@ bool CNetTurnManager::Update(float frameLength, size_t maxTurns)
return true;
}
bool CNetTurnManager::UpdateFastForward()
{
m_DeltaTime = 0;
NETTURN_LOG((L"UpdateFastForward current=%d ready=%d\n", m_CurrentTurn, m_ReadyTurn));
// Check that the next turn is ready for execution
if (m_ReadyTurn <= m_CurrentTurn)
return false;
while (m_ReadyTurn > m_CurrentTurn)
{
// TODO: It would be nice to remove some of the duplication with Update()
// (This is similar but doesn't call any Notify functions or update DeltaTime,
// it just updates the simulation state)
m_CurrentTurn += 1;
m_Simulation2.FlushDestroyedEntities();
// Put all the client commands into a single list, in a globally consistent order
std::vector<SimulationCommand> commands;
for (std::map<u32, std::vector<SimulationCommand> >::iterator it = m_QueuedCommands[0].begin(); it != m_QueuedCommands[0].end(); ++it)
{
commands.insert(commands.end(), it->second.begin(), it->second.end());
}
m_QueuedCommands.pop_front();
m_QueuedCommands.resize(m_QueuedCommands.size() + 1);
m_Replay.Turn(m_CurrentTurn-1, m_TurnLength, commands);
NETTURN_LOG((L"Running %d cmds\n", commands.size()));
m_Simulation2.Update(m_TurnLength, commands);
}
return true;
}
void CNetTurnManager::OnSyncError(u32 turn, const std::string& expectedHash)
{
NETTURN_LOG((L"OnSyncError(%d, %ls)\n", turn, Hexify(expectedHash).c_str()));
@@ -191,7 +240,10 @@ void CNetTurnManager::OnSyncError(u32 turn, const std::string& expectedHash)
msg << L"Out of sync on turn " << turn << L": expected hash " << Hexify(expectedHash) << L"\n\n";
msg << L"Current state: turn " << m_CurrentTurn << L", hash " << Hexify(hash) << L"\n\n";
msg << L"Dumping current state to " << path;
g_GUI->DisplayMessageBox(600, 350, L"Sync error", msg.str());
if (g_GUI)
g_GUI->DisplayMessageBox(600, 350, L"Sync error", msg.str());
else
LOGERROR(L"%ls", msg.str().c_str());
}
void CNetTurnManager::Interpolate(float frameLength)
@@ -263,12 +315,7 @@ void CNetTurnManager::RewindTimeWarp()
// won't do the next snapshot until the appropriate time.
// (Ideally we ought to serialise the turn manager state and restore it
// here, but this is simpler for now.)
m_CurrentTurn = 0;
m_ReadyTurn = 1;
m_DeltaTime = 0;
size_t queuedCommandsSize = m_QueuedCommands.size();
m_QueuedCommands.clear();
m_QueuedCommands.resize(queuedCommandsSize);
ResetState(0, 1);
}
void CNetTurnManager::QuickSave()
@@ -304,12 +351,7 @@ void CNetTurnManager::QuickLoad()
LOGMESSAGERENDER(L"Quickloaded game");
// See RewindTimeWarp
m_CurrentTurn = 0;
m_ReadyTurn = 1;
m_DeltaTime = 0;
size_t queuedCommandsSize = m_QueuedCommands.size();
m_QueuedCommands.clear();
m_QueuedCommands.resize(queuedCommandsSize);
ResetState(0, 1);
}
@@ -411,6 +453,10 @@ void CNetLocalTurnManager::OnSimulationMessage(CSimulationMessage* UNUSED(msg))
CNetServerTurnManager::CNetServerTurnManager(CNetServerWorker& server) :
m_NetServer(server), m_ReadyTurn(1), m_TurnLength(DEFAULT_TURN_LENGTH_MP)
{
// The first turn we will actually execute is number 2,
// so store dummy values into the saved lengths list
m_SavedTurnLengths.push_back(0);
m_SavedTurnLengths.push_back(0);
}
void CNetServerTurnManager::NotifyFinishedClientCommands(int client, u32 turn)
@@ -448,6 +494,10 @@ void CNetServerTurnManager::CheckClientsReady()
msg.m_TurnLength = m_TurnLength;
msg.m_Turn = m_ReadyTurn;
m_NetServer.Broadcast(&msg);
// Save the turn length in case it's needed later
ENSURE(m_SavedTurnLengths.size() == m_ReadyTurn);
m_SavedTurnLengths.push_back(m_TurnLength);
}
void CNetServerTurnManager::NotifyFinishedClientUpdate(int client, u32 turn, const std::string& hash)
@@ -497,13 +547,13 @@ void CNetServerTurnManager::NotifyFinishedClientUpdate(int client, u32 turn, con
m_ClientStateHashes.erase(m_ClientStateHashes.begin(), m_ClientStateHashes.lower_bound(newest+1));
}
void CNetServerTurnManager::InitialiseClient(int client)
void CNetServerTurnManager::InitialiseClient(int client, u32 turn)
{
NETTURN_LOG((L"InitialiseClient(client=%d)\n", client));
NETTURN_LOG((L"InitialiseClient(client=%d, turn=%d)\n", client, turn));
ENSURE(m_ClientsReady.find(client) == m_ClientsReady.end());
m_ClientsReady[client] = 1;
m_ClientsSimulated[client] = 0;
m_ClientsReady[client] = turn + 1;
m_ClientsSimulated[client] = turn;
}
void CNetServerTurnManager::UninitialiseClient(int client)
@@ -523,3 +573,9 @@ void CNetServerTurnManager::SetTurnLength(u32 msecs)
{
m_TurnLength = msecs;
}
u32 CNetServerTurnManager::GetSavedTurnLength(u32 turn)
{
ENSURE(turn <= m_ReadyTurn);
return m_SavedTurnLengths.at(turn);
}
+28 -2
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2010 Wildfire Games.
/* Copyright (C) 2011 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -60,6 +60,8 @@ public:
virtual ~CNetTurnManager() { }
void ResetState(u32 newCurrentTurn, u32 newReadyTurn);
/**
* Set the current user's player ID, which will be added into command messages.
*/
@@ -74,6 +76,13 @@ public:
*/
bool Update(float frameLength, size_t maxTurns);
/**
* Advance the simulation by as much as possible. Intended for catching up
* over a small number of turns when rejoining a multiplayer match.
* Returns true if it advanced by at least one turn.
*/
bool UpdateFastForward();
/**
* Returns whether Update(frameLength, ...) will process at least one new turn.
* @param frameLength length of the previous frame in seconds
@@ -122,6 +131,8 @@ public:
void QuickSave();
void QuickLoad();
u32 GetCurrentTurn() { return m_CurrentTurn; }
protected:
/**
* Store a command to be executed at a given turn.
@@ -234,7 +245,7 @@ public:
/**
* Inform the turn manager of a new client who will be sending commands.
*/
void InitialiseClient(int client);
void InitialiseClient(int client, u32 turn);
/**
* Inform the turn manager that a previously-initialised client has left the game
@@ -244,6 +255,18 @@ public:
void SetTurnLength(u32 msecs);
/**
* Returns the latest turn for which all clients are ready;
* they will have already been told to execute this turn.
*/
u32 GetReadyTurn() { return m_ReadyTurn; }
/**
* Returns the turn length that was used for the given turn.
* Requires turn <= GetReadyTurn().
*/
u32 GetSavedTurnLength(u32 turn);
protected:
void CheckClientsReady();
@@ -263,6 +286,9 @@ protected:
// Current turn length
u32 m_TurnLength;
// Turn lengths for all previously executed turns
std::vector<u32> m_SavedTurnLengths;
CNetServerWorker& m_NetServer;
};
+140 -2
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2010 Wildfire Games.
/* Copyright (C) 2011 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -38,7 +38,7 @@ public:
void setUp()
{
g_VFS = CreateVfs(20 * MiB);
TS_ASSERT_OK(g_VFS->Mount(L"", DataDir()/"mods/public", VFS_MOUNT_MUST_EXIST));
TS_ASSERT_OK(g_VFS->Mount(L"", DataDir()/"mods"/"public", VFS_MOUNT_MUST_EXIST));
TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir()/"_testcache"));
CXeromyces::Startup();
@@ -195,4 +195,142 @@ public:
client3Game.GetTurnManager()->Update(1.0f, 1);
wait(clients, 100);
}
void test_rejoin_DISABLED()
{
ScriptInterface scriptInterface("Engine", "Test", ScriptInterface::CreateRuntime());
TestStdoutLogger logger;
std::vector<CNetClient*> clients;
CGame client1Game(true);
CGame client2Game(true);
CGame client3Game(true);
CNetServer server;
CScriptValRooted attrs;
scriptInterface.Eval("({mapType:'scenario',map:'_default',thing:'example'})", attrs);
server.UpdateGameAttributes(attrs.get(), scriptInterface);
CNetClient client1(&client1Game);
CNetClient client2(&client2Game);
CNetClient client3(&client3Game);
client1.SetUserName(L"alice");
client2.SetUserName(L"bob");
client3.SetUserName(L"charlie");
clients.push_back(&client1);
clients.push_back(&client2);
clients.push_back(&client3);
connect(server, clients);
debug_printf(L"%ls", client1.TestReadGuiMessages().c_str());
server.StartGame();
SDL_Delay(100);
for (size_t j = 0; j < clients.size(); ++j)
{
clients[j]->Poll();
TS_ASSERT_OK(LDR_NonprogressiveLoad());
clients[j]->LoadFinished();
}
wait(clients, 100);
{
CScriptValRooted cmd;
client1.GetScriptInterface().Eval("({type:'debug-print', message:'[>>> client1 test sim command 1]\\n'})", cmd);
client1Game.GetTurnManager()->PostCommand(cmd);
}
wait(clients, 100);
client1Game.GetTurnManager()->Update(1.0f, 1);
client2Game.GetTurnManager()->Update(1.0f, 1);
client3Game.GetTurnManager()->Update(1.0f, 1);
wait(clients, 100);
{
CScriptValRooted cmd;
client1.GetScriptInterface().Eval("({type:'debug-print', message:'[>>> client1 test sim command 2]\\n'})", cmd);
client1Game.GetTurnManager()->PostCommand(cmd);
}
debug_printf(L"==== Disconnecting client 2\n");
client2.DestroyConnection();
clients.erase(clients.begin()+1);
debug_printf(L"==== Connecting client 2B\n");
CGame client2BGame(true);
CNetClient client2B(&client2BGame);
client2B.SetUserName(L"bob");
clients.push_back(&client2B);
TS_ASSERT(client2B.SetupConnection("127.0.0.1"));
for (size_t i = 0; ; ++i)
{
debug_printf(L"[%d]\n", client2B.GetCurrState());
client2B.Poll();
if (client2B.GetCurrState() == NCS_PREGAME)
break;
if (client2B.GetCurrState() == NCS_UNCONNECTED)
{
TS_FAIL("connection rejected");
return;
}
if (i > 20)
{
TS_FAIL("connection timeout");
return;
}
SDL_Delay(100);
}
wait(clients, 100);
client1Game.GetTurnManager()->Update(1.0f, 1);
client3Game.GetTurnManager()->Update(1.0f, 1);
wait(clients, 100);
server.SetTurnLength(100);
client1Game.GetTurnManager()->Update(1.0f, 1);
client3Game.GetTurnManager()->Update(1.0f, 1);
wait(clients, 100);
// (This SetTurnLength thing doesn't actually detect errors unless you change
// CNetTurnManager::TurnNeedsFullHash to always return true)
{
CScriptValRooted cmd;
client1.GetScriptInterface().Eval("({type:'debug-print', message:'[>>> client1 test sim command 3]\\n'})", cmd);
client1Game.GetTurnManager()->PostCommand(cmd);
}
clients[2]->Poll();
TS_ASSERT_OK(LDR_NonprogressiveLoad());
clients[2]->LoadFinished();
wait(clients, 100);
{
CScriptValRooted cmd;
client1.GetScriptInterface().Eval("({type:'debug-print', message:'[>>> client1 test sim command 4]\\n'})", cmd);
client1Game.GetTurnManager()->PostCommand(cmd);
}
for (size_t i = 0; i < 3; ++i)
{
client1Game.GetTurnManager()->Update(1.0f, 1);
client2BGame.GetTurnManager()->Update(1.0f, 1);
client3Game.GetTurnManager()->Update(1.0f, 1);
wait(clients, 100);
}
}
};