Index: ps/trunk/binaries/data/mods/public/art/textures/ui/lobby/private.png =================================================================== Cannot display: file marked as a binary type. svn:mime-type = application/octet-stream Index: ps/trunk/binaries/data/mods/public/art/textures/ui/lobby/private.png =================================================================== --- ps/trunk/binaries/data/mods/public/art/textures/ui/lobby/private.png (nonexistent) +++ ps/trunk/binaries/data/mods/public/art/textures/ui/lobby/private.png (revision 24795) Property changes on: ps/trunk/binaries/data/mods/public/art/textures/ui/lobby/private.png ___________________________________________________________________ Added: svn:mime-type ## -0,0 +1 ## +application/octet-stream \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/art/textures/ui/lobby/public.png =================================================================== Cannot display: file marked as a binary type. svn:mime-type = application/octet-stream Index: ps/trunk/binaries/data/mods/public/art/textures/ui/lobby/public.png =================================================================== --- ps/trunk/binaries/data/mods/public/art/textures/ui/lobby/public.png (nonexistent) +++ ps/trunk/binaries/data/mods/public/art/textures/ui/lobby/public.png (revision 24795) Property changes on: ps/trunk/binaries/data/mods/public/art/textures/ui/lobby/public.png ___________________________________________________________________ Added: svn:mime-type ## -0,0 +1 ## +application/octet-stream \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Game.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Game.js (revision 24794) +++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Game.js (revision 24795) @@ -1,338 +1,340 @@ /** * This class represents a multiplayer match hosted by a player in the lobby. * Having this represented as a class allows to leverage significant performance * gains by caching computed, escaped, translated strings and sorting keys. * * Additionally class representation allows implementation of events such as * a new match being hosted, a match having ended, or a buddy having joined a match. * * Ensure that escapeText is applied to player controlled data for display. * * Users of the properties of this class: * GameList, GameDetails, MapFilters, JoinButton, any user of GameList.selectedGame() */ class Game { constructor(mapCache) { this.mapCache = mapCache; // Stanza data, object with exclusively string values // Used to compare which part of the stanza data changed, // perform partial updates and trigger event notifications. this.stanza = {}; for (let name of this.StanzaKeys) this.stanza[name] = ""; // This will be displayed in the GameList and GameDetails // Important: Player input must be processed with escapeText this.displayData = { "tags": {} }; // Cache the values used for sorting this.sortValues = { "state": "", "compatibility": "", "hasBuddyString": "" }; // Array of objects, result of stringifiedTeamListToPlayerData this.players = []; // Whether the current player has the same mods launched as the host of this game this.isCompatible = undefined; // Used to display which mods are missing if the player attempts a join this.mods = undefined; // Used by the rating column and rating filer this.gameRating = undefined; // 'Persistent temporary' sprintf arguments object to avoid repeated object construction this.playerCountArgs = {}; } /** * Called from GameList to ensure call order. */ onBuddyChange() { this.updatePlayers(this.stanza); } /** * This function computes values that will either certainly or * most likely be used later (i.e. by filtering, sorting and gamelist display). * * The performance benefit arises from the fact that for a new gamelist stanza * many if not most games and game properties did not change. */ update(newStanza, sortKey) { let oldStanza = this.stanza; let displayData = this.displayData; let sortValues = this.sortValues; if (oldStanza.name != newStanza.name) { Engine.ProfileStart("gameName"); sortValues.gameName = newStanza.name.toLowerCase(); this.updateGameName(newStanza); Engine.ProfileStop(); } if (oldStanza.state != newStanza.state) { Engine.ProfileStart("gameState"); this.updateGameTags(newStanza); sortValues.state = this.GameStatusOrder.indexOf(newStanza.state); Engine.ProfileStop(); } if (oldStanza.niceMapName != newStanza.niceMapName) { Engine.ProfileStart("niceMapName"); displayData.mapName = escapeText(this.mapCache.translateMapName(newStanza.niceMapName)); displayData.mapDescription = this.mapCache.getTranslatedMapDescription(newStanza.mapType, newStanza.mapName); Engine.ProfileStop(); } if (oldStanza.mapName != newStanza.mapName) { Engine.ProfileStart("mapName"); sortValues.mapName = displayData.mapName; Engine.ProfileStop(); } if (oldStanza.mapType != newStanza.mapType) { Engine.ProfileStart("mapType"); displayData.mapType = g_MapTypes.Title[g_MapTypes.Name.indexOf(newStanza.mapType)] || ""; sortValues.mapType = newStanza.mapType; Engine.ProfileStop(); } if (oldStanza.mapSize != newStanza.mapSize) { Engine.ProfileStart("mapSize"); displayData.mapSize = translateMapSize(newStanza.mapSize); sortValues.mapSize = newStanza.mapSize; Engine.ProfileStop(); } let playersChanged = oldStanza.players != newStanza.players; if (playersChanged) { Engine.ProfileStart("playerData"); this.updatePlayers(newStanza); Engine.ProfileStop(); } if (oldStanza.nbp != newStanza.nbp || oldStanza.maxnbp != newStanza.maxnbp || playersChanged) { Engine.ProfileStart("playerCount"); displayData.playerCount = this.getTranslatedPlayerCount(newStanza); sortValues.maxnbp = newStanza.maxnbp; Engine.ProfileStop(); } if (oldStanza.mods != newStanza.mods) { Engine.ProfileStart("mods"); this.updateMods(newStanza); Engine.ProfileStop(); } + displayData.private = newStanza.hasPassword ? '[icon="icon_private"]' : '[icon="icon_public"]'; + this.stanza = newStanza; this.sortValue = this.sortValues[sortKey]; } updatePlayers(newStanza) { let players; { Engine.ProfileStart("stringifiedTeamListToPlayerData"); players = stringifiedTeamListToPlayerData(newStanza.players); this.players = players; Engine.ProfileStop(); } { Engine.ProfileStart("parsePlayers"); let observerCount = 0; let hasBuddies = 0; let playerRatingTotal = 0; for (let player of players) { let playerNickRating = splitRatingFromNick(player.Name); if (player.Team == "observer") ++observerCount; else playerRatingTotal += playerNickRating.rating || g_DefaultLobbyRating; // Sort games with playing buddies above games with spectating buddies if (hasBuddies < 2 && g_Buddies.indexOf(playerNickRating.nick) != -1) hasBuddies = player.Team == "observer" ? 1 : 2; } this.observerCount = observerCount; this.hasBuddies = hasBuddies; let displayData = this.displayData; let sortValues = this.sortValues; displayData.buddy = this.hasBuddies ? setStringTags(g_BuddySymbol, displayData.tags) : ""; sortValues.hasBuddyString = String(hasBuddies); sortValues.buddy = sortValues.hasBuddyString + sortValues.gameName; let playerCount = players.length - observerCount; let gameRating = playerCount ? Math.round(playerRatingTotal / playerCount) : g_DefaultLobbyRating; this.gameRating = gameRating; sortValues.gameRating = gameRating; Engine.ProfileStop(); } } updateMods(newStanza) { { Engine.ProfileStart("JSON.parse"); try { this.mods = JSON.parse(newStanza.mods); } catch (e) { this.mods = []; } Engine.ProfileStop(); } { Engine.ProfileStart("hasSameMods"); let isCompatible = this.mods && hasSameMods(this.mods, Engine.GetEngineInfo().mods); if (this.isCompatible != isCompatible) { this.isCompatible = isCompatible; this.updateGameTags(newStanza); this.sortValues.compatibility = String(isCompatible); } Engine.ProfileStop(); } } updateGameTags(newStanza) { let displayData = this.displayData; displayData.tags = this.isCompatible ? this.StateTags[newStanza.state] : this.IncompatibleTags; displayData.buddy = this.hasBuddies ? setStringTags(g_BuddySymbol, displayData.tags) : ""; this.updateGameName(newStanza); } updateGameName(newStanza) { let displayData = this.displayData; displayData.gameName = setStringTags(escapeText(newStanza.name), displayData.tags); let sortValues = this.sortValues; sortValues.gameName = sortValues.compatibility + sortValues.state + sortValues.gameName; sortValues.buddy = sortValues.hasBuddyString + sortValues.gameName; } getTranslatedPlayerCount(newStanza) { let playerCountArgs = this.playerCountArgs; playerCountArgs.current = setStringTags(escapeText(newStanza.nbp), this.PlayerCountTags.CurrentPlayers); playerCountArgs.max = setStringTags(escapeText(newStanza.maxnbp), this.PlayerCountTags.MaxPlayers); let txt; if (this.observerCount) { playerCountArgs.observercount = setStringTags(this.observerCount, this.PlayerCountTags.Observers); txt = this.PlayerCountObservers; } else txt = this.PlayerCountNoObservers; return sprintf(txt, playerCountArgs); } } /** * These are all keys that occur in a gamelist stanza sent by XPartaMupp. */ Game.prototype.StanzaKeys = [ "name", "hasPassword", "hostUsername", "state", "nbp", "maxnbp", "players", "mapName", "niceMapName", "mapSize", "mapType", "victoryConditions", "startTime", "mods" ]; /** * Initial sorting order of the gamelist. */ Game.prototype.GameStatusOrder = [ "init", "waiting", "running" ]; // Translation: The number of players and observers in this game Game.prototype.PlayerCountObservers = translate("%(current)s/%(max)s +%(observercount)s"); // Translation: The number of players in this game Game.prototype.PlayerCountNoObservers = translate("%(current)s/%(max)s"); /** * Compatible games will be listed in these colors. */ Game.prototype.StateTags = { "init": { "color": "0 219 0" }, "waiting": { "color": "255 127 0" }, "running": { "color": "219 0 0" } }; /** * Games that require different mods than the ones launched by the current player are grayed out. */ Game.prototype.IncompatibleTags = { "color": "gray" }; /** * Color for the player count number in the games list. */ Game.prototype.PlayerCountTags = { "CurrentPlayers": { "color": "0 160 160" }, "MaxPlayers": { "color": "0 160 160" }, "Observers": { "color": "0 128 128" } }; Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameList.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameList.js (revision 24794) +++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameList.js (revision 24795) @@ -1,222 +1,226 @@ /** * Each property of this class handles one specific map filter and is defined in external files. */ class GameListFilters { } /** * This class displays the list of multiplayer matches that are currently being set up or running, * filtered and sorted depending on player selection. */ class GameList { constructor(xmppMessages, buddyButton, mapCache) { this.mapCache = mapCache; // Array of Game class instances, where the keys are ip+port strings, used for quick lookups this.games = {}; // Array of Game class instances sorted by display order this.gameList = []; this.selectionChangeHandlers = new Set(); this.gamesBox = Engine.GetGUIObjectByName("gamesBox"); this.gamesBox.onSelectionChange = this.onSelectionChange.bind(this); this.gamesBox.onSelectionColumnChange = this.onFilterChange.bind(this); let ratingColumn = Engine.ConfigDB_GetValue("user", "lobby.columns.gamerating") == "true"; this.gamesBox.hidden_mapType = ratingColumn; this.gamesBox.hidden_gameRating = !ratingColumn; // Avoid repeated array construction this.list_buddy = []; + this.list_private = []; this.list_gameName = []; this.list_mapName = []; this.list_mapSize = []; this.list_mapType = []; this.list_maxnbp = []; this.list_gameRating = []; this.list = []; this.filters = []; for (let name in GameListFilters) this.filters.push(new GameListFilters[name](this.onFilterChange.bind(this))); xmppMessages.registerXmppMessageHandler("game", "gamelist", this.rebuildGameList.bind(this)); xmppMessages.registerXmppMessageHandler("system", "disconnected", this.rebuildGameList.bind(this)); buddyButton.registerBuddyChangeHandler(this.onBuddyChange.bind(this)); this.rebuildGameList(); } registerSelectionChangeHandler(handler) { this.selectionChangeHandlers.add(handler); } onFilterChange() { this.rebuildGameList(); } onBuddyChange() { for (let name in this.games) this.games[name].onBuddyChange(); this.rebuildGameList(); } onSelectionChange() { let game = this.selectedGame(); for (let handler of this.selectionChangeHandlers) handler(game); } selectedGame() { return this.gameList[this.gamesBox.selected] || undefined; } rebuildGameList() { Engine.ProfileStart("rebuildGameList"); Engine.ProfileStart("getGameList"); let selectedGame = this.selectedGame(); let gameListData = Engine.GetGameList(); Engine.ProfileStop(); { Engine.ProfileStart("updateGames"); let selectedColumn = this.gamesBox.selected_column; let newGames = {}; for (let stanza of gameListData) { let game = this.games[stanza.hostUsername] || undefined; let exists = !!game; if (!exists) game = new Game(this.mapCache); game.update(stanza, selectedColumn); newGames[stanza.hostUsername] = game; } this.games = newGames; Engine.ProfileStop(); } { Engine.ProfileStart("filterGameList"); this.gameList.length = 0; for (let ip in this.games) { let game = this.games[ip]; if (this.filters.every(filter => filter.filter(game))) this.gameList.push(game); } Engine.ProfileStop(); } { Engine.ProfileStart("sortGameList"); let sortOrder = this.gamesBox.selected_column_order; this.gameList.sort((game1, game2) => { if (game1.sortValue < game2.sortValue) return -sortOrder; if (game1.sortValue > game2.sortValue) return +sortOrder; return 0; }); Engine.ProfileStop(); } let selectedGameIndex = -1; { Engine.ProfileStart("setupGameList"); let length = this.gameList.length; this.list_buddy.length = length; + this.list_private.length = length; this.list_gameName.length = length; this.list_mapName.length = length; this.list_mapSize.length = length; this.list_mapType.length = length; this.list_maxnbp.length = length; this.list_gameRating.length = length; this.list.length = length; this.gameList.forEach((game, i) => { let displayData = game.displayData; this.list_buddy[i] = displayData.buddy; + this.list_private[i] = displayData.private; this.list_gameName[i] = displayData.gameName; this.list_mapName[i] = displayData.mapName; this.list_mapSize[i] = displayData.mapSize; this.list_mapType[i] = displayData.mapType; this.list_maxnbp[i] = displayData.playerCount; this.list_gameRating[i] = game.gameRating; this.list[i] = ""; if (selectedGame && game.hostUsername == selectedGame.hostUsername && game.stanza.gameName == selectedGame.stanza.gameName) selectedGameIndex = i; }); Engine.ProfileStop(); } { Engine.ProfileStart("copyToGUI"); let gamesBox = this.gamesBox; + gamesBox.list_private = this.list_private; gamesBox.list_buddy = this.list_buddy; gamesBox.list_gameName = this.list_gameName; gamesBox.list_mapName = this.list_mapName; gamesBox.list_mapSize = this.list_mapSize; gamesBox.list_mapType = this.list_mapType; gamesBox.list_maxnbp = this.list_maxnbp; gamesBox.list_gameRating = this.list_gameRating; // Change these last, otherwise crash gamesBox.list = this.list; gamesBox.list_data = this.list; gamesBox.auto_scroll = false; Engine.ProfileStop(); gamesBox.selected = selectedGameIndex; } Engine.ProfileStop(); } /** * Select the game where the selected player is currently playing, observing or offline. * Selects in that order to account for players that occur in multiple games. */ selectGameFromPlayername(playerName) { if (!playerName) return; let foundAsObserver = false; for (let i = 0; i < this.gameList.length; ++i) for (let player of this.gameList[i].players) { if (playerName != splitRatingFromNick(player.Name).nick) continue; this.gamesBox.auto_scroll = true; if (player.Team == "observer") { foundAsObserver = true; this.gamesBox.selected = i; } else if (!player.Offline) { this.gamesBox.selected = i; return; } if (!foundAsObserver) this.gamesBox.selected = i; } } } Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameList.xml =================================================================== --- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameList.xml (revision 24794) +++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameList.xml (revision 24795) @@ -1,29 +1,30 @@ + Name Map Name Size Type Players Rating Index: ps/trunk/binaries/data/mods/public/gui/lobby/icons/private.xml =================================================================== --- ps/trunk/binaries/data/mods/public/gui/lobby/icons/private.xml (nonexistent) +++ ps/trunk/binaries/data/mods/public/gui/lobby/icons/private.xml (revision 24795) @@ -0,0 +1,8 @@ + + + + + Index: ps/trunk/binaries/data/mods/public/gui/lobby/icons/public.xml =================================================================== --- ps/trunk/binaries/data/mods/public/gui/lobby/icons/public.xml (nonexistent) +++ ps/trunk/binaries/data/mods/public/gui/lobby/icons/public.xml (revision 24795) @@ -0,0 +1,8 @@ + + + + + Index: ps/trunk/binaries/data/mods/public/gui/page_lobby.xml =================================================================== --- ps/trunk/binaries/data/mods/public/gui/page_lobby.xml (revision 24794) +++ ps/trunk/binaries/data/mods/public/gui/page_lobby.xml (revision 24795) @@ -1,15 +1,16 @@ common/modern/setup.xml common/modern/styles.xml common/modern/sprites.xml common/styles.xml common/sprites.xml + lobby/icons/ lobby/lobby.xml common/global.xml