From 81f5e0ac5f03fe1f8b31990f17dbedf079c554fa Mon Sep 17 00:00:00 2001 From: Ykkrosh Date: Wed, 3 Nov 2010 00:21:52 +0000 Subject: [PATCH] Allow continuing playing when a client leaves a multiplayer game (fixes #620). Fix multiplayer autostart. Make NETTURN_LOG syntax differently ugly. This was SVN commit r8525. --- .../mods/public/gui/session_new/messages.js | 31 +++++-- .../gui/session_new/selection_details.js | 6 ++ .../mods/public/gui/session_new/session.js | 5 +- .../mods/public/gui/session_new/session.xml | 2 +- .../gui/session_new/utility_functions.js | 35 +++++++- source/network/NetServer.cpp | 7 +- source/network/NetTurnManager.cpp | 84 ++++++++++--------- source/network/NetTurnManager.h | 11 +++ source/ps/CLogger.cpp | 2 +- source/ps/GameSetup/GameSetup.cpp | 19 +++-- 10 files changed, 141 insertions(+), 61 deletions(-) diff --git a/binaries/data/mods/public/gui/session_new/messages.js b/binaries/data/mods/public/gui/session_new/messages.js index 70904183c2..3737eac77f 100644 --- a/binaries/data/mods/public/gui/session_new/messages.js +++ b/binaries/data/mods/public/gui/session_new/messages.js @@ -45,7 +45,7 @@ function displayNotifications() getGUIObjectByName("notificationText").caption = messages.join("\n"); } -//Messages +// Messages function handleNetMessage(message) { log("Net message: "+uneval(message)); @@ -74,22 +74,39 @@ function handleNetMessage(message) break; } break; + case "players": // Find and report all leavings for (var host in g_PlayerAssignments) { if (! message.hosts[host]) { - var obj = getGUIObjectByName("netStatus"); - obj.caption = g_PlayerAssignments[host].name + " has disconnected.\n\nThe game has ended."; - obj.hidden = false; - getGUIObjectByName("disconnectedExitButton").hidden = false; + // Tell the user about the disconnection + addChatMessage({ "type": "disconnect", "guid": host }); + + // Update the cached player data, so we can display the disconnection status + updatePlayerDataRemove(g_Players, host); } } + + // Find and report all joinings + for (var host in message.hosts) + { + if (! g_PlayerAssignments[host]) + { + // Update the cached player data, so we can display the correct name + updatePlayerDataAdd(g_Players, host, message.hosts[host]); + } + } + + g_PlayerAssignments = message.hosts; + break; + case "chat": addChatMessage({ "type": "message", "guid": message.guid, "text": message.text }); break; + default: error("Unrecognised net message type "+message.type); } @@ -136,11 +153,9 @@ function addChatMessage(msg) switch (msg.type) { - /* case "disconnect": - formatted = "<[color=\"" + playerColor + "\"]" + username + "[/color]> has left"; + formatted = "[color=\"" + playerColor + "\"]" + username + "[/color] has left the game."; break; - */ case "message": console.write("<" + username + "> " + message); formatted = "<[color=\"" + playerColor + "\"]" + username + "[/color]> " + message; diff --git a/binaries/data/mods/public/gui/session_new/selection_details.js b/binaries/data/mods/public/gui/session_new/selection_details.js index 2cd86d366f..7606540c37 100644 --- a/binaries/data/mods/public/gui/session_new/selection_details.js +++ b/binaries/data/mods/public/gui/session_new/selection_details.js @@ -38,6 +38,12 @@ function displaySingle(entState, template) var playerColor = g_Players[entState.player].color.r + " " + g_Players[entState.player].color.g + " " + g_Players[entState.player].color.b+ " " + g_Players[entState.player].color.a; + // Indicate disconnected players by prefixing their name + if (g_Players[entState.player].offline) + { + playerName = "[OFFLINE] " + playerName; + } + // Rank getGUIObjectByName("rankIcon").cell_id = getRankIconCellId(entState); diff --git a/binaries/data/mods/public/gui/session_new/session.js b/binaries/data/mods/public/gui/session_new/session.js index eb89568237..472e21b252 100644 --- a/binaries/data/mods/public/gui/session_new/session.js +++ b/binaries/data/mods/public/gui/session_new/session.js @@ -66,7 +66,10 @@ function init(initData, hotloadData) { g_IsNetworked = initData.isNetworked; // Set network mode g_PlayerAssignments = initData.playerAssignments; - g_Players = getPlayerData(initData.playerAssignments); // Cache the player data + + // Cache the player data + // (This may be updated at runtime by handleNetMessage) + g_Players = getPlayerData(g_PlayerAssignments); } else // Needed for autostart loading option { diff --git a/binaries/data/mods/public/gui/session_new/session.xml b/binaries/data/mods/public/gui/session_new/session.xml index b002a37d71..ba4fc6dbcc 100644 --- a/binaries/data/mods/public/gui/session_new/session.xml +++ b/binaries/data/mods/public/gui/session_new/session.xml @@ -459,7 +459,7 @@ - + diff --git a/binaries/data/mods/public/gui/session_new/utility_functions.js b/binaries/data/mods/public/gui/session_new/utility_functions.js index 4560292442..ba74c33ac1 100644 --- a/binaries/data/mods/public/gui/session_new/utility_functions.js +++ b/binaries/data/mods/public/gui/session_new/utility_functions.js @@ -25,23 +25,54 @@ function getPlayerData(playerAssignments) var civ = playerState.civ; var color = {"r": playerState.colour.r*255, "g": playerState.colour.g*255, "b": playerState.colour.b*255, "a": playerState.colour.a*255}; - var player = {"name": name, "civ": civ, "color": color, "team": playerState.team, "diplomacy": playerState.diplomacy, "state": playerState.state}; + var player = { + "name": name, + "civ": civ, + "color": color, + "team": playerState.team, + "diplomacy": playerState.diplomacy, + "state": playerState.state, + "guid": undefined, // network guid for players controlled by hosts + "disconnected": false, // flag for host-controlled players who have left the game + }; players.push(player); } // Overwrite default player names with multiplayer names if (playerAssignments) { - for each (var playerAssignment in playerAssignments) + for (var playerGuid in playerAssignments) { + var playerAssignment = playerAssignments[playerGuid]; if (players[playerAssignment.player]) + { + players[playerAssignment.player].guid = playerGuid; players[playerAssignment.player].name = playerAssignment.name; + } } } return players; } +// Update player data when a host has connected +function updatePlayerDataAdd(players, hostGuid, playerAssignment) +{ + if (players[playerAssignment.player]) + { + players[playerAssignment.player].guid = hostGuid; + players[playerAssignment.player].name = playerAssignment.name; + } +} + +// Update player data when a host has disconnected +function updatePlayerDataRemove(players, hostGuid) +{ + for each (var player in players) + if (player.guid == hostGuid) + player.offline = true; +} + function isUnit(entState) { if (entState.identity) diff --git a/source/network/NetServer.cpp b/source/network/NetServer.cpp index 1bb9dd59c6..22f147edf2 100644 --- a/source/network/NetServer.cpp +++ b/source/network/NetServer.cpp @@ -56,7 +56,6 @@ static CStr DebugName(CNetServerSession* session) /* * XXX: We use some non-threadsafe functions from the worker thread. * See http://trac.wildfiregames.com/ticket/654 - * and http://trac.wildfiregames.com/ticket/653 */ CNetServerWorker::CNetServerWorker(int autostartPlayers) : @@ -399,6 +398,12 @@ void CNetServerWorker::OnUserJoin(CNetServerSession* session) void CNetServerWorker::OnUserLeave(CNetServerSession* session) { RemovePlayer(session->GetGUID()); + + if (m_ServerTurnManager) + m_ServerTurnManager->UninitialiseClient(session->GetHostID()); // TODO: only for non-observers + + // TODO: ought to switch the player controlled by that client + // back to AI control, or something? } void CNetServerWorker::AddPlayer(const CStr& guid, const CStrW& name) diff --git a/source/network/NetTurnManager.cpp b/source/network/NetTurnManager.cpp index 85f4127895..4013647dfa 100644 --- a/source/network/NetTurnManager.cpp +++ b/source/network/NetTurnManager.cpp @@ -40,7 +40,11 @@ static const int DEFAULT_TURN_LENGTH_SP = 200; static const int COMMAND_DELAY = 2; -//#define NETTURN_LOG debug_printf +#if 0 +#define NETTURN_LOG(args) debug_printf args +#else +#define NETTURN_LOG(args) +#endif static std::string Hexify(const std::string& s) { @@ -77,9 +81,7 @@ bool CNetTurnManager::Update(float frameLength) if (m_DeltaTime < 0) return false; -#ifdef NETTURN_LOG - NETTURN_LOG(L"Update current=%d ready=%d\n", m_CurrentTurn, m_ReadyTurn); -#endif + NETTURN_LOG((L"Update current=%d ready=%d\n", m_CurrentTurn, m_ReadyTurn)); // Check that the next turn is ready for execution if (m_ReadyTurn > m_CurrentTurn) @@ -99,9 +101,7 @@ bool CNetTurnManager::Update(float frameLength) m_Replay.Turn(m_CurrentTurn-1, m_TurnLength, commands); -#ifdef NETTURN_LOG - NETTURN_LOG(L"Running %d cmds\n", commands.size()); -#endif + NETTURN_LOG((L"Running %d cmds\n", commands.size())); m_Simulation2.Update(m_TurnLength, commands); @@ -130,9 +130,7 @@ bool CNetTurnManager::Update(float frameLength) void CNetTurnManager::OnSyncError(u32 turn, const std::string& expectedHash) { -#ifdef NETTURN_LOG - NETTURN_LOG(L"OnSyncError(%d, %hs)\n", turn, Hexify(expectedHash).c_str()); -#endif + NETTURN_LOG((L"OnSyncError(%d, %hs)\n", turn, Hexify(expectedHash).c_str())); // Only complain the first time if (m_HasSyncError) @@ -166,9 +164,7 @@ void CNetTurnManager::Interpolate(float frameLength) void CNetTurnManager::AddCommand(int client, int player, CScriptValRooted data, u32 turn) { -#ifdef NETTURN_LOG - NETTURN_LOG(L"AddCommand(client=%d player=%d turn=%d)\n", client, player, turn); -#endif + NETTURN_LOG((L"AddCommand(client=%d player=%d turn=%d)\n", client, player, turn)); if (!(m_CurrentTurn < turn && turn <= m_CurrentTurn + COMMAND_DELAY + 1)) { @@ -184,9 +180,7 @@ void CNetTurnManager::AddCommand(int client, int player, CScriptValRooted data, void CNetTurnManager::FinishedAllCommands(u32 turn, u32 turnLength) { -#ifdef NETTURN_LOG - NETTURN_LOG(L"FinishedAllCommands(%d, %d)\n", turn, turnLength); -#endif + NETTURN_LOG((L"FinishedAllCommands(%d, %d)\n", turn, turnLength)); debug_assert(turn == m_ReadyTurn + 1); m_ReadyTurn = turn; @@ -201,9 +195,7 @@ CNetClientTurnManager::CNetClientTurnManager(CSimulation2& simulation, CNetClien void CNetClientTurnManager::PostCommand(CScriptValRooted data) { -#ifdef NETTURN_LOG - NETTURN_LOG(L"PostCommand()\n"); -#endif + NETTURN_LOG((L"PostCommand()\n")); // Transmit command to server CSimulationMessage msg(m_Simulation2.GetScriptInterface(), m_ClientId, m_PlayerId, m_CurrentTurn + COMMAND_DELAY, data.get()); @@ -216,9 +208,7 @@ void CNetClientTurnManager::PostCommand(CScriptValRooted data) void CNetClientTurnManager::NotifyFinishedOwnCommands(u32 turn) { -#ifdef NETTURN_LOG - NETTURN_LOG(L"NotifyFinishedOwnCommands(%d)\n", turn); -#endif + NETTURN_LOG((L"NotifyFinishedOwnCommands(%d)\n", turn)); // Send message to the server CEndCommandBatchMessage msg; @@ -229,10 +219,6 @@ void CNetClientTurnManager::NotifyFinishedOwnCommands(u32 turn) void CNetClientTurnManager::NotifyFinishedUpdate(u32 turn) { -#ifdef NETTURN_LOG - NETTURN_LOG(L"NotifyFinishedUpdate(%d, %hs)\n", turn, Hexify(hash).c_str()); -#endif - std::string hash; { PROFILE("state hash check"); @@ -240,6 +226,8 @@ void CNetClientTurnManager::NotifyFinishedUpdate(u32 turn) debug_assert(ok); } + NETTURN_LOG((L"NotifyFinishedUpdate(%d, %hs)\n", turn, Hexify(hash).c_str())); + m_Replay.Hash(hash); // Send message to the server @@ -301,9 +289,7 @@ CNetServerTurnManager::CNetServerTurnManager(CNetServerWorker& server) : void CNetServerTurnManager::NotifyFinishedClientCommands(int client, u32 turn) { -#ifdef NETTURN_LOG - NETTURN_LOG(L"NotifyFinishedClientCommands(client=%d, turn=%d)\n", client, turn); -#endif + NETTURN_LOG((L"NotifyFinishedClientCommands(client=%d, turn=%d)\n", client, turn)); // Must be a client we've already heard of debug_assert(m_ClientsReady.find(client) != m_ClientsReady.end()); @@ -312,23 +298,30 @@ void CNetServerTurnManager::NotifyFinishedClientCommands(int client, u32 turn) debug_assert(turn == m_ClientsReady[client] + 1); m_ClientsReady[client] = turn; + // Check whether this was the final client to become ready + CheckClientsReady(); +} + +void CNetServerTurnManager::CheckClientsReady() +{ // See if all clients (including self) are ready for a new turn for (std::map::iterator it = m_ClientsReady.begin(); it != m_ClientsReady.end(); ++it) { -#ifdef NETTURN_LOG - NETTURN_LOG(L" %d: %d <=? %d\n", it->first, it->second, m_ReadyTurn); -#endif + NETTURN_LOG((L" %d: %d <=? %d\n", it->first, it->second, m_ReadyTurn)); if (it->second <= m_ReadyTurn) - return; + return; // wasn't ready for m_ReadyTurn+1 } + // Advance the turn + ++m_ReadyTurn; + + NETTURN_LOG((L"CheckClientsReady: ready for turn %d\n", m_ReadyTurn)); + // Tell all clients that the next turn is ready CEndCommandBatchMessage msg; msg.m_TurnLength = m_TurnLength; - msg.m_Turn = turn; + msg.m_Turn = m_ReadyTurn; m_NetServer.Broadcast(&msg); - - m_ReadyTurn = turn; } void CNetServerTurnManager::NotifyFinishedClientUpdate(int client, u32 turn, const std::string& hash) @@ -358,9 +351,7 @@ void CNetServerTurnManager::NotifyFinishedClientUpdate(int client, u32 turn, con for (std::map::iterator cit = it->second.begin(); cit != it->second.end(); ++cit) { -#ifdef NETTURN_LOG - NETTURN_LOG(L"sync check %d: %d = %hs\n", it->first, cit->first, Hexify(cit->second).c_str()); -#endif + NETTURN_LOG((L"sync check %d: %d = %hs\n", it->first, cit->first, Hexify(cit->second).c_str())); if (cit->second != expected) { // Oh no, out of sync @@ -382,11 +373,24 @@ void CNetServerTurnManager::NotifyFinishedClientUpdate(int client, u32 turn, con void CNetServerTurnManager::InitialiseClient(int client) { + NETTURN_LOG((L"InitialiseClient(client=%d)\n", client)); + debug_assert(m_ClientsReady.find(client) == m_ClientsReady.end()); m_ClientsReady[client] = 1; m_ClientsSimulated[client] = 0; +} - // TODO: do we need some kind of UninitialiseClient in case they leave? +void CNetServerTurnManager::UninitialiseClient(int client) +{ + NETTURN_LOG((L"UninitialiseClient(client=%d)\n", client)); + + debug_assert(m_ClientsReady.find(client) != m_ClientsReady.end()); + m_ClientsReady.erase(client); + m_ClientsSimulated.erase(client); + + // Check whether we're ready for the next turn now that we're not + // waiting for this client any more + CheckClientsReady(); } void CNetServerTurnManager::SetTurnLength(u32 msecs) diff --git a/source/network/NetTurnManager.h b/source/network/NetTurnManager.h index 945a00aaa1..153d0cf95b 100644 --- a/source/network/NetTurnManager.h +++ b/source/network/NetTurnManager.h @@ -197,11 +197,22 @@ public: void NotifyFinishedClientUpdate(int client, u32 turn, const std::string& hash); + /** + * Inform the turn manager of a new client who will be sending commands. + */ void InitialiseClient(int client); + /** + * Inform the turn manager that a previously-initialised client has left the game + * and will no longer be sending commands. + */ + void UninitialiseClient(int client); + void SetTurnLength(u32 msecs); protected: + void CheckClientsReady(); + /// The latest turn for which we have received all commands from all clients u32 m_ReadyTurn; diff --git a/source/ps/CLogger.cpp b/source/ps/CLogger.cpp index d0588a4206..de2752701a 100644 --- a/source/ps/CLogger.cpp +++ b/source/ps/CLogger.cpp @@ -342,7 +342,7 @@ void CLogger::Render() void CLogger::PushRenderMessage(ELogMethod method, const wchar_t* message) { - double now = timer_Time(); // XXX: this is not thread-safe (http://trac.wildfiregames.com/ticket/653) + double now = timer_Time(); // Add each message line separately const wchar_t* pos = message; diff --git a/source/ps/GameSetup/GameSetup.cpp b/source/ps/GameSetup/GameSetup.cpp index 5a1f474878..200882ee12 100644 --- a/source/ps/GameSetup/GameSetup.cpp +++ b/source/ps/GameSetup/GameSetup.cpp @@ -458,7 +458,7 @@ static void InitVfs(const CmdLineArgs& args) } -static void InitPs(bool setup_gui, const CStrW& gui_page) +static void InitPs(bool setup_gui, const CStrW& gui_page, CScriptVal initData) { { // console @@ -490,12 +490,12 @@ static void InitPs(bool setup_gui, const CStrW& gui_page) { // We do actually need *some* kind of GUI loaded, so use the // (currently empty) Atlas one - g_GUI->SwitchPage(L"page_atlas.xml", JSVAL_VOID); + g_GUI->SwitchPage(L"page_atlas.xml", initData); return; } // GUI uses VFS, so this must come after VFS init. - g_GUI->SwitchPage(gui_page, JSVAL_VOID); + g_GUI->SwitchPage(gui_page, initData); // Warn nicely about missing S3TC support if (!ogl_tex_has_s3tc()) @@ -887,7 +887,7 @@ void InitGraphics(const CmdLineArgs& args, int flags) if (!Autostart(args)) { const bool setup_gui = ((flags & INIT_NO_GUI) == 0); - InitPs(setup_gui, L"page_pregame.xml"); + InitPs(setup_gui, L"page_pregame.xml", JSVAL_VOID); } } @@ -918,9 +918,14 @@ static bool Autostart(const CmdLineArgs& args) scriptInterface.SetProperty(attrs.get(), "mapType", std::string("scenario"), false); scriptInterface.SetProperty(attrs.get(), "map", std::string(autostartMap), false); + CScriptValRooted mpInitData; + g_GUI->GetScriptInterface().Eval("({isNetworked:true, playerAssignments:{}})", mpInitData); + g_GUI->GetScriptInterface().SetProperty(mpInitData.get(), "attribs", + CScriptVal(g_GUI->GetScriptInterface().CloneValueFromOtherContext(scriptInterface, attrs.get())), false); + if (args.Has("autostart-host")) { - InitPs(true, L"page_loading.xml"); + InitPs(true, L"page_loading.xml", mpInitData.get()); size_t maxPlayers = 2; if (args.Has("autostart-players")) @@ -939,7 +944,7 @@ static bool Autostart(const CmdLineArgs& args) } else if (args.Has("autostart-client")) { - InitPs(true, L"page_loading.xml"); + InitPs(true, L"page_loading.xml", mpInitData.get()); g_NetClient = new CNetClient(g_Game); // TODO: player name, etc @@ -959,7 +964,7 @@ static bool Autostart(const CmdLineArgs& args) PSRETURN ret = g_Game->ReallyStartGame(); debug_assert(ret == PSRETURN_OK); - InitPs(true, L"page_session_new.xml"); + InitPs(true, L"page_session_new.xml", JSVAL_VOID); } return true;