diff --git a/binaries/data/mods/public/gui/common/functions_utility.js b/binaries/data/mods/public/gui/common/functions_utility.js index 2da15e1cae..83990e264a 100644 --- a/binaries/data/mods/public/gui/common/functions_utility.js +++ b/binaries/data/mods/public/gui/common/functions_utility.js @@ -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"); } diff --git a/binaries/data/mods/public/gui/gamesetup/gamesetup.js b/binaries/data/mods/public/gui/gamesetup/gamesetup.js index 6cc00a4526..3b160211ad 100644 --- a/binaries/data/mods/public/gui/gamesetup/gamesetup.js +++ b/binaries/data/mods/public/gui/gamesetup/gamesetup.js @@ -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 diff --git a/binaries/data/mods/public/gui/lobby/lobby.js b/binaries/data/mods/public/gui/lobby/lobby.js index f1e18bb735..120155e11b 100644 --- a/binaries/data/mods/public/gui/lobby/lobby.js +++ b/binaries/data/mods/public/gui/lobby/lobby.js @@ -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( diff --git a/binaries/data/mods/public/gui/session/messages.js b/binaries/data/mods/public/gui/session/messages.js index 5392657cbf..13f2838d76 100644 --- a/binaries/data/mods/public/gui/session/messages.js +++ b/binaries/data/mods/public/gui/session/messages.js @@ -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) diff --git a/binaries/data/mods/public/gui/session/session.js b/binaries/data/mods/public/gui/session/session.js index 245ff19335..d52dc38deb 100644 --- a/binaries/data/mods/public/gui/session/session.js +++ b/binaries/data/mods/public/gui/session/session.js @@ -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 diff --git a/binaries/data/mods/public/gui/session/utility_functions.js b/binaries/data/mods/public/gui/session/utility_functions.js index e47dac4a1f..36a4552d67 100644 --- a/binaries/data/mods/public/gui/session/utility_functions.js +++ b/binaries/data/mods/public/gui/session/utility_functions.js @@ -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)); +} diff --git a/source/lobby/XmppClient.cpp b/source/lobby/XmppClient.cpp index f48881ee75..10a2f28818 100644 --- a/source/lobby/XmppClient.cpp +++ b/source/lobby/XmppClient.cpp @@ -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); diff --git a/source/tools/XpartaMuPP/XpartaMuPP.py b/source/tools/XpartaMuPP/XpartaMuPP.py index f0dfdb9c23..f3e2ff83a7 100644 --- a/source/tools/XpartaMuPP/XpartaMuPP.py +++ b/source/tools/XpartaMuPP/XpartaMuPP.py @@ -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. ##