Index: binaries/data/config/default.cfg =================================================================== --- binaries/data/config/default.cfg +++ binaries/data/config/default.cfg @@ -399,6 +399,9 @@ [chat.session] extended = true ; Whether to display the chat history +[load] +gamessort = "date:-1,mapName:1,mapType:1,description:1" + [lobby] history = 0 ; Number of past messages to display on join room = "arena23" ; Default MUC room to join @@ -409,6 +412,10 @@ [lobby.columns] gamerating = false ; Show the average rating of the participating players in a column of the gamelist +[lobby.sort] +players = "buddy:1,status:1,name:1,rating:-1" +games = "buddy:1,name:1,mapType:1,mapSize:1,gameRating:-1,mapName:1,nPlayers:-1" + [lobby.stun] enabled = true ; The STUN protocol allows hosting games without configuring the firewall and router. ; If STUN is disabled, the game relies on direct connection, UPnP and port forwarding. @@ -436,6 +443,9 @@ gpu.ext.enable = true ; Allow GL_EXT_timer_query timing mode when available gpu.intel.enable = true ; Allow GL_INTEL_performance_queries timing mode when available +[replay] +sort = "months:-1,players:1,mapName:1,mapSize:1,popCapacity:1,duration:1" + [sound] mastergain = 0.9 musicgain = 0.2 Index: binaries/data/mods/public/gui/common/functions_utility.js =================================================================== --- binaries/data/mods/public/gui/common/functions_utility.js +++ binaries/data/mods/public/gui/common/functions_utility.js @@ -8,6 +8,11 @@ }; /** + * Locale language used for string comparisons. + */ +var g_Localization = Engine.ConfigDB_GetValue("user", "locale").replace("_", "-") || "en-GB"; + +/** * Save setting for current instance and write setting to the user config file. */ function saveSettingAndWriteToUserConfig(setting, value) @@ -237,3 +242,75 @@ for (let i = start; i < objects.length; ++i) objects[i].hidden = true; } + +/** + * Initialize GUI list sort. + * + * @param {string} guiList - Name of GUI List object + * @param {string} config - User config setting name + * @returns {array} - column sort array + */ +function initGUIListSort(guiList, config) +{ + let columnOrder = Engine.ConfigDB_GetValue("user", config).split(",").map(key => { + let [name, order] = key.split(":"); + return { "name": name, "order": +order }; + }); + + let guiObj = Engine.GetGUIObjectByName(guiList); + + guiObj.selected_column_order = columnOrder[0].order; + guiObj.selected_column = columnOrder[0].name; + + return columnOrder; +} + +/** + * Change order of sorts array and sort order. + * + * @param {obj} guiListObj - GUI list object + * @param {array} sorts - Sorts array + */ +function changeGUIListSort(guiListObj, sorts, config) +{ + let columnName = guiListObj.selected_column; + let sortsIndex = sorts.findIndex(sort => sort.name == columnName); + + if (sortsIndex > 0) + guiListObj.selected_column_order = sorts[sortsIndex].order; + + if (sortsIndex > -1) + sorts.splice(sortsIndex, 1); + + sorts.unshift({ "name": columnName, "order": guiListObj.selected_column_order }); + + saveSettingAndWriteToUserConfig(config, sorts.map(sort => sort.name + ":" + sort.order).join(",")); +} + +/** + * Compare two objects a and b by an attribute. Also mind language localization if strings are compared. + * + * @param {obj} objA - Object A + * @param {obj} objB - Object B + * @param {string} attribute - Attribute of object a and b to compare. + * @param {obj} attributeTranslation - Attributes translation object + * @param {number} order - Sorting order + */ +function cmpObjs(objA, objB, attribute, attributeTranslation, order) +{ + let cmp = attributeTranslation[attribute] ? obj => attributeTranslation[attribute](obj) : obj[attribute] || 0; + + let cmpA = cmp(objA); + let cmpB = cmp(objB); + + if (typeof cmpA == "string" && typeof cmpB == "string") + return order * cmpA.localeCompare(cmpB, g_Localization); + + if (cmpA < cmpB) + return -order; + + if (cmpA > cmpB) + return +order; + + return 0; +} Index: binaries/data/mods/public/gui/loadgame/load.js =================================================================== --- binaries/data/mods/public/gui/loadgame/load.js +++ binaries/data/mods/public/gui/loadgame/load.js @@ -5,6 +5,11 @@ */ const g_CivData = loadCivData(false, false); +/** + * Set gamelist column sort. + */ +var g_GamesSort = []; + function init() { let savedGames = Engine.GetSavedGames(); @@ -15,7 +20,7 @@ if (Engine.GetGUIObjectByName("compatibilityFilter").checked) savedGames = savedGames.filter(game => isCompatibleSavegame(game.metadata, engineInfo)); - let gameSelection = Engine.GetGUIObjectByName("gameSelection"); + let gameSelection = Engine.GetGUIObjectByName("gameList"); gameSelection.enabled = !!savedGames.length; Engine.GetGUIObjectByName("gameSelectionFeedback").hidden = !!savedGames.length; @@ -27,35 +32,22 @@ return game.metadata; }); - let sortKey = gameSelection.selected_column; - let sortOrder = gameSelection.selected_column_order; + g_GamesSort = initGUIListSort("gameList", "load.gamessort"); g_SavedGamesMetadata = g_SavedGamesMetadata.sort((a, b) => { - let cmpA, cmpB; - switch (sortKey) + + for (let sort of g_GamesSort) { - case 'date': - cmpA = +a.time; - cmpB = +b.time; - break; - case 'mapName': - cmpA = translate(a.initAttributes.settings.Name); - cmpB = translate(b.initAttributes.settings.Name); - break; - case 'mapType': - cmpA = translateMapType(a.initAttributes.mapType); - cmpB = translateMapType(b.initAttributes.mapType); - break; - case 'description': - cmpA = a.description; - cmpB = b.description; - break; + let ret = cmpObjs(a, b, sort.name, { + 'date': obj => +obj.time, + 'mapName': obj => translate(obj.initAttributes.settings.Name), + 'mapType': obj => translateMapType(obj.initAttributes.mapType), + 'description': obj => obj.description + }, sort.order); + + if (ret) + return ret; } - if (cmpA < cmpB) - return -sortOrder; - else if (cmpA > cmpB) - return +sortOrder; - return 0; }); @@ -95,7 +87,7 @@ function selectionChanged() { - let metadata = g_SavedGamesMetadata[Engine.GetGUIObjectByName("gameSelection").selected]; + let metadata = g_SavedGamesMetadata[Engine.GetGUIObjectByName("gameList").selected]; Engine.GetGUIObjectByName("invalidGame").hidden = !!metadata; Engine.GetGUIObjectByName("validGame").hidden = !metadata; Engine.GetGUIObjectByName("loadGameButton").enabled = !!metadata; @@ -127,7 +119,7 @@ function loadGame() { - let gameSelection = Engine.GetGUIObjectByName("gameSelection"); + let gameSelection = Engine.GetGUIObjectByName("gameList"); let gameId = gameSelection.list_data[gameSelection.selected]; let metadata = g_SavedGamesMetadata[gameSelection.selected]; Index: binaries/data/mods/public/gui/loadgame/load.xml =================================================================== --- binaries/data/mods/public/gui/loadgame/load.xml +++ binaries/data/mods/public/gui/loadgame/load.xml @@ -15,7 +15,7 @@ - selectionChanged(); - init(); + + changeGUIListSort(this, g_GamesSort, "load.gamessort"); + init(); + loadGame(); Index: binaries/data/mods/public/gui/lobby/lobby.js =================================================================== --- binaries/data/mods/public/gui/lobby/lobby.js +++ binaries/data/mods/public/gui/lobby/lobby.js @@ -101,6 +101,16 @@ var g_GameList = []; /** + * Set gamelist column sort. + */ +var g_GamesSort = []; + +/** + * Set playerlist column sort. + */ +var g_PlayersSort = []; + +/** * Used to restore the selection after updating the playerlist. */ var g_SelectedPlayer = ""; @@ -408,6 +418,7 @@ global.music.setState(global.music.states.MENU); initDialogStyle(); + g_GamesSort = initGUIListSort("gameList", "lobby.sort.games"); initGameFilters(); updateConnectedState(); @@ -415,6 +426,7 @@ // When rejoining the lobby after a game, we don't need to process presence changes Engine.LobbyClearPresenceUpdates(); + g_PlayersSort = initGUIListSort("playerList", "lobby.sort.players"); updatePlayerList(); updateSubject(Engine.LobbyGetRoomSubject()); updateLobbyColumns(); @@ -491,7 +503,7 @@ let gameRating = Engine.ConfigDB_GetValue("user", "lobby.columns.gamerating") == "true"; // Only show the selected columns - let gamesBox = Engine.GetGUIObjectByName("gamesBox"); + let gamesBox = Engine.GetGUIObjectByName("gameList"); gamesBox.hidden_mapType = gameRating; gamesBox.hidden_gameRating = !gameRating; @@ -679,7 +691,7 @@ */ function updateToggleBuddy() { - let playerList = Engine.GetGUIObjectByName("playersBox"); + let playerList = Engine.GetGUIObjectByName("playerList"); let playerName = playerList.list[playerList.selected]; let toggleBuddyButton = Engine.GetGUIObjectByName("toggleBuddyButton"); @@ -692,10 +704,7 @@ */ function updatePlayerList() { - let playersBox = Engine.GetGUIObjectByName("playersBox"); - let sortBy = playersBox.selected_column || "name"; - let sortOrder = playersBox.selected_column_order || 1; - + let playersBox = Engine.GetGUIObjectByName("playerList"); let buddyStatusList = []; let playerList = []; let presenceList = []; @@ -706,33 +715,21 @@ player.isBuddy = g_Buddies.indexOf(player.name) != -1; return player; }).sort((a, b) => { - let sortA, sortB; - let statusOrder = Object.keys(g_PlayerStatuses); - let statusA = statusOrder.indexOf(a.presence) + a.name.toLowerCase(); - let statusB = statusOrder.indexOf(b.presence) + b.name.toLowerCase(); + let status = obj => Object.keys(g_PlayerStatuses).indexOf(obj.presence) + obj.name.toLowerCase(); - switch (sortBy) + for (let sort of g_PlayersSort) { - case 'buddy': - sortA = (a.isBuddy ? 1 : 2) + statusA; - sortB = (b.isBuddy ? 1 : 2) + statusB; - break; - case 'rating': - sortA = +a.rating; - sortB = +b.rating; - break; - case 'status': - sortA = statusA; - sortB = statusB; - break; - case 'name': - default: - sortA = a.name.toLowerCase(); - sortB = b.name.toLowerCase(); - break; + let ret = cmpObjs(a, b, sort.name, { + 'buddy': obj => (obj.isBuddy ? 1 : 2) + status(obj), + 'rating': obj => +obj.rating, + 'status': obj => status(obj), + 'name': obj => obj.name.toLowerCase() + }, sort.order); + + if (ret) + return ret; } - if (sortA < sortB) return -sortOrder; - if (sortA > sortB) return +sortOrder; + return 0; }); @@ -769,7 +766,7 @@ */ function toggleBuddy() { - let playerList = Engine.GetGUIObjectByName("playersBox"); + let playerList = Engine.GetGUIObjectByName("playerList"); let name = playerList.list[playerList.selected]; if (!name || name == g_Username || name.indexOf(g_BuddyListDelimiter) != -1) @@ -798,7 +795,7 @@ if (!g_SelectedPlayer) return; - let gameList = Engine.GetGUIObjectByName("gamesBox"); + let gameList = Engine.GetGUIObjectByName("gameList"); let foundAsObserver = false; for (let i = 0; i < g_GameList.length; ++i) @@ -825,12 +822,12 @@ function onPlayerListSelection() { - let playerList = Engine.GetGUIObjectByName("playersBox"); + let playerList = Engine.GetGUIObjectByName("playerList"); if (playerList.selected == playerList.list.indexOf(g_SelectedPlayer)) return; g_SelectedPlayer = playerList.list[playerList.selected]; - lookupSelectedUserProfile("playersBox"); + lookupSelectedUserProfile("playerList"); updateToggleBuddy(); selectGameFromPlayername(); } @@ -840,7 +837,7 @@ if (visible) Engine.SendGetBoardList(); - lookupSelectedUserProfile(visible ? "leaderboardBox" : "playersBox"); + lookupSelectedUserProfile(visible ? "leaderboardBox" : "playerList"); Engine.GetGUIObjectByName("leaderboard").hidden = !visible; Engine.GetGUIObjectByName("fade").hidden = !visible; } @@ -929,7 +926,7 @@ if (!Engine.GetGUIObjectByName("leaderboard").hidden) playerList = Engine.GetGUIObjectByName("leaderboardBox"); else - playerList = Engine.GetGUIObjectByName("playersBox"); + playerList = Engine.GetGUIObjectByName("playerList"); if (attributes.rating == "-2") return; @@ -982,9 +979,8 @@ */ function updateGameList() { - let gamesBox = Engine.GetGUIObjectByName("gamesBox"); - let sortBy = gamesBox.selected_column; - let sortOrder = gamesBox.selected_column_order; + let gamesBox = Engine.GetGUIObjectByName("gameList"); + let compTrans = (compTrans, obj, att, defAtt) => compTrans[att] && compTrans[att](obj) || obj[att] || obj[defAtt]; if (gamesBox.selected > -1) { @@ -1018,34 +1014,23 @@ return game; }).filter(game => !filterGame(game)).sort((a, b) => { - let sortA, sortB; - switch (sortBy) + + for (let sort of g_GamesSort) { - case 'name': - sortA = g_GameStatusOrder.indexOf(a.state) + a.name.toLowerCase(); - sortB = g_GameStatusOrder.indexOf(b.state) + b.name.toLowerCase(); - break; - case 'gameRating': - case 'mapSize': - case 'mapType': - sortA = a[sortBy]; - sortB = b[sortBy]; - break; - case 'buddy': - sortA = String(b.hasBuddies) + g_GameStatusOrder.indexOf(a.state) + a.name.toLowerCase(); - sortB = String(a.hasBuddies) + g_GameStatusOrder.indexOf(b.state) + b.name.toLowerCase(); - break; - case 'mapName': - sortA = translate(a.niceMapName); - sortB = translate(b.niceMapName); - break; - case 'nPlayers': - sortA = a.maxnbp; - sortB = b.maxnbp; - break; + if (gamesBox["hidden_" + sort.name]) + continue; + + let ret = cmpObjs(a, b, sort.name, { + 'buddy': obj => String(b.hasBuddies) + g_GameStatusOrder.indexOf(obj.state) + obj.name.toLowerCase(), + 'name': obj => g_GameStatusOrder.indexOf(obj.state) + obj.name.toLowerCase(), + 'mapName': obj => translate(obj.niceMapName), + 'nPlayers': obj => obj.maxnbp + }, sort.order); + + if (ret) + return ret; } - if (sortA < sortB) return -sortOrder; - if (sortA > sortB) return +sortOrder; + return 0; }); @@ -1151,7 +1136,7 @@ function selectedGame() { - let gamesBox = Engine.GetGUIObjectByName("gamesBox"); + let gamesBox = Engine.GetGUIObjectByName("gameList"); if (gamesBox.selected < 0) return undefined; Index: binaries/data/mods/public/gui/lobby/lobby_panels.xml =================================================================== --- binaries/data/mods/public/gui/lobby/lobby_panels.xml +++ binaries/data/mods/public/gui/lobby/lobby_panels.xml @@ -10,7 +10,7 @@ - + changeGUIListSort(this, g_PlayersSort, "lobby.sort.players"); updatePlayerList(); @@ -173,7 +174,7 @@ - updateGameSelection(); - applyFilters(); + + changeGUIListSort(this, g_GamesSort, "lobby.sort.games"); + applyFilters(); + joinButton(); Index: binaries/data/mods/public/gui/replaymenu/replay_filters.js =================================================================== --- binaries/data/mods/public/gui/replaymenu/replay_filters.js +++ binaries/data/mods/public/gui/replaymenu/replay_filters.js @@ -180,44 +180,22 @@ */ function filterReplays() { - let sortKey = Engine.GetGUIObjectByName("replaySelection").selected_column; - let sortOrder = Engine.GetGUIObjectByName("replaySelection").selected_column_order; - g_ReplaysFiltered = g_Replays.filter(replay => filterReplay(replay)).sort((a, b) => { - let cmpA, cmpB; - switch (sortKey) + for (let sort of g_ColumnOrder) { - case 'months': - cmpA = +a.attribs.timestamp; - cmpB = +b.attribs.timestamp; - break; - case 'duration': - cmpA = +a.duration; - cmpB = +b.duration; - break; - case 'players': - cmpA = +a.attribs.settings.PlayerData.length; - cmpB = +b.attribs.settings.PlayerData.length; - break; - case 'mapName': - cmpA = getReplayMapName(a); - cmpB = getReplayMapName(b); - break; - case 'mapSize': - cmpA = +a.attribs.settings.Size; - cmpB = +b.attribs.settings.Size; - break; - case 'popCapacity': - cmpA = +a.attribs.settings.PopulationCap; - cmpB = +b.attribs.settings.PopulationCap; - break; + let ret = cmpObjs(a, b, sort.name, { + 'months': obj => obj.attribs.timestamp, + 'duration': obj => obj.duration, + 'players': obj => +obj.attribs.settings.PlayerData.length, + 'mapName': obj => getReplayMapName(obj), + 'mapSize': obj => +obj.attribs.settings.Size, + 'popCapacity': obj => +obj.attribs.settings.PopulationCap + }, sort.order); + + if (ret) + return ret; } - if (cmpA < cmpB) - return -sortOrder; - else if (cmpA > cmpB) - return +sortOrder; - return 0; }); } Index: binaries/data/mods/public/gui/replaymenu/replay_menu.js =================================================================== --- binaries/data/mods/public/gui/replaymenu/replay_menu.js +++ binaries/data/mods/public/gui/replaymenu/replay_menu.js @@ -54,6 +54,11 @@ var g_SummarySelectedData; /** + * Set list column sort. + */ +var g_ColumnOrder = []; + +/** * Initializes globals, loads replays and displays the list. */ function init(data) @@ -64,6 +69,7 @@ return; } + g_ColumnOrder = initGUIListSort("replaySelection", "replay.sort"); loadReplays(data && data.replaySelectionData, false); if (!g_Replays) Index: binaries/data/mods/public/gui/replaymenu/replay_menu.xml =================================================================== --- binaries/data/mods/public/gui/replaymenu/replay_menu.xml +++ binaries/data/mods/public/gui/replaymenu/replay_menu.xml @@ -61,7 +61,10 @@ > displayReplayDetails(); - displayReplayList(); + + changeGUIListSort(this, g_ColumnOrder, "replay.sort"); + displayReplayList(); + startReplay();