Index: ps/trunk/binaries/data/mods/public/gui/session/chat/Chat.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/chat/Chat.js +++ ps/trunk/binaries/data/mods/public/gui/session/chat/Chat.js @@ -35,8 +35,10 @@ this.ChatHistory.displayChatHistory(); }); - registerPlayersFinishedHandler(this.onUpdatePlayers.bind(this)); - playerViewControl.registerViewedPlayerChangeHandler(this.onUpdatePlayers.bind(this)); + let updater = this.onUpdatePlayers.bind(this); + registerPlayersFinishedHandler(updater); + registerPlayerAssignmentsChangeHandler(updater); + playerViewControl.registerViewedPlayerChangeHandler(updater); Engine.SetGlobalHotkey("chat", this.openPage.bind(this)); Engine.SetGlobalHotkey("privatechat", this.openPage.bind(this)); Index: ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyGamelistReporter.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyGamelistReporter.js +++ ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyGamelistReporter.js @@ -0,0 +1,90 @@ +/** + * Send the current list of players, teams, AIs, observers and defeated/won and offline states to the lobby. + * This report excludes the matchsettings, since they do not change during the match. + * + * The playerData format from g_GameAttributes is kept to reuse the GUI function presenting the data, + * but the payload size is minimized by only extracting properties relevant for display. + */ +class LobbyGamelistReporter +{ + constructor() + { + if (!LobbyGamelistReporter.Available()) + throw new Error("Lobby gamelist service not available"); + + let updater = this.sendGamelistUpdate.bind(this); + registerPlayersInitHandler(updater); + registerPlayersFinishedHandler(updater); + registerPlayerAssignmentsChangeHandler(updater); + } + + sendGamelistUpdate() + { + Engine.SendChangeStateGame( + this.countConnectedPlayers(), + playerDataToStringifiedTeamList([...this.getPlayers(), ...this.getObservers()])); + } + + getPlayers() + { + let players = []; + + // Skip gaia + for (let playerID = 1; playerID < g_GameAttributes.settings.PlayerData.length; ++playerID) + { + let pData = g_GameAttributes.settings.PlayerData[playerID]; + + let player = { + "Name": pData.Name, + "Civ": pData.Civ + }; + + if (g_GameAttributes.settings.LockTeams) + player.Team = pData.Team; + + if (pData.AI) + { + player.AI = pData.AI; + player.AIDiff = pData.AIDiff; + player.AIBehavior = pData.AIBehavior; + } + + if (g_Players[playerID].offline) + player.Offline = true; + + // Whether the player has won or was defeated + let state = g_Players[playerID].state; + if (state != "active") + player.State = state; + + players.push(player); + } + return players; + } + + getObservers() + { + let observers = []; + for (let guid in g_PlayerAssignments) + if (g_PlayerAssignments[guid].player == -1) + observers.push({ + "Name": g_PlayerAssignments[guid].name, + "Team": "observer" + }); + return observers; + } + + countConnectedPlayers() + { + let connectedPlayers = 0; + for (let guid in g_PlayerAssignments) + if (g_PlayerAssignments[guid].player != -1) + ++connectedPlayers; + return connectedPlayers; + } +} + +LobbyGamelistReporter.Available = function() +{ + return Engine.HasXmppClient() && g_IsController; +}; Index: ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Buildings.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Buildings.js +++ ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Buildings.js @@ -0,0 +1,23 @@ +/** + * This class reports the buildings built, lost and destroyed of some selected structure classes. + */ +LobbyRatingReport.prototype.Buildings = class +{ + insertValues(report, playerStates) + { + let lower = txt => txt.substr(0, 1).toLowerCase() + txt.substr(1); + let time = playerStates[0].sequences.time.length - 1; + + for (let buildingClass in playerStates[0].sequences.buildingsConstructed) + report[lower(buildingClass) + "BuildingsConstructed"] = playerStates.map(playerState => + playerState.sequences.buildingsConstructed[buildingClass][time]).join(",") + ","; + + for (let buildingClass in playerStates[0].sequences.buildingsLost) + report[lower(buildingClass) + "BuildingsLost"] = playerStates.map(playerState => + playerState.sequences.buildingsLost[buildingClass][time]).join(",") + ","; + + for (let buildingClass in playerStates[0].sequences.enemyBuildingsDestroyed) + report["enemy" + buildingClass + "BuildingsDestroyed"] = playerStates.map(playerState => + playerState.sequences.enemyBuildingsDestroyed[buildingClass][time]).join(",") + ","; + } +}; Index: ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Misc.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Misc.js +++ ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Misc.js @@ -0,0 +1,21 @@ +/** + * This class counts trade, tributes, loot and map exploration. + */ +LobbyRatingReport.prototype.Misc = class +{ + insertValues(report, playerStates) + { + for (let category of this.MiscCategories) + report[category] = playerStates.map(playerState => + playerState.sequences[category][playerState.sequences.time.length - 1]).join(",") + ","; + } +}; + +LobbyRatingReport.prototype.Misc.prototype.MiscCategories = [ + "tradeIncome", + "tributesSent", + "tributesReceived", + "treasuresCollected", + "lootCollected", + "percentMapExplored" +]; Index: ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Players.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Players.js +++ ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Players.js @@ -0,0 +1,15 @@ +/** + * This class reports the chosen settings and victory state of the participating players. + */ +LobbyRatingReport.prototype.Players = class +{ + insertValues(report, playerStates) + { + Object.assign(report, { + "playerStates": playerStates.map(playerState => playerState.state).join(",") + ",", + "civs": playerStates.map(playerState => playerState.civ).join(",") + ",", + "teams": playerStates.map(playerState => playerState.team).join(",") + ",", + "teamsLocked": String(playerStates.every(playerState => playerState.teamsLocked)) + }); + } +}; Index: ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Resources.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Resources.js +++ ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Resources.js @@ -0,0 +1,25 @@ +/** + * This class reports the amount of resources that each player has obtained or used. + */ +LobbyRatingReport.prototype.Resources = class +{ + insertValues(report, playerStates) + { + let time = playerStates[0].sequences.time.length - 1; + + for (let action of this.Actions) + for (let resCode of g_ResourceData.GetCodes()) + report[resCode + action] = playerStates.map(playerState => + playerState.sequences["resources" + action][resCode][time]).join(",") + ","; + + report.vegetarianFoodGathered = playerStates.map( + playerState => playerState.sequences.resourcesGathered.vegetarianFood[time]).join(",") + ","; + } +}; + +LobbyRatingReport.prototype.Resources.prototype.Actions = [ + "Gathered", + "Used", + "Sold", + "Bought" +]; Index: ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Score.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Score.js +++ ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Score.js @@ -0,0 +1,57 @@ +/** + * This class computes the economic and military score points of each player. + */ +LobbyRatingReport.prototype.Score = class +{ + insertValues(report, playerStates) + { + Object.assign(report, { + "economyScore": playerStates.map(this.economyScore.bind(this)).join(",") + ",", + "militaryScore": playerStates.map(this.militaryScore.bind(this)).join(",") + ",", + "totalScore": playerStates.map(this.totalScore.bind(this)).join(",") + ",", + }); + } + + /** + * Keep this in sync with summary screen score computation! + */ + economyScore(playerState) + { + let total = 0; + let time = playerState.sequences.time.length - 1; + + // Notice that this avoids the vegetarianFood property of resourcesGathered + for (let resCode of g_ResourceData.GetCodes()) + total += playerState.sequences.resourcesGathered[resCode][time]; + + total += playerState.sequences.tradeIncome[time]; + + return Math.round(total / 10); + } + + militaryScore(playerState) + { + let time = playerState.sequences.time.length - 1; + + let totalDestruction = + playerState.sequences.enemyUnitsKilledValue[time] + + playerState.sequences.enemyBuildingsDestroyedValue[time] + + playerState.sequences.unitsCapturedValue[time] + + playerState.sequences.buildingsCapturedValue[time]; + + return Math.round(totalDestruction / 10); + } + + explorationScore(playerState) + { + let time = playerState.sequences.time.length - 1; + return playerState.sequences.percentMapExplored[time] * 10; + } + + totalScore(playerState) + { + return this.economyScore(playerState) + + this.militaryScore(playerState) + + this.explorationScore(playerState); + } +}; Index: ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Units.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Units.js +++ ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Units.js @@ -0,0 +1,23 @@ +/** + * This class reports the units trained, lost and killed of some selected unit classes. + */ +LobbyRatingReport.prototype.Units = class +{ + insertValues(report, playerStates) + { + let lower = txt => txt.substr(0, 1).toLowerCase() + txt.substr(1); + let time = playerStates[0].sequences.time.length - 1; + + for (let unitClass in playerStates[0].sequences.unitsTrained) + report[lower(unitClass) + "UnitsTrained"] = playerStates.map(playerState => + playerState.sequences.unitsTrained[unitClass][time]).join(",") + ","; + + for (let unitClass in playerStates[0].sequences.unitsLost) + report[lower(unitClass) + "UnitsLost"] = playerStates.map(playerState => + playerState.sequences.unitsLost[unitClass][time]).join(",") + ","; + + for (let unitClass in playerStates[0].sequences.enemyUnitsKilled) + report["enemy" + unitClass + "UnitsKilled"] = playerStates.map(playerState => + playerState.sequences.enemyUnitsKilled[unitClass][time]).join(",") + ","; + } +}; Index: ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReporter.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReporter.js +++ ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReporter.js @@ -0,0 +1,53 @@ +/** + * This is a container for classes that extend the report object. + * Keep in sync with the lobby bot code, the StatisticsTracker. + */ +class LobbyRatingReport +{ +} + +/** + * This class reports the state of the current game to the lobby bot when the current player has been defeated or won. + */ +class LobbyRatingReporter +{ + constructor() + { + if (!LobbyRatingReporter.Available()) + throw new Error("Lobby rating service is not available"); + + registerPlayersFinishedHandler(this.onPlayersFinished.bind(this)); + } + + onPlayersFinished(players) + { + // Observers don't send the state, players send it only once per match + if (players.indexOf(Engine.GetPlayerID()) != -1) + return; + + let extendedSimState = Engine.GuiInterfaceCall("GetExtendedSimulationState"); + + let report = { + "playerID": Engine.GetPlayerID(), + "matchID": g_GameAttributes.matchID, + "mapName": g_GameAttributes.settings.Name, + "timeElapsed": extendedSimState.timeElapsed, + }; + + // Remove gaia + let playerStates = clone(extendedSimState.players).slice(1); + + for (let name in LobbyRatingReport.prototype) + new LobbyRatingReport.prototype[name]().insertValues(report, playerStates); + + Engine.SendGameReport(report); + } +} + +/** + * Only 1v1 games are rated, account for gaia. + */ +LobbyRatingReporter.Available = function() +{ + return Engine.HasXmppClient() && Engine.IsRankedGame() && g_GameAttributes.settings.PlayerData.length == 3; +}; Index: ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyService.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyService.js +++ ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyService.js @@ -0,0 +1,18 @@ +/** + * @file The lobby scripting code is kept separate from the rest of the session to + * ease distribution of the game without any lobby code. + */ + +/** + * The host sends a gamelist update everytime a client joins or leaves the match. + */ +var g_LobbyGamelistReporter = + LobbyGamelistReporter.Available() && + new LobbyGamelistReporter(); + +/** + * The participants of a rated 1v1 match send a rating report when the winner was decided. + */ +var g_LobbyRatingReporter = + LobbyRatingReporter.Available() && + new LobbyRatingReporter(); Index: ps/trunk/binaries/data/mods/public/gui/session/messages.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/messages.js +++ ps/trunk/binaries/data/mods/public/gui/session/messages.js @@ -14,6 +14,11 @@ var g_TutorialNewMessageTags = { "color": "yellow" }; /** + * These handlers are called everytime a client joins or disconnects. + */ +var g_PlayerAssignmentsChangeHandlers = new Set(); + +/** * Handle all netmessage types that can occur. */ var g_NetMessageTypes = { @@ -281,6 +286,11 @@ } }; +function registerPlayerAssignmentsChangeHandler(handler) +{ + g_PlayerAssignmentsChangeHandlers.add(handler); +} + /** * Loads all known cheat commands. */ @@ -561,9 +571,11 @@ onClientJoin(guid); }); + for (let handler of g_PlayerAssignmentsChangeHandlers) + handler(); + + // TODO: use subscription instead updateGUIObjects(); - g_Chat.onUpdatePlayers(); - sendLobbyPlayerlistUpdate(); } function onClientJoin(guid) Index: ps/trunk/binaries/data/mods/public/gui/session/session.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/session.js +++ ps/trunk/binaries/data/mods/public/gui/session/session.js @@ -154,7 +154,7 @@ * These events are fired when the user has performed a hotkey assignment change. * Currently only fired on init, but to be fired from any hotkey editor dialog. */ -var g_HotkeyChangeHandlers = []; +var g_HotkeyChangeHandlers = new Set(); /** * Top coordinate of the research list. @@ -312,8 +312,6 @@ g_Players = hotloadData.player; } - sendLobbyPlayerlistUpdate(); - // TODO: use event instead onSimulationUpdate(); @@ -352,7 +350,7 @@ function registerHotkeyChangeHandler(handler) { - g_HotkeyChangeHandlers.push(handler); + g_HotkeyChangeHandlers.add(handler); } function updatePlayerData() @@ -505,11 +503,6 @@ "players": players }); - if (players.indexOf(Engine.GetPlayerID()) != -1) - reportGame(); - - sendLobbyPlayerlistUpdate(); - updatePlayerData(); // TODO: The other calls in this function should move too @@ -1161,259 +1154,3 @@ g_ResearchListTop = 4 + 14 * counters.length; } - -/** - * 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, "Civ": pData.Civ }; - - if (g_GameAttributes.settings.LockTeams) - minPData.Team = pData.Team; - - if (pData.AI) - { - minPData.AI = pData.AI; - minPData.AIDiff = pData.AIDiff; - minPData.AIBehavior = pData.AIBehavior; - } - - 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)); -} - -/** - * Send a report on the gamestatus to the lobby. - * Keep in sync with source/tools/XpartaMuPP/LobbyRanking.py - */ -function reportGame() -{ - // Only 1v1 games are rated (and Gaia is part of g_Players) - if (!Engine.HasXmppClient() || !Engine.IsRankedGame() || - g_Players.length != 3 || Engine.GetPlayerID() == -1) - return; - - let extendedSimState = Engine.GuiInterfaceCall("GetExtendedSimulationState"); - - let unitsClasses = [ - "total", - "Infantry", - "Worker", - "FemaleCitizen", - "Cavalry", - "Champion", - "Hero", - "Siege", - "Ship", - "Trader" - ]; - - let unitsCountersTypes = [ - "unitsTrained", - "unitsLost", - "enemyUnitsKilled" - ]; - - let buildingsClasses = [ - "total", - "CivCentre", - "House", - "Economic", - "Outpost", - "Military", - "Fortress", - "Wonder" - ]; - - let buildingsCountersTypes = [ - "buildingsConstructed", - "buildingsLost", - "enemyBuildingsDestroyed" - ]; - - let resourcesTypes = [ - "wood", - "food", - "stone", - "metal" - ]; - - let resourcesCounterTypes = [ - "resourcesGathered", - "resourcesUsed", - "resourcesSold", - "resourcesBought" - ]; - - let misc = [ - "tradeIncome", - "tributesSent", - "tributesReceived", - "treasuresCollected", - "lootCollected", - "percentMapExplored" - ]; - - let playerStatistics = {}; - - // Unit Stats - for (let unitCounterType of unitsCountersTypes) - { - if (!playerStatistics[unitCounterType]) - playerStatistics[unitCounterType] = { }; - for (let unitsClass of unitsClasses) - playerStatistics[unitCounterType][unitsClass] = ""; - } - - playerStatistics.unitsLostValue = ""; - playerStatistics.unitsKilledValue = ""; - // Building stats - for (let buildingCounterType of buildingsCountersTypes) - { - if (!playerStatistics[buildingCounterType]) - playerStatistics[buildingCounterType] = { }; - for (let buildingsClass of buildingsClasses) - playerStatistics[buildingCounterType][buildingsClass] = ""; - } - - playerStatistics.buildingsLostValue = ""; - playerStatistics.enemyBuildingsDestroyedValue = ""; - // Resources - for (let resourcesCounterType of resourcesCounterTypes) - { - if (!playerStatistics[resourcesCounterType]) - playerStatistics[resourcesCounterType] = { }; - for (let resourcesType of resourcesTypes) - playerStatistics[resourcesCounterType][resourcesType] = ""; - } - playerStatistics.resourcesGathered.vegetarianFood = ""; - - for (let type of misc) - playerStatistics[type] = ""; - - // Total - playerStatistics.economyScore = ""; - playerStatistics.militaryScore = ""; - playerStatistics.totalScore = ""; - - let mapName = g_GameAttributes.settings.Name; - let playerStates = ""; - let playerCivs = ""; - let teams = ""; - let teamsLocked = true; - - // Serialize the statistics for each player into a comma-separated list. - // Ignore gaia - for (let i = 1; i < extendedSimState.players.length; ++i) - { - let player = extendedSimState.players[i]; - let maxIndex = player.sequences.time.length - 1; - - playerStates += player.state + ","; - playerCivs += player.civ + ","; - teams += player.team + ","; - teamsLocked = teamsLocked && player.teamsLocked; - for (let resourcesCounterType of resourcesCounterTypes) - for (let resourcesType of resourcesTypes) - playerStatistics[resourcesCounterType][resourcesType] += player.sequences[resourcesCounterType][resourcesType][maxIndex] + ","; - playerStatistics.resourcesGathered.vegetarianFood += player.sequences.resourcesGathered.vegetarianFood[maxIndex] + ","; - - for (let unitCounterType of unitsCountersTypes) - for (let unitsClass of unitsClasses) - playerStatistics[unitCounterType][unitsClass] += player.sequences[unitCounterType][unitsClass][maxIndex] + ","; - - for (let buildingCounterType of buildingsCountersTypes) - for (let buildingsClass of buildingsClasses) - playerStatistics[buildingCounterType][buildingsClass] += player.sequences[buildingCounterType][buildingsClass][maxIndex] + ","; - let total = 0; - for (let type in player.sequences.resourcesGathered) - total += player.sequences.resourcesGathered[type][maxIndex]; - - playerStatistics.economyScore += total + ","; - playerStatistics.militaryScore += Math.round((player.sequences.enemyUnitsKilledValue[maxIndex] + - player.sequences.enemyBuildingsDestroyedValue[maxIndex]) / 10) + ","; - playerStatistics.totalScore += (total + Math.round((player.sequences.enemyUnitsKilledValue[maxIndex] + - player.sequences.enemyBuildingsDestroyedValue[maxIndex]) / 10)) + ","; - - for (let type of misc) - playerStatistics[type] += player.sequences[type][maxIndex] + ","; - } - - // Send the report with serialized data - let reportObject = {}; - reportObject.timeElapsed = extendedSimState.timeElapsed; - reportObject.playerStates = playerStates; - reportObject.playerID = Engine.GetPlayerID(); - reportObject.matchID = g_GameAttributes.matchID; - reportObject.civs = playerCivs; - reportObject.teams = teams; - reportObject.teamsLocked = String(teamsLocked); - reportObject.ceasefireActive = String(extendedSimState.ceasefireActive); - reportObject.ceasefireTimeRemaining = String(extendedSimState.ceasefireTimeRemaining); - reportObject.mapName = mapName; - reportObject.economyScore = playerStatistics.economyScore; - reportObject.militaryScore = playerStatistics.militaryScore; - reportObject.totalScore = playerStatistics.totalScore; - for (let rct of resourcesCounterTypes) - for (let rt of resourcesTypes) - reportObject[rt + rct.substr(9)] = playerStatistics[rct][rt]; - // eg. rt = food rct.substr = Gathered rct = resourcesGathered - - reportObject.vegetarianFoodGathered = playerStatistics.resourcesGathered.vegetarianFood; - for (let type of unitsClasses) - { - // eg. type = Infantry (type.substr(0,1)).toLowerCase()+type.substr(1) = infantry - reportObject[(type.substr(0, 1)).toLowerCase() + type.substr(1) + "UnitsTrained"] = playerStatistics.unitsTrained[type]; - reportObject[(type.substr(0, 1)).toLowerCase() + type.substr(1) + "UnitsLost"] = playerStatistics.unitsLost[type]; - reportObject["enemy" + type + "UnitsKilled"] = playerStatistics.enemyUnitsKilled[type]; - } - for (let type of buildingsClasses) - { - reportObject[(type.substr(0, 1)).toLowerCase() + type.substr(1) + "BuildingsConstructed"] = playerStatistics.buildingsConstructed[type]; - reportObject[(type.substr(0, 1)).toLowerCase() + type.substr(1) + "BuildingsLost"] = playerStatistics.buildingsLost[type]; - reportObject["enemy" + type + "BuildingsDestroyed"] = playerStatistics.enemyBuildingsDestroyed[type]; - } - for (let type of misc) - reportObject[type] = playerStatistics[type]; - - Engine.SendGameReport(reportObject); -} Index: ps/trunk/binaries/data/mods/public/gui/session/session.xml =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/session.xml +++ ps/trunk/binaries/data/mods/public/gui/session/session.xml @@ -8,6 +8,8 @@