mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-06-16 05:13:58 -07:00
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:
parent
cf04bad4bc
commit
d7d0a7f869
8 changed files with 227 additions and 54 deletions
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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. ##
|
||||
|
|
|
|||
Loading…
Reference in a new issue