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.
This commit is contained in:
Ykkrosh 2010-11-03 00:21:52 +00:00
parent 786279fb95
commit 81f5e0ac5f
10 changed files with 141 additions and 61 deletions

View File

@ -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;

View File

@ -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);

View File

@ -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
{

View File

@ -459,7 +459,7 @@
<object size="50%-112 0 50%+112 100%" name="detailsAreaSingle">
<!-- Player Name -->
<object size="0 10 100% 30" name="player" type="text" style="largeCenteredOutlinedText" tooltip_style="snToolTip"/>
<object size="0 0 100% 40" name="player" type="text" style="largeCenteredOutlinedText" tooltip_style="snToolTip"/>
<!-- Stats -->
<object size="6 36 50 100%" name="statsArea" type="image">

View File

@ -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)

View File

@ -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)

View File

@ -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<int, u32>::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<int, std::string>::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)

View File

@ -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;

View File

@ -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;

View File

@ -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;