Submit and display more information about matches in the lobby. Patch by Imarok, refs #3476.

Includes team numbers, online/offline- and won/defeated state, AI type
and difficulty for running games and
only the playernames with observer-player distinction in the gamesetup.
Use JSON format inside the XML stanza and minimize traffic by packing
teams.

Use the observer distinction to correctly apply the "full games" trigger
in the lobby, fixes #3143.

XPartaMupp patch applied by scythetwirler.
unescapeText function by sanderd17, refs #3409.

This was SVN commit r18534.
This commit is contained in:
elexis 2016-07-20 16:04:23 +00:00
parent cf04bad4bc
commit d7d0a7f869
8 changed files with 227 additions and 54 deletions

View file

@ -59,12 +59,56 @@ function sortNameIgnoreCase(x, y)
* Escape tag start and escape characters, so users cannot use special formatting.
* Also limit string length to 256 characters (not counting escape characters).
*/
function escapeText(text)
function escapeText(text, limitLength = true)
{
if (!text)
return text;
return text.substr(0, 255).replace(/\\/g, "\\\\").replace(/\[/g, "\\[");
if (limitLength)
text = text.substr(0, 255);
return text.replace(/\\/g, "\\\\").replace(/\[/g, "\\[");
}
function unescapeText(text)
{
if (!text)
return text;
return text.replace(/\\\\/g, "\\").replace(/\\\[/g, "\[");
}
/**
* Merge players by team to remove duplicate Team entries, thus reducing the packet size of the lobby report.
*/
function playerDataToStringifiedTeamList(playerData)
{
let teamList = {};
for (let pData of playerData)
{
let team = pData.Team === undefined ? -1 : pData.Team;
if (!teamList[team])
teamList[team] = [];
teamList[team].push(pData);
delete teamList[team].Team;
}
return escapeText(JSON.stringify(teamList), false);
}
function stringifiedTeamListToPlayerData(stringifiedTeamList)
{
let teamList = JSON.parse(unescapeText(stringifiedTeamList));
let playerData = [];
for (let team in teamList)
for (let pData of teamList[team])
{
pData.Team = team;
playerData.push(pData);
}
return playerData;
}
function translateMapTitle(mapTitle)
@ -226,33 +270,66 @@ function formatPlayerInfo(playerDataArray, playerStates)
for (let playerData of playerDataArray)
{
if (playerData == null || playerData.Civ == "gaia")
if (playerData == null || playerData.Civ && playerData.Civ == "gaia")
continue;
++playerIdx;
let teamIdx = playerData.Team;
let isAI = playerData.AI && playerData.AI != "";
let playerState = playerStates && playerStates[playerIdx];
let playerState = playerStates && playerStates[playerIdx] || playerData.State;
let isActive = !playerState || playerState == "active";
let playerDescription;
if (isAI)
{
if (isActive)
// Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu
playerDescription = translate("%(playerName)s (%(civ)s, %(AIdifficulty)s %(AIname)s)");
if (playerData.Civ)
{
if (isActive)
// Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu
playerDescription = translate("%(playerName)s (%(civ)s, %(AIdifficulty)s %(AIname)s)");
else
// Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu
playerDescription = translate("%(playerName)s (%(civ)s, %(AIdifficulty)s %(AIname)s, %(state)s)");
}
else
// Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu
playerDescription = translate("%(playerName)s (%(civ)s, %(AIdifficulty)s %(AIname)s, %(state)s)");
{
if (isActive)
// Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu
playerDescription = translate("%(playerName)s (%(AIdifficulty)s %(AIname)s)");
else
// Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu
playerDescription = translate("%(playerName)s (%(AIdifficulty)s %(AIname)s, %(state)s)");
}
}
else
{
if (isActive)
// Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu
playerDescription = translate("%(playerName)s (%(civ)s)");
if (playerData.Offline)
{
// Can only occur in the lobby for now, so no strings with civ needed
if (isActive)
// Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu
playerDescription = translate("%(playerName)s (OFFLINE)");
else
// Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu
playerDescription = translate("%(playerName)s (OFFLINE, %(state)s)");
}
else
// Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu
playerDescription = translate("%(playerName)s (%(civ)s, %(state)s)");
{
if (playerData.Civ)
if (isActive)
// Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu
playerDescription = translate("%(playerName)s (%(civ)s)");
else
// Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu
playerDescription = translate("%(playerName)s (%(civ)s, %(state)s)");
else
if (isActive)
// Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu
playerDescription = translate("%(playerName)s");
else
// Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu
playerDescription = translate("%(playerName)s (%(state)s)");
}
}
// Sort player descriptions by team
@ -262,7 +339,9 @@ function formatPlayerInfo(playerDataArray, playerStates)
playerDescriptions[teamIdx].push(sprintf(playerDescription, {
"playerName":
'[color="' +
rgbToGuiColor(playerData.Color || g_Settings.PlayerDefaults[playerIdx].Color) +
(typeof getPlayerColor == 'function' ?
(isAI ? "white" : getPlayerColor(playerData.Name)) :
rgbToGuiColor(playerData.Color || g_Settings.PlayerDefaults[playerIdx].Color)) +
'"]' + escapeText(playerData.Name) + "[/color]",
"civ":
@ -283,22 +362,35 @@ function formatPlayerInfo(playerDataArray, playerStates)
}
let teams = Object.keys(playerDescriptions);
if (teams.indexOf("observer") > -1)
teams.splice(teams.indexOf("observer"), 1);
let teamDescription = [];
// If there are no teams, merge all playersDescriptions
if (teams.length == 1)
return playerDescriptions[teams[0]].join("\n") + "\n";
teamDescription.push(playerDescriptions[teams[0]].join("\n"));
// If there are teams, merge "Team N:" + playerDescriptions
return teams.map(team => {
else
teamDescription = teams.map(team => {
let teamCaption = team == -1 ?
translate("No Team") :
sprintf(translate("Team %(team)s"), { "team": +team + 1 });
let teamCaption = team == -1 ?
translate("No Team") :
sprintf(translate("Team %(team)s"), { "team": +team + 1 });
// Translation: Describe players of one team in a selected game, f.e. in the replay- or savegame menu or lobby
return sprintf(translate("%(team)s:\n%(playerDescriptions)s"), {
"team": '[font="sans-bold-14"]' + teamCaption + "[/font]",
"playerDescriptions": playerDescriptions[team].join("\n")
// Translation: Describe players of one team in a selected game, f.e. in the replay- or savegame menu or lobby
return sprintf(translate("%(team)s:\n%(playerDescriptions)s"), {
"team": '[font="sans-bold-14"]' + teamCaption + "[/font]",
"playerDescriptions": playerDescriptions[team].join("\n")
});
});
}).join("\n\n");
if (playerDescriptions.observer)
teamDescription.push(sprintf(translate("%(team)s:\n%(playerDescriptions)s"), {
"team": '[font="sans-bold-14"]' + translatePlural("Observer", "Observers", playerDescriptions.observer.length) + "[/font]",
"playerDescriptions": playerDescriptions.observer.join("\n")
}));
return teamDescription.join("\n\n");
}

View file

@ -676,10 +676,11 @@ function handleReadyMessage(message)
*/
function handleGamestartMessage(message)
{
// Immediately inform the lobby server instead of waiting for the load to finish
if (g_IsController && Engine.HasXmppClient())
{
let playerNames = Object.keys(g_PlayerAssignments).map(guid => g_PlayerAssignments[guid].name);
Engine.SendChangeStateGame(playerNames.length, playerNames.join(", "));
let clients = formatClientsForStanza();
Engine.SendChangeStateGame(clients.connectedPlayers, clients.list);
}
Engine.SwitchGuiPage("page_loading.xml", {
@ -1498,7 +1499,7 @@ function updateMapDescription()
translate(g_GameAttributes.settings.Description) :
translate("Sorry, no description available.");
let victoryIdx = g_VictoryConditions.Name.indexOf(g_GameAttributes.settings.GameType);
let victoryIdx = g_VictoryConditions.Name.indexOf(g_GameAttributes.settings.GameType || g_VictoryConditions.Default);
let victoryTitle;
if (victoryIdx == -1)
@ -1935,6 +1936,34 @@ function resetReadyData()
setReady(false, false);
}
/**
* Send a list of playernames and distinct between players and observers.
* Don't send teams, AIs or anything else until the game was started.
* The playerData format from g_GameAttributes is kept to reuse the GUI function presenting the data.
*/
function formatClientsForStanza()
{
let connectedPlayers = 0;
let playerData = [];
for (let guid in g_PlayerAssignments)
{
let pData = { "Name": g_PlayerAssignments[guid].name };
if (g_GameAttributes.settings.PlayerData[g_PlayerAssignments[guid].player - 1])
++connectedPlayers;
else
pData.Team = "observer";
playerData.push(pData);
}
return {
"list": playerDataToStringifiedTeamList(playerData),
"connectedPlayers": connectedPlayers
};
}
/**
* Send the relevant gamesettings to the lobbybot.
*/
@ -1948,7 +1977,7 @@ function sendRegisterGameStanza()
let mapSize = g_GameAttributes.mapType == "random" ? Engine.GetGUIObjectByName("mapSize").list_data[selectedMapSize] : "Default";
let victoryCondition = Engine.GetGUIObjectByName("victoryCondition").list[selectedVictoryCondition];
let playerNames = Object.keys(g_PlayerAssignments).map(guid => g_PlayerAssignments[guid].name).sort();
let clients = formatClientsForStanza();
let stanza = {
"name": g_ServerName,
@ -1958,9 +1987,9 @@ function sendRegisterGameStanza()
"mapSize": mapSize,
"mapType": g_GameAttributes.mapType,
"victoryCondition": victoryCondition,
"nbp": Object.keys(g_PlayerAssignments).length || 1,
"tnbp": g_GameAttributes.settings.PlayerData.length,
"players": playerNames.join(", ")
"nbp": clients.connectedPlayers,
"maxnbp": g_GameAttributes.settings.PlayerData.length,
"players": clients.list,
};
// Only send the stanza if the relevant settings actually changed

View file

@ -285,14 +285,14 @@ function filterGame(game)
return true;
if (playersNumberFilter.selected != 0 &&
game.tnbp != playersNumberFilter.list_data[playersNumberFilter.selected])
game.maxnbp != playersNumberFilter.list_data[playersNumberFilter.selected])
return true;
if (mapTypeFilter.selected != 0 &&
game.mapType != mapTypeFilter.list_data[mapTypeFilter.selected])
return true;
if (!showFullFilter.checked && game.tnbp <= game.nbp)
if (!showFullFilter.checked && game.maxnbp <= game.nbp)
return true;
return false;
@ -546,8 +546,8 @@ function updateGameList()
break;
case 'nPlayers':
// Compare playercount ratio
sortA = a.nbp * b.tnbp;
sortB = b.nbp * a.tnbp;
sortA = a.nbp * b.maxnbp;
sortB = b.nbp * a.maxnbp;
break;
case 'status':
default:
@ -582,7 +582,7 @@ function updateGameList()
list_mapName.push(translateMapTitle(game.niceMapName));
list_mapSize.push(translateMapSize(game.mapSize));
list_mapType.push(g_MapTypes.Title[mapTypeIdx] || "");
list_nPlayers.push(game.nbp + "/" + game.tnbp);
list_nPlayers.push(game.nbp + "/" + game.maxnbp);
list.push(gameName);
list_data.push(i);
}
@ -618,10 +618,10 @@ function updateGameSelection()
Engine.GetGUIObjectByName("sgNbPlayers").caption = sprintf(
translate("Players: %(current)s/%(total)s"), {
"current": game.nbp,
"total": game.tnbp
"total": game.maxnbp
});
Engine.GetGUIObjectByName("sgPlayersNames").caption = game.players;
Engine.GetGUIObjectByName("sgPlayersNames").caption = formatPlayerInfo(stringifiedTeamListToPlayerData(game.players));
Engine.GetGUIObjectByName("sgMapSize").caption = translateMapSize(game.mapSize);
let mapTypeIdx = g_MapTypes.Name.indexOf(game.mapType);
@ -653,7 +653,7 @@ function joinButton()
let username = g_UserRating ? g_Username + " (" + g_UserRating + ")" : g_Username;
if (game.state == "init" || game.players.split(", ").indexOf(username) > -1)
if (game.state == "init" || stringifiedTeamListToPlayerData(game.players).some(player => player.Name == username))
joinSelectedGame();
else
messageBox(

View file

@ -250,6 +250,7 @@ var g_NotificationsTypes =
"resign": !!notification.resign
});
playerFinished(player, false);
sendLobbyPlayerlistUpdate();
},
"won": function(notification, player)
{
@ -259,6 +260,7 @@ var g_NotificationsTypes =
"player": player
});
playerFinished(player, true);
sendLobbyPlayerlistUpdate();
},
"diplomacy": function(notification, player)
{
@ -541,13 +543,7 @@ function handlePlayerAssignmentsMessage(message)
});
updateChatAddressees();
// Update lobby gamestatus
if (g_IsController && Engine.HasXmppClient())
{
let players = Object.keys(g_PlayerAssignments).map(guid => g_PlayerAssignments[guid].name);
Engine.SendChangeStateGame(Object.keys(g_PlayerAssignments).length, players.join(", "));
}
sendLobbyPlayerlistUpdate();
}
function onClientJoin(guid)

View file

@ -15,7 +15,7 @@ const g_Ambient = [ "audio/ambient/dayscape/day_temperate_gen_03.ogg" ];
/**
* Map, player and match settings set in gamesetup.
*/
var g_GameAttributes;
const g_GameAttributes = Object.freeze(Engine.GetInitAttributes());
/**
* Is this user in control of game settings (i.e. is a network server, or offline player).
@ -225,8 +225,6 @@ function init(initData, hotloadData)
return;
}
g_GameAttributes = Engine.GetInitAttributes();
if (initData)
{
g_IsNetworked = initData.isNetworked;
@ -289,8 +287,8 @@ function init(initData, hotloadData)
if (hotloadData)
g_Selection.selected = hotloadData.selection;
sendLobbyPlayerlistUpdate();
onSimulationUpdate();
setTimeout(displayGamestateNotifications, 1000);
// Report the performance after 5 seconds (when we're still near

View file

@ -131,3 +131,61 @@ function resourcesToAlphaMask(neededResources)
alpha = alpha > 125 ? 125 : alpha;
return "color:255 0 0 " + alpha;
}
/**
* Send the current list of players, teams, AIs, observers and defeated/won and offline states to the lobby.
* The playerData format from g_GameAttributes is kept to reuse the GUI function presenting the data.
*/
function sendLobbyPlayerlistUpdate()
{
if (!g_IsController || !Engine.HasXmppClient())
return;
// Extract the relevant player data and minimize packet load
let minPlayerData = [];
for (let playerID in g_GameAttributes.settings.PlayerData)
{
if (+playerID == 0)
continue;
let pData = g_GameAttributes.settings.PlayerData[playerID];
let minPData = { "Name": pData.Name };
if (g_GameAttributes.settings.LockTeams)
minPData.Team = pData.Team;
if (pData.AI)
{
minPData.AI = pData.AI;
minPData.AIDiff = pData.AIDiff;
}
if (g_Players[playerID].offline)
minPData.Offline = true;
// Whether the player has won or was defeated
let state = g_Players[playerID].state;
if (state != "active")
minPData.State = state;
minPlayerData.push(minPData);
}
// Add observers
let connectedPlayers = 0;
for (let guid in g_PlayerAssignments)
{
let pData = g_GameAttributes.settings.PlayerData[g_PlayerAssignments[guid].player];
if (pData)
++connectedPlayers;
else
minPlayerData.push({
"Name": g_PlayerAssignments[guid].name,
"Team": "observer"
});
}
Engine.SendChangeStateGame(connectedPlayers, playerDataToStringifiedTeamList(minPlayerData));
}

View file

@ -502,7 +502,7 @@ void XmppClient::GUIGetGameList(ScriptInterface& scriptInterface, JS::MutableHan
JSAutoRequest rq(cx);
scriptInterface.Eval("([])", ret);
const char* stats[] = { "name", "ip", "port", "state", "nbp", "tnbp", "players", "mapName", "niceMapName", "mapSize", "mapType", "victoryCondition" };
const char* stats[] = { "name", "ip", "port", "state", "nbp", "maxnbp", "players", "mapName", "niceMapName", "mapSize", "mapType", "victoryCondition" };
for(const glooxwrapper::Tag* const& t : m_GameList)
{
JS::RootedValue game(cx);

View file

@ -301,12 +301,12 @@ class GameList():
if JID in self.gameList:
if self.gameList[JID]['nbp-init'] > data['nbp']:
logging.debug("change game (%s) state from %s to %s", JID, self.gameList[JID]['state'], 'waiting')
self.gameList[JID]['nbp'] = data['nbp']
self.gameList[JID]['state'] = 'waiting'
else:
logging.debug("change game (%s) state from %s to %s", JID, self.gameList[JID]['state'], 'running')
self.gameList[JID]['nbp'] = data['nbp']
self.gameList[JID]['state'] = 'running'
self.gameList[JID]['nbp'] = data['nbp']
self.gameList[JID]['players'] = data['players']
## Class which manages different game reports from clients ##
## and calls leaderboard functions as appropriate. ##