From bfe2126a1716fd35fab9d730b1da0dc317a21e2a Mon Sep 17 00:00:00 2001 From: Ykkrosh Date: Thu, 27 Oct 2011 16:46:48 +0000 Subject: [PATCH] # Support rejoining multiplayer games after disconnection. This was SVN commit r10437. --- source/network/NetClient.cpp | 135 ++++++++++++++- source/network/NetClient.h | 17 +- source/network/NetFileTransfer.cpp | 159 ++++++++++++++++++ source/network/NetFileTransfer.h | 131 +++++++++++++++ source/network/NetHost.h | 16 +- source/network/NetMessage.cpp | 22 ++- source/network/NetMessage.h | 5 +- source/network/NetMessageSim.cpp | 14 +- source/network/NetMessages.h | 39 ++++- source/network/NetServer.cpp | 254 +++++++++++++++++++++++++---- source/network/NetServer.h | 25 ++- source/network/NetSession.cpp | 8 +- source/network/NetSession.h | 30 +++- source/network/NetTurnManager.cpp | 92 +++++++++-- source/network/NetTurnManager.h | 30 +++- source/network/tests/test_Net.h | 142 +++++++++++++++- 16 files changed, 1038 insertions(+), 81 deletions(-) create mode 100644 source/network/NetFileTransfer.cpp create mode 100644 source/network/NetFileTransfer.h diff --git a/source/network/NetClient.cpp b/source/network/NetClient.cpp index e22486ef98..543addda15 100644 --- a/source/network/NetClient.cpp +++ b/source/network/NetClient.cpp @@ -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(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); diff --git a/source/network/NetClient.h b/source/network/NetClient.h index c330b3d341..c9b45de687 100644 --- a/source/network/NetClient.h +++ b/source/network/NetClient.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 @@ -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 m_GuiMessageQueue; + + /// Serialized game state received when joining an in-progress game + std::string m_JoinSyncBuffer; }; /// Global network client for the standard game diff --git a/source/network/NetFileTransfer.cpp b/source/network/NetFileTransfer.cpp new file mode 100644 index 0000000000..200cd1bd93 --- /dev/null +++ b/source/network/NetFileTransfer.cpp @@ -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 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 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& 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 +} \ No newline at end of file diff --git a/source/network/NetFileTransfer.h b/source/network/NetFileTransfer.h new file mode 100644 index 0000000000..91242b8422 --- /dev/null +++ b/source/network/NetFileTransfer.h @@ -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 . + */ + +#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& 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 > FileReceiveTasksMap; + FileReceiveTasksMap m_FileReceiveTasks; + + typedef std::map FileSendTasksMap; + FileSendTasksMap m_FileSendTasks; + + double m_LastProgressReportTime; +}; + +#endif // NETFILETRANSFER_H \ No newline at end of file diff --git a/source/network/NetHost.h b/source/network/NetHost.h index 49a2116922..7433140ebb 100644 --- a/source/network/NetHost.h +++ b/source/network/NetHost.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 PlayerAssignmentMap; // map from GUID -> assignment diff --git a/source/network/NetMessage.cpp b/source/network/NetMessage.cpp index 7b46826399..b5a5d89b1e 100644 --- a/source/network/NetMessage.cpp +++ b/source/network/NetMessage.cpp @@ -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; diff --git a/source/network/NetMessage.h b/source/network/NetMessage.h index 54eea69630..3a79421e19 100644 --- a/source/network/NetMessage.h +++ b/source/network/NetMessage.h @@ -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); diff --git a/source/network/NetMessageSim.cpp b/source/network/NetMessageSim.cpp index f2bb5871bb..5ff765c47a 100644 --- a/source/network/NetMessageSim.cpp +++ b/source/network/NetMessageSim.cpp @@ -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 << " }"; diff --git a/source/network/NetMessages.h b/source/network/NetMessages.h index 7a4d6f68ec..123e70d055 100644 --- a/source/network/NetMessages.h +++ b/source/network/NetMessages.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 @@ -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) diff --git a/source/network/NetServer.cpp b/source/network/NetServer.cpp index ce25bb2db4..953d993617 100644 --- a/source/network/NetServer.cpp +++ b/source/network/NetServer.cpp @@ -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 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(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; diff --git a/source/network/NetServer.h b/source/network/NetServer.h index 7b67c68655..350779c211 100644 --- a/source/network/NetServer.h +++ b/source/network/NetServer.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 @@ -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 > 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: diff --git a/source/network/NetSession.cpp b/source/network/NetSession.cpp index 3e9ec9f73e..28ca8445fc 100644 --- a/source/network/NetSession.cpp +++ b/source/network/NetSession.cpp @@ -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) { } diff --git a/source/network/NetSession.h b/source/network/NetSession.h index de03959f95..dc35f13b9a 100644 --- a/source/network/NetSession.h +++ b/source/network/NetSession.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 @@ -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; diff --git a/source/network/NetTurnManager.cpp b/source/network/NetTurnManager.cpp index a346c93d5d..5c529c99a9 100644 --- a/source/network/NetTurnManager.cpp +++ b/source/network/NetTurnManager.cpp @@ -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 commands; + for (std::map >::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); +} diff --git a/source/network/NetTurnManager.h b/source/network/NetTurnManager.h index babafbbec0..6c4ded5e0c 100644 --- a/source/network/NetTurnManager.h +++ b/source/network/NetTurnManager.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 @@ -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 m_SavedTurnLengths; + CNetServerWorker& m_NetServer; }; diff --git a/source/network/tests/test_Net.h b/source/network/tests/test_Net.h index fde56bea68..5300b0edb4 100644 --- a/source/network/tests/test_Net.h +++ b/source/network/tests/test_Net.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 @@ -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 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); + } + } };