mirror of
https://gitea.wildfiregames.com/0ad/0ad.git
synced 2026-06-21 01:04:06 +00:00
# Support rejoining multiplayer games after disconnection.
This was SVN commit r10437.
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 << " }";
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user