Index: ps/trunk/binaries/data/mods/public/gui/autostart/autostart.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/autostart/autostart.js (revision 26392) +++ ps/trunk/binaries/data/mods/public/gui/autostart/autostart.js (revision 26393) @@ -1,17 +1,17 @@ function init(initData) { let settings = new GameSettings().init(); settings.fromInitAttributes(initData); let assignments = { "local": { "player": 1, "name": Engine.ConfigDB_GetValue("user", "playername.singleplayer") || Engine.GetSystemUsername() } }; settings.launchGame(assignments); Engine.SwitchGuiPage("page_loading.xml", { - "attribs": settings.toInitAttributes(), + "attribs": settings.finalizedAttributes, "playerAssignments": assignments }); } Index: ps/trunk/binaries/data/mods/public/gui/campaigns/default_menu/CampaignMenu.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/campaigns/default_menu/CampaignMenu.js (revision 26392) +++ ps/trunk/binaries/data/mods/public/gui/campaigns/default_menu/CampaignMenu.js (revision 26393) @@ -1,220 +1,219 @@ /** * This is the main menu screen of the campaign. * It shows you the currently available scenarios, scenarios you've already completed, etc. * This particular variant is extremely simple and shows a list similar to Age 1's campaigns, * but conceptually nothing really prevents more complex systems. */ class CampaignMenu extends AutoWatcher { constructor(campaignRun) { super("render"); this.run = campaignRun; this.selectedLevel = -1; this.levelSelection = Engine.GetGUIObjectByName("levelSelection"); this.levelSelection.onSelectionChange = () => { this.selectedLevel = this.levelSelection.selected; }; this.levelSelection.onMouseLeftDoubleClickItem = () => this.startScenario(); Engine.GetGUIObjectByName('startButton').onPress = () => this.startScenario(); Engine.GetGUIObjectByName('backToMain').onPress = () => this.goBackToMainMenu(); Engine.GetGUIObjectByName('savedGamesButton').onPress = () => Engine.PushGuiPage('page_loadgame.xml', { 'campaignRun': this.run.filename }); this.mapCache = new MapCache(); this._ready = true; } goBackToMainMenu() { this.run.save(); Engine.SwitchGuiPage("page_pregame.xml", {}); } startScenario() { const level = this.getSelectedLevelData(); if (!meetsRequirements(this.run, level)) return; + // TODO: level description should also be passed, ideally. const settings = { "mapType": level.MapType, "map": "maps/" + level.Map, "settings": { + // TODO: don't translate this here. + "mapName": this.getLevelName(level), + "mapPreview": level.Preview && "cropped:" + 400/512 + "," + 300/512 + ":" + level.Preview, "CheatsEnabled": true }, "campaignData": { "run": this.run.filename, "levelID": this.levelSelection.list_data[this.selectedLevel], "data": this.run.data } }; const assignments = { "local": { "player": 1, "name": Engine.ConfigDB_GetValue("user", "playername.singleplayer") || Engine.GetSystemUsername() } }; const gameSettings = new GameSettings().init(); gameSettings.fromInitAttributes(settings); - if (level.Preview) - gameSettings.mapPreview.setCustom("cropped:" + 400/512 + "," + 300/512 + ":" + level.Preview); - gameSettings.mapName.set(this.getLevelName(level)); - // TODO: level description should also be passed, ideally. - if (level.useGameSetup) { // Setup some default AI on the non-human players. for (let i = 1; i < gameSettings.playerCount.nbPlayers; ++i) gameSettings.playerAI.set(i, { "bot": g_Settings.PlayerDefaults[i + 1].AI, "difficulty": +Engine.ConfigDB_GetValue("user", "gui.gamesetup.aidifficulty"), "behavior": Engine.ConfigDB_GetValue("user", "gui.gamesetup.aibehavior"), }); const attributes = gameSettings.toInitAttributes(); attributes.guiData = { "lockSettings": { "map": true, }, }; Engine.SwitchGuiPage("page_gamesetup.xml", { "backPage": { "page": this.run.getMenuPath(), "data": { "filename": this.run.filename } }, "gameSettings": attributes, }); return; } gameSettings.launchGame(assignments); Engine.SwitchGuiPage("page_loading.xml", { - "attribs": gameSettings.toInitAttributes(), + "attribs": gameSettings.finalizedAttributes, "playerAssignments": assignments }); } getSelectedLevelData() { if (this.selectedLevel === -1) return undefined; return this.run.template.Levels[this.levelSelection.list_data[this.selectedLevel]]; } shouldShowLevel(levelData) { if (this.run.template.ShowUnavailable) return true; return meetsRequirements(this.run, levelData); } getLevelName(levelData) { if (levelData.Name) return translateWithContext("Campaign Template", levelData.Name); return translate(this.mapCache.getTranslatableMapName(levelData.MapType, "maps/" + levelData.Map)); } getLevelDescription(levelData) { if (levelData.Description) return translateWithContext("Campaign Template", levelData.Description); return this.mapCache.getTranslatedMapDescription(levelData.MapType, "maps/" + levelData.Map); } getLevelPreview(levelData) { if (levelData.Preview) return "cropped:" + 400/512 + "," + 300/512 + ":" + levelData.Preview; return this.mapCache.getMapPreview(levelData.MapType, "maps/" + levelData.Map); } displayLevelsList() { let list = []; for (let key in this.run.template.Levels) { let level = this.run.template.Levels[key]; if (!this.shouldShowLevel(level)) continue; let status = ""; let name = this.getLevelName(level); if (isCompleted(this.run, key)) status = translateWithContext("campaign status", "Completed"); else if (meetsRequirements(this.run, level)) status = coloredText(translateWithContext("campaign status", "Available"), "green"); else name = coloredText(name, "gray"); list.push({ "ID": key, "name": name, "status": status }); } list.sort((a, b) => this.run.template.Order.indexOf(a.ID) - this.run.template.Order.indexOf(b.ID)); list = prepareForDropdown(list); this.levelSelection.list_name = list.name || []; this.levelSelection.list_status = list.status || []; // COList needs these changed last or crashes. this.levelSelection.list = list.ID || []; this.levelSelection.list_data = list.ID || []; } displayLevelDetails() { if (this.selectedLevel === -1) { Engine.GetGUIObjectByName("startButton").enabled = false; Engine.GetGUIObjectByName("startButton").hidden = false; return; } let level = this.getSelectedLevelData(); Engine.GetGUIObjectByName("scenarioName").caption = this.getLevelName(level); Engine.GetGUIObjectByName("scenarioDesc").caption = this.getLevelDescription(level); Engine.GetGUIObjectByName('levelPreviewBox').sprite = this.getLevelPreview(level); Engine.GetGUIObjectByName("startButton").enabled = meetsRequirements(this.run, level); Engine.GetGUIObjectByName("startButton").hidden = false; Engine.GetGUIObjectByName("loadSavedButton").hidden = true; } render() { Engine.GetGUIObjectByName("campaignTitle").caption = this.run.getLabel(); this.displayLevelDetails(); this.displayLevelsList(); } } var g_CampaignMenu; function init(initData) { let run = initData?.filename || CampaignRun.getCurrentRunFilename(); try { run = new CampaignRun(run).load(); if (!run.isCurrent()) run.setCurrent(); g_CampaignMenu = new CampaignMenu(run); } catch (err) { error(sprintf(translate("Error loading campaign run %s: %s."), CampaignRun.getCurrentRunFilename(), err)); Engine.SwitchGuiPage("page_pregame.xml", {}); } } Index: ps/trunk/binaries/data/mods/public/gui/gamesettings/GameSettings.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesettings/GameSettings.js (revision 26392) +++ ps/trunk/binaries/data/mods/public/gui/gamesettings/GameSettings.js (revision 26393) @@ -1,136 +1,141 @@ /** * Data store for game settings. * * This is intended as a helper to create the settings object for a game. * This object is referred to as: * - g_InitAttributes in the GUI session context * - InitAttributes in the JS simulation context * - Either InitAttributes or MapSettings in the C++ simulation. * Settings can depend on each other, and the map provides many. * This class's job is thus to provide a simpler interface around that. */ class GameSettings { init(mapCache) { if (!mapCache) mapCache = new MapCache(); Object.defineProperty(this, "mapCache", { "value": mapCache, }); // Load all possible civ data - don't presume that some will be available. Object.defineProperty(this, "civData", { "value": loadCivData(false, false), }); Object.defineProperty(this, "isNetworked", { "value": Engine.HasNetClient(), }); // Load attributes as regular enumerable (i.e. iterable) properties. for (let comp in GameSettings.prototype.Attributes) { let name = comp[0].toLowerCase() + comp.substr(1); if (name in this) error("Game Settings attribute '" + name + "' is already used."); this[name] = new GameSettings.prototype.Attributes[comp](this); } for (let comp in this) - this[comp].init(); + if (this[comp].init) + this[comp].init(); return this; } /** * 'Serialize' the settings into the InitAttributes format, * which can then be saved as JSON. * Used to set the InitAttributes, for network synching, for hotloading & for persistence. * TODO: it would probably be better to have different paths for at least a few of these. */ toInitAttributes() { let attribs = { "settings": {} }; for (let comp in this) if (this[comp].toInitAttributes) this[comp].toInitAttributes(attribs); return attribs; } /** * Deserialize from a the InitAttributes format (i.e. parsed JSON). * TODO: this could/should maybe support partial deserialization, * which means MP might actually send only the bits that change. */ fromInitAttributes(attribs) { // Settings depend on the map, but some settings // may also be illegal for a given map. // It would be good to validate, but just bulk-accept at the moment. // There is some light order-dependency between settings. // First deserialize the map, then the player #, then victory conditions, then the rest. // TODO: there's a DAG in there. this.map.fromInitAttributes(attribs); this.playerCount.fromInitAttributes(attribs); this.victoryConditions.fromInitAttributes(attribs); for (let comp in this) if (this[comp].fromInitAttributes && comp !== "map" && comp !== "playerCount" && comp !== "victoryConditions") this[comp].fromInitAttributes(attribs); } /** * Change "random" settings into their proper settings. */ pickRandomItems() { let components = Object.keys(this); let i = 0; while (components.length && i < 100) { // Re-pick if any random setting was unrandomised, // to make sure dependencies are cleared. // TODO: there's probably a better way to handle this. components = components.filter(comp => this[comp].pickRandomItems ? !!this[comp].pickRandomItems() : false); ++i; } if (i === 100) { throw new Error("Infinite loop picking random items, remains : " + uneval(components)); } } /** * Start the game & switch to the loading page. * This is here because there's limited value in having a separate folder/file for it, * since you'll need a GameSettings object anyways. * @param playerAssignments - A dict of 'local'/GUID per player and their name/slot. */ launchGame(playerAssignments) { this.pickRandomItems(); - Engine.SetRankedGame(this.rating.enabled); + // Let the settings finalize themselves. Let them do anything they need to do before the + // game starts and set any value in the attributes which mustn't be persisted. + const attribs = this.toInitAttributes(); + for (const comp in this) + if (this[comp].onFinalizeAttributes) + this[comp].onFinalizeAttributes(attribs, playerAssignments); - // Replace player names with the real players. - for (let guid in playerAssignments) - if (playerAssignments[guid].player !== -1) - this.playerName.values[playerAssignments[guid].player -1] = playerAssignments[guid].name; + Object.defineProperty(this, "finalizedAttributes", { + "value": deepfreeze(attribs) + }); // NB: for multiplayer support, the clients must be listening to "start" net messages. if (this.isNetworked) - Engine.StartNetworkGame(this.toInitAttributes()); + Engine.StartNetworkGame(this.finalizedAttributes); else - Engine.StartGame(this.toInitAttributes(), playerAssignments.local.player); + Engine.StartGame(this.finalizedAttributes, playerAssignments.local.player); } } Object.defineProperty(GameSettings.prototype, "Attributes", { "value": {}, "enumerable": false, "writable": true, }); Index: ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/CampaignData.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/CampaignData.js (revision 26392) +++ ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/CampaignData.js (revision 26393) @@ -1,21 +1,17 @@ /** * Store campaign-related data. * This is just a passthrough and makes no assumption about the data. */ GameSettings.prototype.Attributes.CampaignData = class CampaignData extends GameSetting { - init() - { - } - toInitAttributes(attribs) { if (this.value) attribs.campaignData = this.value; } fromInitAttributes(attribs) { this.value = attribs.campaignData; } }; Index: ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/Ceasefire.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/Ceasefire.js (revision 26392) +++ ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/Ceasefire.js (revision 26393) @@ -1,38 +1,36 @@ GameSettings.prototype.Attributes.Ceasefire = class Ceasefire extends GameSetting { init() { this.value = 0; this.settings.map.watch(() => this.onMapChange(), ["map"]); } toInitAttributes(attribs) { - if (!this.value) - return; attribs.settings.Ceasefire = this.value; } fromInitAttributes(attribs) { if (!this.getLegacySetting(attribs, "Ceasefire")) this.value = 0; else this.value = +this.getLegacySetting(attribs, "Ceasefire"); } onMapChange() { if (this.settings.map.type != "scenario") return; if (!this.getMapSetting("Ceasefire")) this.value = 0; else this.value = +this.getMapSetting("Ceasefire"); } setValue(val) { this.value = Math.round(val); } }; Index: ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/CircularMap.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/CircularMap.js (revision 26392) +++ ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/CircularMap.js (revision 26393) @@ -1,31 +1,31 @@ /** * This doesn't have a GUI setting. */ GameSettings.prototype.Attributes.CircularMap = class CircularMap extends GameSetting { init() { - this.value = undefined; + this.value = false; this.settings.map.watch(() => this.onMapChange(), ["map"]); } toInitAttributes(attribs) { - if (this.value) - attribs.settings.CircularMap = this.value; + attribs.settings.CircularMap = this.value; } - /** - * Exceptionally, this setting has no Deserialize: it's entirely determined by the map - */ + fromInitAttributes(attribs) + { + this.value = !!this.getLegacySetting(attribs, "CircularMap"); + } onMapChange() { this.value = this.getMapSetting("CircularMap") || false; } setValue(val) { this.value = val; } }; Index: ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/GameSpeed.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/GameSpeed.js (revision 26392) +++ ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/GameSpeed.js (revision 26393) @@ -1,32 +1,31 @@ GameSettings.prototype.Attributes.GameSpeed = class GameSpeed extends GameSetting { init() { this.gameSpeed = 1; this.settings.map.watch(() => this.onMapChange(), ["map"]); } toInitAttributes(attribs) { attribs.gameSpeed = +this.gameSpeed; } fromInitAttributes(attribs) { - if (!attribs.gameSpeed) - return; - this.gameSpeed = +attribs.gameSpeed; + if (attribs.gameSpeed) + this.gameSpeed = +attribs.gameSpeed; } onMapChange() { if (!this.getMapSetting("gameSpeed")) return; this.setSpeed(+this.getMapSetting("gameSpeed")); } setSpeed(speed) { this.gameSpeed = +speed; } }; Index: ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/MapName.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/MapName.js (revision 26392) +++ ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/MapName.js (revision 26393) @@ -1,36 +1,28 @@ /** * Map name. * This is usually just the regular map name, but can be overwritten. */ GameSettings.prototype.Attributes.MapName = class MapName extends GameSetting { init() { + this.value = undefined; + this.settings.map.watch(() => this.updateName(), ["map"]); } toInitAttributes(attribs) { - if (this.value) - attribs.settings.Name = this.value; - else - { - // Copy from the map data by default - this helps make InitAttributes self-sufficient, - // which is nice for replays / saved games. - // Fallback to the map name to avoid 'undefined' errors. - attribs.settings.Name = this.settings.map?.data?.settings?.Name || this.settings.map.map; - } + attribs.settings.mapName = this.value; } fromInitAttributes(attribs) { - // Ser/Deser from a different attribute name as a poor man's not-persisted-setting. - // TODO: split this off more properly. - if (attribs.mapName) - this.value = attribs.mapName; + if (attribs?.settings?.mapName) + this.value = attribs.settings.mapName; } - set(name) + updateName() { - this.value = name; + this.value = this.settings.map?.data?.settings?.Name || this.settings.map.map; } }; Index: ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/MapPreview.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/MapPreview.js (revision 26392) +++ ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/MapPreview.js (revision 26393) @@ -1,74 +1,64 @@ /** * Map Preview. * Can optionally overwrite the default map preview. */ GameSettings.prototype.Attributes.MapPreview = class MapPreview extends GameSetting { init() { - this.isDefault = true; + this.value = undefined; this.settings.map.watch(() => this.updatePreview(), ["map"]); this.settings.biome.watch(() => this.updatePreview(), ["biome"]); this.settings.landscape.watch(() => this.updatePreview(), ["value"]); this.settings.daytime.watch(() => this.updatePreview(), ["value"]); } toInitAttributes(attribs) { - // TODO: this shouldn't be persisted, only serialised for the game proper. - if (this.value) - attribs.mapPreview = this.value; + if (this.value !== undefined) + attribs.settings.mapPreview = this.value; } fromInitAttributes(attribs) { - // For now - this won't be deserialized or persisted match settings will be problematic. + if (!!this.getLegacySetting(attribs, "mapPreview")) + this.value = this.getLegacySetting(attribs, "mapPreview"); } getPreviewForSubtype(basepath, subtype) { if (!subtype) return undefined; let substr = subtype.substr(subtype.lastIndexOf("/") + 1); let path = basepath + "_" + substr + ".png"; if (this.settings.mapCache.previewExists(path)) return this.settings.mapCache.getMapPreview(this.settings.map.type, this.settings.map.map, path); return undefined; } getLandscapePreview() { let filename = this.settings.landscape.getPreviewFilename(); if (!filename) return undefined; return this.settings.mapCache.getMapPreview(this.settings.map.type, this.settings.map.map, filename); } updatePreview() { - // Don't overwrite the preview if it's been manually set. - if (!this.isDefault) - return; - if (!this.settings.map.map) { this.value = undefined; return; } // This handles "random" map type (mostly for convenience). let mapPath = basename(this.settings.map.map); this.value = this.getPreviewForSubtype(mapPath, this.settings.biome.biome) || this.getLandscapePreview() || this.getPreviewForSubtype(mapPath, this.settings.daytime.value) || this.settings.mapCache.getMapPreview(this.settings.map.type, this.settings.map.map); } - - setCustom(preview) - { - this.isDefault = false; - this.value = preview; - } }; Index: ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/MatchID.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/MatchID.js (revision 26392) +++ ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/MatchID.js (revision 26393) @@ -1,23 +1,7 @@ GameSettings.prototype.Attributes.MatchID = class MatchID extends GameSetting { - init() + onFinalizeAttributes(attribs) { - this.matchID = 0; - } - - toInitAttributes(attribs) - { - attribs.matchID = this.matchID; - } - - fromInitAttributes(attribs) - { - if (attribs.matchID !== undefined) - this.matchID = attribs.matchID; - } - - pickRandomItems() - { - this.matchID = Engine.GetMatchID(); + attribs.matchID = Engine.GetMatchID(); } }; Index: ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/PlayerName.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/PlayerName.js (revision 26392) +++ ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/PlayerName.js (revision 26393) @@ -1,116 +1,138 @@ /** * Stores in-game names for all players. * * NB: the regular gamesetup has a particular handling of this setting. - * The names are loaded from the map, but the GUI also show playernames - * and forces them when starting the game. - * This is therefore just handling map-defined names & AI random bot names. + * The names are loaded from the map, but the GUI also show playernames. + * Force these at the start of the match. */ GameSettings.prototype.Attributes.PlayerName = class PlayerName extends GameSetting { init() { // NB: watchers aren't auto-triggered when modifying array elements. this.values = []; this.settings.playerCount.watch(() => this.maybeUpdate(), ["nbPlayers"]); this.settings.map.watch(() => this.onMapChange(), ["map"]); } toInitAttributes(attribs) { if (!attribs.settings.PlayerData) attribs.settings.PlayerData = []; while (attribs.settings.PlayerData.length < this.values.length) attribs.settings.PlayerData.push({}); for (let i in this.values) if (this.values[i]) attribs.settings.PlayerData[i].Name = this.values[i]; } + fromInitAttributes(attribs) + { + if (!this.getLegacySetting(attribs, "PlayerData")) + return; + const pData = this.getLegacySetting(attribs, "PlayerData"); + if (this.values.length < pData.length) + this._resize(pData.length); + for (const i in pData) + if (pData[i] && pData[i].Name !== undefined) + { + this.values[i] = pData[i].Name; + this.trigger("values"); + } + } + _resize(nb) { while (this.values.length > nb) this.values.pop(); while (this.values.length < nb) this.values.push(undefined); } onMapChange() { // Reset. this._resize(0); this.maybeUpdate(); } maybeUpdate() { this._resize(this.settings.playerCount.nbPlayers); this.values.forEach((_, i) => this._set(i)); this.trigger("values"); } /** * Pick bot names. */ pickRandomItems() { let picked = false; for (let i in this.values) { if (!!this.values[i] && this.values[i] !== g_Settings.PlayerDefaults[+i + 1].Name) continue; let ai = this.settings.playerAI.values[i]; if (!ai) continue; let civ = this.settings.playerCiv.values[i]; if (!civ || civ == "random") continue; picked = true; // Pick one of the available botnames for the chosen civ // Determine botnames let chosenName = pickRandom(this.settings.civData[civ].AINames); if (!this.settings.isNetworked) chosenName = translate(chosenName); // Count how many players use the chosenName let usedName = this.values.filter(oName => oName && oName.indexOf(chosenName) !== -1).length; this.values[i] = usedName ? sprintf(this.RomanLabel, { "playerName": chosenName, "romanNumber": this.RomanNumbers[usedName + 1] }) : chosenName; } if (picked) this.trigger("values"); return picked; } + onFinalizeAttributes(attribs, playerAssignments) + { + // Replace client player names with the real players. + for (const guid in playerAssignments) + if (playerAssignments[guid].player !== -1) + attribs.settings.PlayerData[playerAssignments[guid].player -1].Name = playerAssignments[guid].name; + } + _getMapData(i) { let data = this.settings.map.data; if (!data || !data.settings || !data.settings.PlayerData) return undefined; if (data.settings.PlayerData.length <= i) return undefined; return data.settings.PlayerData[i].Name; } _set(playerIndex) { this.values[playerIndex] = this._getMapData(playerIndex) || g_Settings && g_Settings.PlayerDefaults[playerIndex + 1].Name || ""; } }; GameSettings.prototype.Attributes.PlayerName.prototype.RomanLabel = translate("%(playerName)s %(romanNumber)s"); GameSettings.prototype.Attributes.PlayerName.prototype.RomanNumbers = [undefined, "I", "II", "III", "IV", "V", "VI", "VII", "VIII"]; Index: ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/Rating.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/Rating.js (revision 26392) +++ ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/Rating.js (revision 26393) @@ -1,39 +1,44 @@ GameSettings.prototype.Attributes.Rating = class Rating extends GameSetting { init() { this.hasXmppClient = Engine.HasXmppClient(); this.settings.playerCount.watch(() => this.maybeUpdate(), ["nbPlayers"]); this.settings.cheats.watch(() => this.maybeUpdate(), ["enabled"]); this.maybeUpdate(); } toInitAttributes(attribs) { if (this.available) attribs.settings.RatingEnabled = this.enabled; } fromInitAttributes(attribs) { if (this.getLegacySetting(attribs, "RatingEnabled") !== undefined) { this.available = this.hasXmppClient && this.settings.playerCount.nbPlayers === 2; this.enabled = this.available && !!this.getLegacySetting(attribs, "RatingEnabled"); } } setEnabled(enabled) { this.enabled = this.available && enabled; } maybeUpdate() { // This setting is activated by default if it's possible. this.available = this.hasXmppClient && this.settings.playerCount.nbPlayers === 2 && !this.settings.cheats.enabled; this.enabled = this.available; } + + onFinalizeAttributes() + { + Engine.SetRankedGame(this.enabled); + } }; Index: ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/RegicideGarrison.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/RegicideGarrison.js (revision 26392) +++ ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/RegicideGarrison.js (revision 26393) @@ -1,37 +1,38 @@ GameSettings.prototype.Attributes.RegicideGarrison = class RegicideGarrison extends GameSetting { init() { this.setEnabled(false); this.settings.victoryConditions.watch(() => this.maybeUpdate(), ["active"]); this.settings.map.watch(() => this.onMapChange(), ["map"]); } toInitAttributes(attribs) { - attribs.settings.RegicideGarrison = this.enabled; + if (this.available) + attribs.settings.RegicideGarrison = this.enabled; } fromInitAttributes(attribs) { this.enabled = !!this.getLegacySetting(attribs, "RegicideGarrison"); } onMapChange() { if (this.settings.map.type != "scenario") return; this.setEnabled(!!this.getMapSetting("RegicideGarrison")); } setEnabled(enabled) { this.available = this.settings.victoryConditions.active.has("regicide"); this.enabled = (enabled && this.available); } maybeUpdate() { this.setEnabled(this.enabled); } }; Index: ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/SeaLevelRise.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/SeaLevelRise.js (revision 26392) +++ ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/SeaLevelRise.js (revision 26393) @@ -1,43 +1,43 @@ GameSettings.prototype.Attributes.SeaLevelRise = class SeaLevelRise extends GameSetting { init() { this.min = undefined; this.max = undefined; this.value = undefined; this.settings.map.watch(() => this.onMapChange(), ["map"]); } toInitAttributes(attribs) { - if (this.value) + if (this.value !== undefined) attribs.settings.SeaLevelRiseTime = this.value; } fromInitAttributes(attribs) { if (!!this.getLegacySetting(attribs, "SeaLevelRiseTime")) this.setValue(this.getLegacySetting(attribs, "SeaLevelRiseTime")); } onMapChange() { if (!this.getMapSetting("SeaLevelRise")) { this.value = undefined; return; } let mapData = this.settings.map.data; this.min = mapData.settings.SeaLevelRise.Min; this.max = mapData.settings.SeaLevelRise.Max; this.value = mapData.settings.SeaLevelRise.Default; } setValue(val) { if (!this.getMapSetting("SeaLevelRise")) this.value = undefined; else this.value = Math.max(this.min, Math.min(this.max, Math.round(val))); } }; Index: ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/Seeds.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/Seeds.js (revision 26392) +++ ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/Seeds.js (revision 26393) @@ -1,30 +1,39 @@ GameSettings.prototype.Attributes.Seeds = class Seeds extends GameSetting { init() { - this.seed = 0; - this.AIseed = 0; + this.seed = "random"; + this.AIseed = "random"; } toInitAttributes(attribs) { - // Seed is used for map generation and simulation. - attribs.settings.Seed = this.seed; - attribs.settings.AISeed = this.AIseed; + attribs.settings.Seed = this.seed == "random" ? this.seed : +this.seed; + attribs.settings.AISeed = this.AIseed == "random" ? this.AIseed : +this.AIseed; } fromInitAttributes(attribs) { - // Seed is used for map generation and simulation. if (this.getLegacySetting(attribs, "Seed") !== undefined) this.seed = this.getLegacySetting(attribs, "Seed"); if (this.getLegacySetting(attribs, "AISeed") !== undefined) this.AIseed = this.getLegacySetting(attribs, "AISeed"); } pickRandomItems() { - this.seed = randIntExclusive(0, Math.pow(2, 32)); - this.AIseed = randIntExclusive(0, Math.pow(2, 32)); + let picked = false; + if (this.seed === "random") + { + this.seed = randIntExclusive(0, Math.pow(2, 32)); + picked = true; + } + + if (this.AIseed === "random") + { + this.AIseed = randIntExclusive(0, Math.pow(2, 32)); + picked = true; + } + return picked; } }; Index: ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/StartingCamera.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/StartingCamera.js (revision 26392) +++ ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/StartingCamera.js (revision 26393) @@ -1,44 +1,55 @@ /** * For compatibility reasons, this loads the per-player StartingCamera from the map data * In general, this is probably better handled by map triggers or the default camera placement. * This doesn't have a GUI setting. */ GameSettings.prototype.Attributes.StartingCamera = class StartingCamera extends GameSetting { init() { this.values = []; this.settings.map.watch(() => this.onMapChange(), ["map"]); } toInitAttributes(attribs) { if (!attribs.settings.PlayerData) attribs.settings.PlayerData = []; while (attribs.settings.PlayerData.length < this.values.length) attribs.settings.PlayerData.push({}); - for (let i in this.values) + for (const i in this.values) if (this.values[i]) attribs.settings.PlayerData[i].StartingCamera = this.values[i]; } - /** - * Exceptionally, this setting has no Deserialize: it's entirely determined by the map - */ + fromInitAttributes(attribs) + { + if (!this.getLegacySetting(attribs, "PlayerData")) + return; + const pData = this.getLegacySetting(attribs, "PlayerData"); + if (this.values.length < pData.length) + this._resize(pData.length); + for (const i in pData) + if (pData[i] && pData[i].StartingCamera !== undefined) + { + this.values[i] = pData[i].StartingCamera; + this.trigger("values"); + } + } _resize(nb) { while (this.values.length > nb) this.values.pop(); while (this.values.length < nb) this.values.push(undefined); } onMapChange() { let pData = this.getMapSetting("PlayerData"); this._resize(pData?.length || 0); for (let i in pData) this.values[i] = pData?.[i]?.StartingCamera; } }; Index: ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/TeamPlacement.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/TeamPlacement.js (revision 26392) +++ ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/TeamPlacement.js (revision 26393) @@ -1,75 +1,75 @@ GameSettings.prototype.Attributes.TeamPlacement = class TeamPlacement extends GameSetting { init() { this.available = undefined; this.value = undefined; this.settings.map.watch(() => this.onMapChange(), ["map"]); } toInitAttributes(attribs) { - if (this.value) + if (this.value !== undefined) attribs.settings.TeamPlacement = this.value; } fromInitAttributes(attribs) { if (!!this.getLegacySetting(attribs, "TeamPlacement")) this.value = this.getLegacySetting(attribs, "TeamPlacement"); } onMapChange() { if (!this.getMapSetting("TeamPlacements")) { this.value = undefined; this.available = undefined; return; } // TODO: should probably validate that they fit one of the known schemes. this.available = this.getMapSetting("TeamPlacements"); this.value = "random"; } setValue(val) { this.value = val; } pickRandomItems() { // If the map is random, we need to wait until it is selected. if (this.settings.map.map === "random") return true; if (this.value !== "random") return false; this.value = pickRandom(this.available).Id; return true; } }; GameSettings.prototype.Attributes.TeamPlacement.prototype.StartingPositions = [ { "Id": "radial", "Name": translateWithContext("team placement", "Circle"), "Description": translate("Allied players are grouped and placed with opposing players on one circle spanning the map.") }, { "Id": "line", "Name": translateWithContext("team placement", "Line"), "Description": translate("Allied players are placed in a linear pattern."), }, { "Id": "randomGroup", "Name": translateWithContext("team placement", "Random Group"), "Description": translate("Allied players are grouped, but otherwise placed randomly on the map."), }, { "Id": "stronghold", "Name": translateWithContext("team placement", "Stronghold"), "Description": translate("Allied players are grouped in one random place of the map."), } ]; Index: ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/TriggerScripts.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/TriggerScripts.js (revision 26392) +++ ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/TriggerScripts.js (revision 26393) @@ -1,42 +1,52 @@ GameSettings.prototype.Attributes.TriggerScripts = class TriggerScripts extends GameSetting { init() { - this.victory = new Set(); - this.map = new Set(); + this.customScripts = new Set(); + this.victoryScripts = new Set(); + this.mapScripts = new Set(); this.settings.map.watch(() => this.updateMapScripts(), ["map"]); this.settings.victoryConditions.watch(() => this.updateVictoryScripts(), ["active"]); } toInitAttributes(attribs) { - let scripts = new Set(this.victory); - for (let elem of this.map) - scripts.add(elem); - attribs.settings.TriggerScripts = Array.from(scripts); + attribs.settings.TriggerScripts = Array.from(this.customScripts); } - /** - * Exceptionally, this setting has no Deserialize: it's entirely determined from other settings. - */ + fromInitAttributes(attribs) + { + if (!!this.getLegacySetting(attribs, "TriggerScripts")) + this.customScripts = new Set(this.getLegacySetting(attribs, "TriggerScripts")); + } updateVictoryScripts() { let setting = this.settings.victoryConditions; let scripts = new Set(); for (let cond of setting.active) setting.conditions[cond].Scripts.forEach(script => scripts.add(script)); - this.victory = scripts; + this.victoryScripts = scripts; } updateMapScripts() { if (!this.settings.map.data || !this.settings.map.data.settings || !this.settings.map.data.settings.TriggerScripts) { - this.map = new Set(); + this.mapScripts = new Set(); return; } - this.map = new Set(this.settings.map.data.settings.TriggerScripts); + this.mapScripts = new Set(this.settings.map.data.settings.TriggerScripts); + } + + onFinalizeAttributes(attribs) + { + const scripts = this.customScripts; + for (const elem of this.victoryScripts) + scripts.add(elem); + for (const elem of this.mapScripts) + scripts.add(elem); + attribs.settings.TriggerScripts = Array.from(scripts); } }; Index: ps/trunk/binaries/data/mods/public/gui/gamesetup/Controllers/GameSettingsController.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesetup/Controllers/GameSettingsController.js (revision 26392) +++ ps/trunk/binaries/data/mods/public/gui/gamesetup/Controllers/GameSettingsController.js (revision 26393) @@ -1,305 +1,305 @@ /** * Controller for the GUI handling of gamesettings. */ class GameSettingsController { constructor(setupWindow, netMessages, playerAssignmentsController, mapCache) { this.setupWindow = setupWindow; this.mapCache = mapCache; this.persistentMatchSettings = new PersistentMatchSettings(g_IsNetworked); this.guiData = new GameSettingsGuiData(); // When joining a game, the complete set of attributes // may not have been received yet. this.loading = true; // If this is true, the ready controller won't reset readiness. // TODO: ideally the ready controller would be somewhat independent from this one, // possibly by listening to gamesetup messages itself. this.gameStarted = false; this.updateLayoutHandlers = new Set(); this.settingsChangeHandlers = new Set(); this.loadingChangeHandlers = new Set(); this.settingsLoadedHandlers = new Set(); setupWindow.registerLoadHandler(this.onLoad.bind(this)); setupWindow.registerGetHotloadDataHandler(this.onGetHotloadData.bind(this)); setupWindow.registerClosePageHandler(this.onClose.bind(this)); if (g_IsNetworked) { if (g_IsController) playerAssignmentsController.registerClientJoinHandler(this.onClientJoin.bind(this)); else // In MP, the host launches the game and switches right away, // clients switch when they receive the appropriate message. netMessages.registerNetMessageHandler("start", this.switchToLoadingPage.bind(this)); netMessages.registerNetMessageHandler("gamesetup", this.onGamesetupMessage.bind(this)); } } /** * @param handler will be called when the layout needs to be updated. */ registerUpdateLayoutHandler(handler) { this.updateLayoutHandlers.add(handler); } /** * @param handler will be called when any setting change. * (this isn't exactly what happens but the behaviour should be similar). */ registerSettingsChangeHandler(handler) { this.settingsChangeHandlers.add(handler); } /** * @param handler will be called when the 'loading' state change. */ registerLoadingChangeHandler(handler) { this.loadingChangeHandlers.add(handler); } /** * @param handler will be called when the initial settings have been loaded. */ registerSettingsLoadedHandler(handler) { this.settingsLoadedHandlers.add(handler); } onLoad(initData, hotloadData) { if (hotloadData) this.parseSettings(hotloadData.initAttributes); else if (g_IsController && (initData?.gameSettings || this.persistentMatchSettings.enabled)) { // Allow opting-in to persistence when sending initial data (though default off) if (initData?.gameSettings) this.persistentMatchSettings.enabled = !!initData.gameSettings?.usePersistence; const settings = initData?.gameSettings || this.persistentMatchSettings.loadFile(); if (settings) this.parseSettings(settings); } // If the new settings led to AI & players conflict, remove the AI. for (const guid in g_PlayerAssignments) if (g_PlayerAssignments[guid].player !== -1 && g_GameSettings.playerAI.get(g_PlayerAssignments[guid].player - 1)) g_GameSettings.playerAI.set(g_PlayerAssignments[guid].player - 1, undefined); for (const handler of this.settingsLoadedHandlers) handler(); this.updateLayout(); this.setNetworkInitAttributes(); // If we are the controller, we are done loading. if (hotloadData || !g_IsNetworked || g_IsController) this.setLoading(false); } onClientJoin() { /** * A note on network synchronization: * The net server does not keep the current state of attributes, * nor does it act like a message queue, so a new client * will only receive updates after they've joined. * In particular, new joiners start with no information, * so the controller must first send them a complete copy of the settings. * However, messages could be in-flight towards the controller, * but the new client may never receive these or have already received them, * leading to an ordering issue that might desync the new client. * * The simplest solution is to have the (single) controller * act as the single source of truth. Any other message must * first go through the controller, which will send updates. * This enforces the ordering of the controller. * In practical terms, if e.g. players controlling their own civ is implemented, * the message will need to be ignored by everyone but the controller, * and the controller will need to send an update once it rejects/accepts the changes, * which will then update the other clients. * Of course, the original client GUI may want to temporarily show a different state. * Note that the final attributes are sent on game start anyways, so any * synchronization issue that might happen at that point can be resolved. */ Engine.SendGameSetupMessage({ "type": "initial-update", "initAttribs": this.getSettings() }); } onGetHotloadData(object) { object.initAttributes = this.getSettings(); } onGamesetupMessage(message) { // For now, the controller only can send updates, so no need to listen to messages. if (!message.data || g_IsController) return; if (message.data.type !== "update" && message.data.type !== "initial-update") { error("Unknown message type " + message.data.type); return; } if (message.data.type === "initial-update") { // Ignore initial updates if we've already received settings. if (!this.loading) return; this.setLoading(false); } this.parseSettings(message.data.initAttribs); // This assumes that messages aren't sent spuriously without changes // (which is generally fair), but technically it would be good // to check if the new data is different from the previous data. for (let handler of this.settingsChangeHandlers) handler(); } /** * Returns the InitAttributes, augmented by GUI-specific data. */ getSettings() { let ret = g_GameSettings.toInitAttributes(); ret.guiData = this.guiData.Serialize(); return ret; } /** * Parse the following settings. */ parseSettings(settings) { if (settings.guiData) this.guiData.Deserialize(settings.guiData); g_GameSettings.fromInitAttributes(settings); } setLoading(loading) { if (this.loading === loading) return; this.loading = loading; for (let handler of this.loadingChangeHandlers) handler(loading); } /** * This should be called whenever the GUI layout needs to be updated. * Triggers on the next GUI tick to avoid un-necessary layout. */ updateLayout() { if (this.layoutTimer) return; this.layoutTimer = setTimeout(() => { for (let handler of this.updateLayoutHandlers) handler(); delete this.layoutTimer; }, 0); } /** * This function is to be called when a GUI control has initiated a value change. * * To avoid an infinite loop, do not call this function when a game setup message was * received and the data had only been modified deterministically. * * This is run on a timer to avoid flooding the network with messages, * e.g. when modifying a slider. */ setNetworkInitAttributes() { for (let handler of this.settingsChangeHandlers) handler(); if (g_IsNetworked && this.timer === undefined) this.timer = setTimeout(this.setNetworkInitAttributesImmediately.bind(this), this.Timeout); } setNetworkInitAttributesImmediately() { if (this.timer) { clearTimeout(this.timer); delete this.timer; } // See note in onClientJoin on network synchronization. if (g_IsController) Engine.SendGameSetupMessage({ "type": "update", "initAttribs": this.getSettings() }); } /** * Cheat prevention: * * 1. Ensure that the host cannot start the game unless all clients agreed on the game settings using the ready system. * * TODO: * 2. Ensure that the host cannot start the game with InitAttributes different from the agreed ones. * This may be achieved by: * - Determining the seed collectively. * - passing the agreed game settings to the engine when starting the game instance * - rejecting new game settings from the server after the game launch event */ launchGame() { // Save the file before random settings are resolved. this.savePersistentMatchSettings(); // Mark the game as started so the readyController won't reset state. this.gameStarted = true; // This will resolve random settings & send game start messages. // TODO: this will trigger observers, which is somewhat wasteful. g_GameSettings.launchGame(g_PlayerAssignments); // Switch to the loading page right away, // the GUI will otherwise show the unrandomised settings. this.switchToLoadingPage(); } switchToLoadingPage(attributes) { Engine.SwitchGuiPage("page_loading.xml", { - "attribs": attributes?.initAttributes || g_GameSettings.toInitAttributes(), + "attribs": attributes?.initAttributes || g_GameSettings.finalizedAttributes, "playerAssignments": g_PlayerAssignments }); } onClose() { this.savePersistentMatchSettings(); } savePersistentMatchSettings() { if (g_IsController) // TODO: ought to only save a subset of settings. this.persistentMatchSettings.saveFile(this.getSettings()); } } /** * Wait (at most) this many milliseconds before sending network messages. */ GameSettingsController.prototype.Timeout = 400; Index: ps/trunk/binaries/data/mods/public/gui/loadgame/SavegameList.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/loadgame/SavegameList.js (revision 26392) +++ ps/trunk/binaries/data/mods/public/gui/loadgame/SavegameList.js (revision 26393) @@ -1,200 +1,200 @@ /** * This class obtains the list of savegames from the engine, * builds the list dependent on selected filters and sorting order. * * If the selected savegame changes, class instances that subscribed via * registerSelectionChangeHandler will have their onSelectionChange function * called with the relevant savegame data. */ class SavegameList { constructor(campaignRun) { this.savedGamesMetadata = []; this.selectionChangeHandlers = []; // If not null, only show games for the following campaign run // (campaign save-games are not shown by default). // Campaign games are saved in the same folder as regular ones, // as there is no strong reason to do otherwise (since games from different runs // need to be hidden from one another anyways, we need code to handle it). this.campaignRun = campaignRun; this.gameSelection = Engine.GetGUIObjectByName("gameSelection"); this.gameSelectionFeedback = Engine.GetGUIObjectByName("gameSelectionFeedback"); this.confirmButton = Engine.GetGUIObjectByName("confirmButton"); this.compatibilityFilter = Engine.GetGUIObjectByName("compatibilityFilter"); this.compatibilityFilter.onPress = () => { this.updateSavegameList(); }; this.initSavegameList(); } initSavegameList() { let engineInfo = Engine.GetEngineInfo(); this.gameSelection.onSelectionColumnChange = () => { this.updateSavegameList(); }; this.gameSelection.onMouseLeftDoubleClickItem = () => { this.confirmButton.onPress(); }; this.gameSelection.onSelectionChange = () => { let gameId = this.gameSelection.list_data[this.gameSelection.selected]; let metadata = this.savedGamesMetadata[this.gameSelection.selected]; let label = this.generateSavegameLabel(metadata, engineInfo); for (let handler of this.selectionChangeHandlers) handler.onSelectionChange(gameId, metadata, label); }; this.updateSavegameList(); } registerSelectionChangeHandler(selectionChangeHandler) { this.selectionChangeHandlers.push(selectionChangeHandler); } onSavegameListChange() { this.updateSavegameList(); // Allow subscribers (delete button) to update their press function in case // the list items changed but the selected index remained the same. this.gameSelection.onSelectionChange(); } selectFirst() { if (this.gameSelection.list.length) this.gameSelection.selected = 0; } updateSavegameList() { let savedGames = Engine.GetSavedGames(); // Get current game version and loaded mods let engineInfo = Engine.GetEngineInfo(); if (this.compatibilityFilter.checked) savedGames = savedGames.filter(game => { return this.isCompatibleSavegame(game.metadata, engineInfo) && this.campaignFilter(game.metadata, this.campaignRun); }); else if (this.campaignRun) savedGames = savedGames.filter(game => this.campaignFilter(game.metadata, this.campaignRun)); this.gameSelection.enabled = !!savedGames.length; this.gameSelectionFeedback.hidden = !!savedGames.length; let selectedGameId = this.gameSelection.list_data[this.gameSelection.selected]; // Save metadata for the detailed view this.savedGamesMetadata = savedGames.map(game => { game.metadata.id = game.id; return game.metadata; }); let sortKey = this.gameSelection.selected_column; let sortOrder = this.gameSelection.selected_column_order; this.savedGamesMetadata = this.savedGamesMetadata.sort((a, b) => { let cmpA, cmpB; switch (sortKey) { case 'date': cmpA = +a.time; cmpB = +b.time; break; case 'mapName': - cmpA = translate(a.initAttributes.settings.Name); - cmpB = translate(b.initAttributes.settings.Name); + cmpA = translate(a.initAttributes.settings.mapName); + cmpB = translate(b.initAttributes.settings.mapName); break; case 'mapType': cmpA = translateMapType(a.initAttributes.mapType); cmpB = translateMapType(b.initAttributes.mapType); break; case 'description': cmpA = a.description; cmpB = b.description; break; } if (cmpA < cmpB) return -sortOrder; else if (cmpA > cmpB) return +sortOrder; return 0; }); let list = this.savedGamesMetadata.map(metadata => { let isCompatible = this.isCompatibleSavegame(metadata, engineInfo) && this.campaignFilter(metadata, this.campaignRun); return { "date": this.generateSavegameDateString(metadata, engineInfo), - "mapName": compatibilityColor(translate(metadata.initAttributes.settings.Name), isCompatible), + "mapName": compatibilityColor(translate(metadata.initAttributes.settings.mapName), isCompatible), "mapType": compatibilityColor(translateMapType(metadata.initAttributes.mapType), isCompatible), "description": compatibilityColor(metadata.description, isCompatible) }; }); if (list.length) list = prepareForDropdown(list); this.gameSelection.list_date = list.date || []; this.gameSelection.list_mapName = list.mapName || []; this.gameSelection.list_mapType = list.mapType || []; this.gameSelection.list_description = list.description || []; // Change these last, otherwise crash this.gameSelection.list = this.savedGamesMetadata.map(metadata => 0); this.gameSelection.list_data = this.savedGamesMetadata.map(metadata => metadata.id); // Restore selection if the selected savegame still exists. // If the last savegame was deleted, or if it was hidden by the compatibility filter, select the new last item. let selectedGameIndex = this.savedGamesMetadata.findIndex(metadata => metadata.id == selectedGameId); if (selectedGameIndex != -1) this.gameSelection.selected = selectedGameIndex; else if (this.gameSelection.selected >= this.savedGamesMetadata.length) this.gameSelection.selected = this.savedGamesMetadata.length - 1; } campaignFilter(metadata, campaignRun) { if (!campaignRun) return !metadata.initAttributes.campaignData; if (metadata.initAttributes.campaignData) return metadata.initAttributes.campaignData.run == campaignRun; return false; } isCompatibleSavegame(metadata, engineInfo) { return engineInfo && metadata.engine_version && metadata.engine_version == engineInfo.engine_version && hasSameMods(metadata.mods, engineInfo.mods); } generateSavegameDateString(metadata, engineInfo) { return compatibilityColor( Engine.FormatMillisecondsIntoDateStringLocal(metadata.time * 1000, translate("yyyy-MM-dd HH:mm:ss")), this.isCompatibleSavegame(metadata, engineInfo)); } generateSavegameLabel(metadata, engineInfo) { if (!metadata) return undefined; return sprintf( metadata.description ? translate("%(dateString)s %(map)s - %(description)s") : translate("%(dateString)s %(map)s"), { "dateString": this.generateSavegameDateString(metadata, engineInfo), "map": metadata.initAttributes.map, "description": metadata.description || "" }); } } Index: ps/trunk/binaries/data/mods/public/gui/loading/TitleDisplay.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/loading/TitleDisplay.js (revision 26392) +++ ps/trunk/binaries/data/mods/public/gui/loading/TitleDisplay.js (revision 26393) @@ -1,17 +1,17 @@ /** * This class choses the title of the loading screen page. */ class TitleDisplay { constructor(data) { let loadingMapName = Engine.GetGUIObjectByName("loadingMapName"); loadingMapName.caption = sprintf( data.attribs.mapType == "random" ? this.Generating : this.Loading, - { "map": translate(data.attribs.settings.Name) }); + { "map": translate(data.attribs.settings.mapName) }); } } TitleDisplay.prototype.Generating = translate("Generating “%(map)s”"); TitleDisplay.prototype.Loading = translate("Loading “%(map)s”"); Index: ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_filters.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_filters.js (revision 26392) +++ ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_filters.js (revision 26393) @@ -1,308 +1,308 @@ /** * Allow to filter replays by duration in 15min / 30min intervals. */ var g_DurationFilterIntervals = [ { "min": -1, "max": -1 }, { "min": -1, "max": 15 }, { "min": 15, "max": 30 }, { "min": 30, "max": 45 }, { "min": 45, "max": 60 }, { "min": 60, "max": 90 }, { "min": 90, "max": 120 }, { "min": 120, "max": -1 } ]; /** * Allow to filter by population capacity. */ const g_PopulationCapacities = prepareForDropdown(g_Settings && g_Settings.PopulationCapacities); const g_WorldPopulationCapacities = prepareForDropdown(g_Settings && g_Settings.WorldPopulationCapacities); /** * Reloads the selectable values in the filters. The filters depend on g_Settings and g_Replays * (including its derivatives g_MapSizes, g_MapNames). */ function initFilters(filters) { if (filters && filters.compatibility !== undefined) Engine.GetGUIObjectByName("compatibilityFilter").checked = filters.compatibility; if (filters && filters.playernames) Engine.GetGUIObjectByName("playersFilter").caption = filters.playernames; initDateFilter(filters); initMapSizeFilter(filters); initMapNameFilter(filters); initPopCapFilter(filters); initDurationFilter(filters); initSingleplayerFilter(filters); initVictoryConditionFilter(filters); initRatedGamesFilter(filters); } /** * Allow to filter by month. Uses g_Replays. */ function initDateFilter(filters) { var months = g_Replays.map(replay => getReplayMonth(replay)); months = months.filter((month, index) => months.indexOf(month) == index).sort(); var dateTimeFilter = Engine.GetGUIObjectByName("dateTimeFilter"); dateTimeFilter.list = [translateWithContext("datetime", "Any")].concat(months); dateTimeFilter.list_data = [""].concat(months); if (filters && filters.date) dateTimeFilter.selected = dateTimeFilter.list_data.indexOf(filters.date); if (dateTimeFilter.selected == -1 || dateTimeFilter.selected >= dateTimeFilter.list.length) dateTimeFilter.selected = 0; } /** * Allow to filter by mapsize. Uses g_MapSizes. */ function initMapSizeFilter(filters) { var mapSizeFilter = Engine.GetGUIObjectByName("mapSizeFilter"); mapSizeFilter.list = [translateWithContext("map size", "Any")].concat(g_MapSizes.Name); mapSizeFilter.list_data = [-1].concat(g_MapSizes.Tiles); if (filters && filters.mapSize) mapSizeFilter.selected = mapSizeFilter.list_data.indexOf(filters.mapSize); if (mapSizeFilter.selected == -1 || mapSizeFilter.selected >= mapSizeFilter.list.length) mapSizeFilter.selected = 0; } /** * Allow to filter by mapname. Uses g_MapNames. */ function initMapNameFilter(filters) { var mapNameFilter = Engine.GetGUIObjectByName("mapNameFilter"); mapNameFilter.list = [translateWithContext("map name", "Any")].concat(g_MapNames.map(name => translate(name))); mapNameFilter.list_data = [""].concat(g_MapNames); if (filters && filters.mapName) mapNameFilter.selected = mapNameFilter.list_data.indexOf(filters.mapName); if (mapNameFilter.selected == -1 || mapNameFilter.selected >= mapNameFilter.list.length) mapNameFilter.selected = 0; } /** * Allow to filter by population capacity. */ function initPopCapFilter(filters) { var populationFilter = Engine.GetGUIObjectByName("populationFilter"); populationFilter.list = [translateWithContext("population capacity", "Any")].concat(g_PopulationCapacities.Title); populationFilter.list_data = [""].concat(g_PopulationCapacities.Population); if (filters && filters.popCap) populationFilter.selected = populationFilter.list_data.indexOf(filters.popCap); if (populationFilter.selected == -1 || populationFilter.selected >= populationFilter.list.length) populationFilter.selected = 0; } /** * Allow to filter by game duration. Uses g_DurationFilterIntervals. */ function initDurationFilter(filters) { var durationFilter = Engine.GetGUIObjectByName("durationFilter"); durationFilter.list = g_DurationFilterIntervals.map((interval, index) => { if (index == 0) return translateWithContext("duration", "Any"); if (index == 1) // Translation: Shorter duration than max minutes. return sprintf(translatePluralWithContext("duration filter", "< %(max)s min", "< %(max)s min", interval.max), interval); if (index == g_DurationFilterIntervals.length - 1) // Translation: Longer duration than min minutes. return sprintf(translatePluralWithContext("duration filter", "> %(min)s min", "> %(min)s min", interval.min), interval); // Translation: Duration between min and max minutes. return sprintf(translateWithContext("duration filter", "%(min)s - %(max)s min"), interval); }); durationFilter.list_data = g_DurationFilterIntervals.map((interval, index) => index); if (filters && filters.duration) durationFilter.selected = durationFilter.list_data.indexOf(filters.duration); if (durationFilter.selected == -1 || durationFilter.selected >= g_DurationFilterIntervals.length) durationFilter.selected = 0; } function initSingleplayerFilter(filters) { let singleplayerFilter = Engine.GetGUIObjectByName("singleplayerFilter"); singleplayerFilter.list = [ translateWithContext("replay filter", "Any"), translateWithContext("replay filter", "Single-player"), translateWithContext("replay filter", "Multiplayer"), translateWithContext("replay filter", "Campaigns") ]; singleplayerFilter.list_data = ["", "Single-player", "Multiplayer", "Campaigns"]; if (filters && filters.singleplayer) singleplayerFilter.selected = singleplayerFilter.list_data.indexOf(filters.singleplayer); if (singleplayerFilter.selected < 0 || singleplayerFilter.selected >= singleplayerFilter.list.length) singleplayerFilter.selected = 0; } function initVictoryConditionFilter(filters) { let victoryConditionFilter = Engine.GetGUIObjectByName("victoryConditionFilter"); victoryConditionFilter.list = [translate("Any victory condition")].concat(g_VictoryConditions.map(victoryCondition => translateVictoryCondition(victoryCondition.Name))); victoryConditionFilter.list_data = [""].concat(g_VictoryConditions.map(victoryCondition => victoryCondition.Name)); if (filters && filters.victoryCondition) victoryConditionFilter.selected = victoryConditionFilter.list_data.indexOf(filters.victoryCondition); if (victoryConditionFilter.selected < 0 || victoryConditionFilter.selected >= victoryConditionFilter.list.length) victoryConditionFilter.selected = 0; } function initRatedGamesFilter(filters) { let ratedGamesFilter = Engine.GetGUIObjectByName("ratedGamesFilter"); ratedGamesFilter.list = [translate("Rated and unrated games"), translate("Rated games"), translate("Unrated games")]; ratedGamesFilter.list_data = ["", "rated", "not rated"]; if (filters && filters.ratedGames) ratedGamesFilter.selected = ratedGamesFilter.list_data.indexOf(filters.ratedGames); if (ratedGamesFilter.selected < 0 || ratedGamesFilter.selected >= ratedGamesFilter.list.length) ratedGamesFilter.selected = 0; } /** * Initializes g_ReplaysFiltered with replays that are not filtered out and sort it. */ 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) { 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; } if (cmpA < cmpB) return -sortOrder; else if (cmpA > cmpB) return +sortOrder; return 0; }); } /** * Decides whether the replay should be listed. * * @returns {bool} - true if replay should be visible */ function filterReplay(replay) { // Check for compatibility first (most likely to filter) let compatibilityFilter = Engine.GetGUIObjectByName("compatibilityFilter"); if (compatibilityFilter.checked && !isReplayCompatible(replay)) return false; // Filter by single-player or multiplayer. let singleplayerFilter = Engine.GetGUIObjectByName("singleplayerFilter"); let selectedSingleplayerFilter = singleplayerFilter.list_data[singleplayerFilter.selected] || ""; if (selectedSingleplayerFilter == "Campaigns" && !replay.isCampaign || selectedSingleplayerFilter == "Single-player" && (replay.isMultiplayer || replay.isCampaign) || selectedSingleplayerFilter == "Multiplayer" && (!replay.isMultiplayer || replay.isCampaign)) return false; // Filter by victory condition let victoryConditionFilter = Engine.GetGUIObjectByName("victoryConditionFilter"); if (victoryConditionFilter.selected > 0 && replay.attribs.settings.VictoryConditions.indexOf(victoryConditionFilter.list_data[victoryConditionFilter.selected]) == -1) return false; // Filter by rating let ratedGamesFilter = Engine.GetGUIObjectByName("ratedGamesFilter"); let selectedRatedGamesFilter = ratedGamesFilter.list_data[ratedGamesFilter.selected] || ""; if (selectedRatedGamesFilter == "rated" && !replay.isRated || selectedRatedGamesFilter == "not rated" && replay.isRated) return false; // Filter date/time (select a month) let dateTimeFilter = Engine.GetGUIObjectByName("dateTimeFilter"); if (dateTimeFilter.selected > 0 && getReplayMonth(replay) != dateTimeFilter.list_data[dateTimeFilter.selected]) return false; // Filter by playernames let playersFilter = Engine.GetGUIObjectByName("playersFilter"); let keywords = playersFilter.caption.toLowerCase().split(" "); if (keywords.length) { // We just check if all typed words are somewhere in the playerlist of that replay let playerList = replay.attribs.settings.PlayerData.map(player => player ? player.Name : "").join(" ").toLowerCase(); if (!keywords.every(keyword => playerList.indexOf(keyword) != -1)) return false; } // Filter by map name let mapNameFilter = Engine.GetGUIObjectByName("mapNameFilter"); - if (mapNameFilter.selected > 0 && replay.attribs.settings.Name != mapNameFilter.list_data[mapNameFilter.selected]) + if (mapNameFilter.selected > 0 && replay.attribs.settings.mapName != mapNameFilter.list_data[mapNameFilter.selected]) return false; // Filter by map size let mapSizeFilter = Engine.GetGUIObjectByName("mapSizeFilter"); if (mapSizeFilter.selected > 0 && replay.attribs.settings.Size != mapSizeFilter.list_data[mapSizeFilter.selected]) return false; // Filter by population capacity let populationFilter = Engine.GetGUIObjectByName("populationFilter"); if (populationFilter.selected > 0 && replay.attribs.settings.PopulationCap != populationFilter.list_data[populationFilter.selected]) return false; // Filter by game duration let durationFilter = Engine.GetGUIObjectByName("durationFilter"); if (durationFilter.selected > 0) { let interval = g_DurationFilterIntervals[durationFilter.selected]; if ((interval.min > -1 && replay.duration < interval.min * 60) || (interval.max > -1 && replay.duration > interval.max * 60)) return false; } return true; } Index: ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_menu.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_menu.js (revision 26392) +++ ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_menu.js (revision 26393) @@ -1,366 +1,366 @@ /** * Used for checking replay compatibility. */ const g_EngineInfo = Engine.GetEngineInfo(); /** * Needed for formatPlayerInfo to show the player civs in the details. */ const g_CivData = loadCivData(false, false); /** * Used for creating the mapsize filter. */ const g_MapSizes = prepareForDropdown(g_Settings && g_Settings.MapSizes); /** * All replays found in the directory. */ var g_Replays = []; /** * List of replays after applying the display filter. */ var g_ReplaysFiltered = []; /** * Array of unique usernames of all replays. Used for autocompleting usernames. */ var g_Playernames = []; /** * Sorted list of unique maptitles. Used by mapfilter. */ var g_MapNames = []; /** * Sorted list of the victory conditions occuring in the replays */ var g_VictoryConditions = g_Settings && g_Settings.VictoryConditions; /** * Directory name of the currently selected replay. Used to restore the selection after changing filters. */ var g_SelectedReplayDirectory = ""; /** * Skip duplicate expensive GUI updates before init is complete. */ var g_ReplaysLoaded = false; /** * Remember last viewed summary panel and charts. */ var g_SummarySelection; var g_MapCache = new MapCache(); /** * Initializes globals, loads replays and displays the list. */ function init(data) { if (!g_Settings) { Engine.SwitchGuiPage("page_pregame.xml"); return; } g_SummarySelection = data && data.summarySelection; loadReplays(data && data.replaySelectionData, false); if (!g_Replays) { Engine.SwitchGuiPage("page_pregame.xml"); return; } initHotkeyTooltips(); displayReplayList(); } /** * Store the list of replays loaded in C++ in g_Replays. * Check timestamp and compatibility and extract g_Playernames, g_MapNames, g_VictoryConditions. * Restore selected filters and item. * @param replaySelectionData - Currently selected filters and item to be restored after the loading. * @param compareFiles - If true, compares files briefly (which might be slow with optical harddrives), * otherwise blindly trusts the replay cache. */ function loadReplays(replaySelectionData, compareFiles) { g_Replays = Engine.GetReplays(compareFiles); if (!g_Replays) return; g_Playernames = []; for (let replay of g_Replays) { let nonAIPlayers = 0; // Check replay for compatibility replay.isCompatible = isReplayCompatible(replay); sanitizeInitAttributes(replay.attribs); // Extract map names - if (g_MapNames.indexOf(replay.attribs.settings.Name) == -1 && replay.attribs.settings.Name != "") - g_MapNames.push(replay.attribs.settings.Name); + if (g_MapNames.indexOf(replay.attribs.settings.mapName) == -1 && replay.attribs.settings.mapName != "") + g_MapNames.push(replay.attribs.settings.mapName); // Extract playernames for (let playerData of replay.attribs.settings.PlayerData) { if (!playerData || playerData.AI) continue; // Remove rating from nick let playername = playerData.Name; let ratingStart = playername.indexOf(" ("); if (ratingStart != -1) playername = playername.substr(0, ratingStart); if (g_Playernames.indexOf(playername) == -1) g_Playernames.push(playername); ++nonAIPlayers; } replay.isMultiplayer = nonAIPlayers > 1; if (replay.attribs.campaignData) replay.isCampaign = true; replay.isRated = nonAIPlayers == 2 && replay.attribs.settings.PlayerData.length == 2 && replay.attribs.settings.RatingEnabled; } g_MapNames.sort(); // Reload filters (since they depend on g_Replays and its derivatives) initFilters(replaySelectionData && replaySelectionData.filters); // Restore user selection if (replaySelectionData) { if (replaySelectionData.directory) g_SelectedReplayDirectory = replaySelectionData.directory; let replaySelection = Engine.GetGUIObjectByName("replaySelection"); if (replaySelectionData.column) replaySelection.selected_column = replaySelectionData.column; if (replaySelectionData.columnOrder) replaySelection.selected_column_order = replaySelectionData.columnOrder; } g_ReplaysLoaded = true; } /** * We may encounter malformed replays. */ function sanitizeInitAttributes(attribs) { if (!attribs.settings) attribs.settings = {}; if (!attribs.settings.Size) attribs.settings.Size = -1; - if (!attribs.settings.Name) - attribs.settings.Name = ""; + if (!attribs.settings.mapName) + attribs.settings.mapName = ""; if (!attribs.settings.PlayerData) attribs.settings.PlayerData = []; if (!attribs.settings.PopulationCap) attribs.settings.PopulationCap = 300; if (!attribs.mapType) attribs.mapType = "skirmish"; // Remove gaia if (attribs.settings.PlayerData.length && attribs.settings.PlayerData[0] == null) attribs.settings.PlayerData.shift(); attribs.settings.PlayerData.forEach((pData, index) => { if (!pData.Name) pData.Name = ""; }); } function initHotkeyTooltips() { Engine.GetGUIObjectByName("playersFilter").tooltip = translate("Filter replays by typing one or more, partial or complete player names.") + " " + colorizeAutocompleteHotkey(); let deleteTooltip = colorizeHotkey( translate("Delete the selected replay using %(hotkey)s."), "session.savedgames.delete"); if (deleteTooltip) deleteTooltip += colorizeHotkey( "\n" + translate("Hold %(hotkey)s to skip the confirmation dialog while deleting."), "session.savedgames.noconfirmation"); Engine.GetGUIObjectByName("deleteReplayButton").tooltip = deleteTooltip; } /** * Filter g_Replays, fill the GUI list with that data and show the description of the current replay. */ function displayReplayList() { if (!g_ReplaysLoaded) return; // Remember previously selected replay var replaySelection = Engine.GetGUIObjectByName("replaySelection"); if (replaySelection.selected != -1) g_SelectedReplayDirectory = g_ReplaysFiltered[replaySelection.selected].directory; filterReplays(); var list = g_ReplaysFiltered.map(replay => { let works = replay.isCompatible; return { "directories": replay.directory, "months": compatibilityColor(getReplayDateTime(replay), works), "popCaps": compatibilityColor(translatePopulationCapacity(replay.attribs.settings.PopulationCap, !!replay.attribs.settings.WorldPopulation), works), "mapNames": compatibilityColor(getReplayMapName(replay), works), "mapSizes": compatibilityColor(translateMapSize(replay.attribs.settings.Size), works), "durations": compatibilityColor(getReplayDuration(replay), works), "playerNames": compatibilityColor(getReplayPlayernames(replay), works) }; }); if (list.length) list = prepareForDropdown(list); // Push to GUI replaySelection.selected = -1; replaySelection.list_months = list.months || []; replaySelection.list_players = list.playerNames || []; replaySelection.list_mapName = list.mapNames || []; replaySelection.list_mapSize = list.mapSizes || []; replaySelection.list_popCapacity = list.popCaps || []; replaySelection.list_duration = list.durations || []; // Change these last, otherwise crash replaySelection.list = list.directories || []; replaySelection.list_data = list.directories || []; replaySelection.selected = replaySelection.list.findIndex(directory => directory == g_SelectedReplayDirectory); displayReplayDetails(); } /** * Shows preview image, description and player text in the right panel. */ function displayReplayDetails() { let selected = Engine.GetGUIObjectByName("replaySelection").selected; let replaySelected = selected > -1; Engine.GetGUIObjectByName("replayInfo").hidden = !replaySelected; Engine.GetGUIObjectByName("replayInfoEmpty").hidden = replaySelected; Engine.GetGUIObjectByName("startReplayButton").enabled = replaySelected; Engine.GetGUIObjectByName("deleteReplayButton").enabled = replaySelected; Engine.GetGUIObjectByName("replayFilename").hidden = !replaySelected; Engine.GetGUIObjectByName("summaryButton").hidden = true; if (!replaySelected) return; let replay = g_ReplaysFiltered[selected]; - Engine.GetGUIObjectByName("sgMapName").caption = translate(replay.attribs.settings.Name); + Engine.GetGUIObjectByName("sgMapName").caption = translate(replay.attribs.settings.mapName); Engine.GetGUIObjectByName("sgMapSize").caption = translateMapSize(replay.attribs.settings.Size); Engine.GetGUIObjectByName("sgMapType").caption = translateMapType(replay.attribs.mapType); Engine.GetGUIObjectByName("sgVictory").caption = replay.attribs.settings.VictoryConditions.map(victoryConditionName => translateVictoryCondition(victoryConditionName)).join(translate(", ")); Engine.GetGUIObjectByName("sgNbPlayers").caption = sprintf(translate("Players: %(numberOfPlayers)s"), { "numberOfPlayers": replay.attribs.settings.PlayerData.length }); Engine.GetGUIObjectByName("replayFilename").caption = Engine.GetReplayDirectoryName(replay.directory); let metadata = Engine.GetReplayMetadata(replay.directory); Engine.GetGUIObjectByName("sgPlayersNames").caption = formatPlayerInfo( replay.attribs.settings.PlayerData, Engine.GetGUIObjectByName("showSpoiler").checked && metadata && metadata.playerStates && metadata.playerStates.map(pState => pState.state)); Engine.GetGUIObjectByName("sgMapPreview").sprite = g_MapCache.getMapPreview(replay.attribs.mapType, replay.attribs.map, replay.attribs?.mapPreview); Engine.GetGUIObjectByName("sgMapDescription").caption = g_MapCache.getTranslatedMapDescription(replay.attribs.mapType, replay.attribs.map); Engine.GetGUIObjectByName("summaryButton").hidden = !Engine.HasReplayMetadata(replay.directory); } /** * Returns a human-readable version of the replay date. */ function getReplayDateTime(replay) { return Engine.FormatMillisecondsIntoDateStringLocal(replay.attribs.timestamp * 1000, translate("yyyy-MM-dd HH:mm")); } /** * Returns a human-readable list of the playernames of that replay. * * @returns {string} */ function getReplayPlayernames(replay) { return replay.attribs.settings.PlayerData.map(pData => pData.Name).join(", "); } /** * Returns the name of the map of the given replay. * * @returns {string} */ function getReplayMapName(replay) { - return translate(replay.attribs.settings.Name); + return translate(replay.attribs.settings.mapName); } /** * Returns the month of the given replay in the format "yyyy-MM". * * @returns {string} */ function getReplayMonth(replay) { return Engine.FormatMillisecondsIntoDateStringLocal(replay.attribs.timestamp * 1000, translate("yyyy-MM")); } /** * Returns a human-readable version of the time when the replay started. * * @returns {string} */ function getReplayDuration(replay) { return timeToString(replay.duration * 1000); } /** * True if we can start the given replay with the currently loaded mods. */ function isReplayCompatible(replay) { return replayHasSameEngineVersion(replay) && hasSameMods(replay.attribs.mods, g_EngineInfo.mods); } /** * True if we can start the given replay with the currently loaded mods. */ function replayHasSameEngineVersion(replay) { return replay.attribs.engine_version && replay.attribs.engine_version == g_EngineInfo.engine_version; } Index: ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReporter.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReporter.js (revision 26392) +++ ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReporter.js (revision 26393) @@ -1,57 +1,57 @@ /** * 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_InitAttributes.matchID, - "mapName": g_InitAttributes.settings.Name, + "mapName": g_InitAttributes.settings.mapName, "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() && !g_IsReplay && Engine.GetPlayerID() != -1 && g_InitAttributes.settings.RatingEnabled && g_InitAttributes.settings.PlayerData.length == 3; }; Index: ps/trunk/binaries/data/mods/public/gui/summary/summary.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/summary/summary.js (revision 26392) +++ ps/trunk/binaries/data/mods/public/gui/summary/summary.js (revision 26393) @@ -1,593 +1,593 @@ const g_CivData = loadCivData(false, false); var g_ScorePanelsData; var g_MaxHeadingTitle = 9; var g_LongHeadingWidth = 250; var g_PlayerBoxYSize = 40; var g_PlayerBoxGap = 2; var g_PlayerBoxAlpha = 50; var g_TeamsBoxYStart = 40; var g_TypeColors = { "blue": "196 198 255", "green": "201 255 200", "red": "255 213 213", "yellow": "255 255 157" }; /** * Colors, captions and format used for units, structures, etc. types */ var g_SummaryTypes = { "percent": { "color": "", "caption": "%", "postfix": "%" }, "trained": { "color": g_TypeColors.green, "caption": translate("Trained"), "postfix": " / " }, "constructed": { "color": g_TypeColors.green, "caption": translate("Constructed"), "postfix": " / " }, "gathered": { "color": g_TypeColors.green, "caption": translate("Gathered"), "postfix": " / " }, "count": { "caption": translate("Count"), "hideInSummary": true }, "sent": { "color": g_TypeColors.green, "caption": translate("Sent"), "postfix": " / " }, "bought": { "color": g_TypeColors.green, "caption": translate("Bought"), "postfix": " / " }, "income": { "color": g_TypeColors.green, "caption": translate("Income"), "postfix": " / " }, "captured": { "color": g_TypeColors.yellow, "caption": translate("Captured"), "postfix": " / " }, "succeeded": { "color": g_TypeColors.green, "caption": translate("Succeeded"), "postfix": " / " }, "destroyed": { "color": g_TypeColors.blue, "caption": translate("Destroyed"), "postfix": "\n" }, "killed": { "color": g_TypeColors.blue, "caption": translate("Killed"), "postfix": "\n" }, "lost": { "color": g_TypeColors.red, "caption": translate("Lost"), "postfix": "" }, "used": { "color": g_TypeColors.red, "caption": translate("Used"), "postfix": "" }, "received": { "color": g_TypeColors.red, "caption": translate("Received"), "postfix": "" }, "population": { "color": g_TypeColors.red, "caption": translate("Population"), "postfix": "" }, "sold": { "color": g_TypeColors.red, "caption": translate("Sold"), "postfix": "" }, "outcome": { "color": g_TypeColors.red, "caption": translate("Outcome"), "postfix": "" }, "failed": { "color": g_TypeColors.red, "caption": translate("Failed"), "postfix": "" } }; // Translation: Unicode encoded infinity symbol indicating a division by zero in the summary screen. var g_InfinitySymbol = translate("\u221E"); var g_Teams = []; var g_PlayerCount; var g_GameData; var g_ResourceData = new Resources(); /** * Selected chart indexes. */ var g_SelectedChart = { "category": [0, 0], "value": [0, 1], "type": [0, 0] }; function init(data) { initSummaryData(data); initGUISummary(); } function initSummaryData(data) { g_GameData = data; g_ScorePanelsData = getScorePanelsData(); let teamCharts = false; if (data && data.gui && data.gui.summarySelection) { g_TabCategorySelected = data.gui.summarySelection.panel; g_SelectedChart = data.gui.summarySelection.charts; teamCharts = data.gui.summarySelection.teamCharts; } Engine.GetGUIObjectByName("toggleTeamBox").checked = g_Teams && teamCharts; initTeamData(); calculateTeamCounterDataHelper(); } function initGUISummary() { initGUIWindow(); initPlayerBoxPositions(); initGUICharts(); initGUILabels(); initGUIButtons(); } /** * Sets the style and title of the page. */ function initGUIWindow() { let summaryWindow = Engine.GetGUIObjectByName("summaryWindow"); summaryWindow.sprite = g_GameData.gui.dialog ? "ModernDialog" : "ModernWindow"; summaryWindow.size = g_GameData.gui.dialog ? "16 24 100%-16 100%-24" : "0 0 100% 100%"; Engine.GetGUIObjectByName("summaryWindowTitle").size = g_GameData.gui.dialog ? "50%-128 -16 50%+128 16" : "50%-128 4 50%+128 36"; } function selectPanelGUI(panel) { adjustTabDividers(Engine.GetGUIObjectByName("tabButton[" + panel + "]").size); let generalPanel = Engine.GetGUIObjectByName("generalPanel"); let chartsPanel = Engine.GetGUIObjectByName("chartsPanel"); // We assume all scorePanels come before the charts. let chartsHidden = panel < g_ScorePanelsData.length; generalPanel.hidden = !chartsHidden; chartsPanel.hidden = chartsHidden; if (chartsHidden) updatePanelData(g_ScorePanelsData[panel]); else [0, 1].forEach(updateCategoryDropdown); } function constructPlayersWithColor(color, playerListing) { return sprintf(translateWithContext("Player listing with color indicator", "%(colorIndicator)s %(playerListing)s"), { "colorIndicator": setStringTags(translateWithContext( "Charts player color indicator", "■"), { "color": color }), "playerListing": playerListing }); } function updateChartColorAndLegend() { let playerColors = []; for (let i = 1; i <= g_PlayerCount; ++i) { let playerState = g_GameData.sim.playerStates[i]; playerColors.push( Math.floor(playerState.color.r * 255) + " " + Math.floor(playerState.color.g * 255) + " " + Math.floor(playerState.color.b * 255) + " 255" ); } for (let i = 0; i < 2; ++i) Engine.GetGUIObjectByName("chart[" + i + "]").series_color = Engine.GetGUIObjectByName("toggleTeamBox").checked ? g_Teams.filter(el => el !== null).map(players => playerColors[players[0] - 1]) : playerColors; let chartLegend = Engine.GetGUIObjectByName("chartLegend"); chartLegend.caption = (Engine.GetGUIObjectByName("toggleTeamBox").checked ? g_Teams.filter(el => el !== null).map(players => constructPlayersWithColor(playerColors[players[0] - 1], players.map(player => g_GameData.sim.playerStates[player].name ).join(translateWithContext("Player listing", ", "))) ) : g_GameData.sim.playerStates.slice(1).map((state, index) => constructPlayersWithColor(playerColors[index], state.name)) ).join(" "); } function initGUICharts() { updateChartColorAndLegend(); let chart1Part = Engine.GetGUIObjectByName("chart[1]Part"); let chart1PartSize = chart1Part.size; chart1PartSize.rright += 50; chart1PartSize.rleft += 50; chart1PartSize.right -= 5; chart1PartSize.left -= 5; chart1Part.size = chart1PartSize; Engine.GetGUIObjectByName("toggleTeam").hidden = !g_Teams; } function resizeDropdown(dropdown) { let size = dropdown.size; size.bottom = dropdown.size.top + (Engine.GetTextWidth(dropdown.font, dropdown.list[dropdown.selected]) > dropdown.size.right - dropdown.size.left - 32 ? 42 : 27); dropdown.size = size; } function updateCategoryDropdown(number) { let chartCategory = Engine.GetGUIObjectByName("chart[" + number + "]CategorySelection"); chartCategory.list_data = g_ScorePanelsData.map((panel, idx) => idx); chartCategory.list = g_ScorePanelsData.map(panel => panel.label); chartCategory.onSelectionChange = function() { if (!this.list_data[this.selected]) return; if (g_SelectedChart.category[number] != this.selected) { g_SelectedChart.category[number] = this.selected; g_SelectedChart.value[number] = 0; g_SelectedChart.type[number] = 0; } resizeDropdown(this); updateValueDropdown(number, this.list_data[this.selected]); }; chartCategory.selected = g_SelectedChart.category[number]; } function updateValueDropdown(number, category) { let chartValue = Engine.GetGUIObjectByName("chart[" + number + "]ValueSelection"); let list = g_ScorePanelsData[category].headings.map(heading => heading.caption); list.shift(); chartValue.list = list; let list_data = g_ScorePanelsData[category].headings.map(heading => heading.identifier); list_data.shift(); chartValue.list_data = list_data; chartValue.onSelectionChange = function() { if (!this.list_data[this.selected]) return; if (g_SelectedChart.value[number] != this.selected) { g_SelectedChart.value[number] = this.selected; g_SelectedChart.type[number] = 0; } resizeDropdown(this); updateTypeDropdown(number, category, this.list_data[this.selected], this.selected); }; chartValue.selected = g_SelectedChart.value[number]; } function updateTypeDropdown(number, category, item, itemNumber) { let testValue = g_ScorePanelsData[category].counters[itemNumber].fn(g_GameData.sim.playerStates[1], 0, item); let hide = !g_ScorePanelsData[category].counters[itemNumber].fn || typeof testValue != "object" || Object.keys(testValue).length < 2; Engine.GetGUIObjectByName("chart[" + number + "]TypeLabel").hidden = hide; let chartType = Engine.GetGUIObjectByName("chart[" + number + "]TypeSelection"); chartType.hidden = hide; if (hide) { updateChart(number, category, item, itemNumber, Object.keys(testValue)[0] || undefined); return; } chartType.list = Object.keys(testValue).map(type => g_SummaryTypes[type].caption); chartType.list_data = Object.keys(testValue); chartType.onSelectionChange = function() { if (!this.list_data[this.selected]) return; g_SelectedChart.type[number] = this.selected; resizeDropdown(this); updateChart(number, category, item, itemNumber, this.list_data[this.selected]); }; chartType.selected = g_SelectedChart.type[number]; } function updateChart(number, category, item, itemNumber, type) { if (!g_ScorePanelsData[category].counters[itemNumber].fn) return; let chart = Engine.GetGUIObjectByName("chart[" + number + "]"); chart.format_y = g_ScorePanelsData[category].headings[itemNumber + 1].format || "INTEGER"; Engine.GetGUIObjectByName("chart[" + number + "]XAxisLabel").caption = translate("Time elapsed"); let series = []; if (Engine.GetGUIObjectByName("toggleTeamBox").checked) for (let team in g_Teams) { let data = []; for (let index in g_GameData.sim.playerStates[1].sequences.time) { let value = g_ScorePanelsData[category].teamCounterFn(team, index, item, g_ScorePanelsData[category].counters, g_ScorePanelsData[category].headings); if (type) value = value[type]; data.push({ "x": g_GameData.sim.playerStates[1].sequences.time[index], "y": value }); } series.push(data); } else for (let j = 1; j <= g_PlayerCount; ++j) { let playerState = g_GameData.sim.playerStates[j]; let data = []; for (let index in playerState.sequences.time) { let value = g_ScorePanelsData[category].counters[itemNumber].fn(playerState, index, item); if (type) value = value[type]; data.push({ "x": playerState.sequences.time[index], "y": value }); } series.push(data); } chart.series = series; } function adjustTabDividers(tabSize) { let tabButtonsLeft = Engine.GetGUIObjectByName("tabButtonsFrame").size.left; let leftSpacer = Engine.GetGUIObjectByName("tabDividerLeft"); let leftSpacerSize = leftSpacer.size; leftSpacerSize.right = tabSize.left + tabButtonsLeft + 2; leftSpacer.size = leftSpacerSize; let rightSpacer = Engine.GetGUIObjectByName("tabDividerRight"); let rightSpacerSize = rightSpacer.size; rightSpacerSize.left = tabSize.right + tabButtonsLeft - 2; rightSpacer.size = rightSpacerSize; } function updatePanelData(panelInfo) { resetGeneralPanel(); updateGeneralPanelHeadings(panelInfo.headings); updateGeneralPanelTitles(panelInfo.titleHeadings); let rowPlayerObjectWidth = updateGeneralPanelCounter(panelInfo.counters); updateGeneralPanelTeams(); let index = g_GameData.sim.playerStates[1].sequences.time.length - 1; let playerBoxesCounts = []; for (let i = 0; i < g_PlayerCount; ++i) { let playerState = g_GameData.sim.playerStates[i + 1]; if (!playerBoxesCounts[playerState.team + 1]) playerBoxesCounts[playerState.team + 1] = 1; else playerBoxesCounts[playerState.team + 1] += 1; let positionObject = playerBoxesCounts[playerState.team + 1] - 1; let rowPlayer = "playerBox[" + positionObject + "]"; let playerOutcome = "playerOutcome[" + positionObject + "]"; let playerNameColumn = "playerName[" + positionObject + "]"; let playerCivicBoxColumn = "civIcon[" + positionObject + "]"; let playerCounterValue = "valueData[" + positionObject + "]"; if (playerState.team != -1) { rowPlayer = "playerBoxt[" + playerState.team + "][" + positionObject + "]"; playerOutcome = "playerOutcomet[" + playerState.team + "][" + positionObject + "]"; playerNameColumn = "playerNamet[" + playerState.team + "][" + positionObject + "]"; playerCivicBoxColumn = "civIcont[" + playerState.team + "][" + positionObject + "]"; playerCounterValue = "valueDataTeam[" + playerState.team + "][" + positionObject + "]"; } let colorString = "color: " + Math.floor(playerState.color.r * 255) + " " + Math.floor(playerState.color.g * 255) + " " + Math.floor(playerState.color.b * 255); let rowPlayerObject = Engine.GetGUIObjectByName(rowPlayer); rowPlayerObject.hidden = false; rowPlayerObject.sprite = colorString + " " + g_PlayerBoxAlpha; let boxSize = rowPlayerObject.size; boxSize.right = rowPlayerObjectWidth; rowPlayerObject.size = boxSize; setOutcomeIcon(playerState.state, Engine.GetGUIObjectByName(playerOutcome)); playerNameColumn = Engine.GetGUIObjectByName(playerNameColumn); playerNameColumn.caption = g_GameData.sim.playerStates[i + 1].name; playerNameColumn.tooltip = translateAISettings(g_GameData.sim.mapSettings.PlayerData[i + 1]); let civIcon = Engine.GetGUIObjectByName(playerCivicBoxColumn); civIcon.sprite = "stretched:" + g_CivData[playerState.civ].Emblem; civIcon.tooltip = g_CivData[playerState.civ].Name; updateCountersPlayer(playerState, panelInfo.counters, panelInfo.headings, playerCounterValue, index); } let teamCounterFn = panelInfo.teamCounterFn; if (g_Teams && teamCounterFn) updateCountersTeam(teamCounterFn, panelInfo.counters, panelInfo.headings, index); } function continueButton() { let summarySelection = { "panel": g_TabCategorySelected, "charts": g_SelectedChart, "teamCharts": Engine.GetGUIObjectByName("toggleTeamBox").checked }; if (g_GameData.gui.isInGame) Engine.PopGuiPage({ "summarySelection": summarySelection }); else if (g_GameData.gui.dialog) Engine.PopGuiPage(); else if (Engine.HasXmppClient()) Engine.SwitchGuiPage("page_lobby.xml", { "dialog": false }); else if (g_GameData.gui.isReplay) Engine.SwitchGuiPage("page_replaymenu.xml", { "replaySelectionData": g_GameData.gui.replaySelectionData, "summarySelection": summarySelection }); else if (g_GameData.campaignData) Engine.SwitchGuiPage(g_GameData.nextPage, g_GameData.campaignData); else Engine.SwitchGuiPage("page_pregame.xml"); } function startReplay() { if (!Engine.StartVisualReplay(g_GameData.gui.replayDirectory)) { warn("Replay file not found!"); return; } Engine.SwitchGuiPage("page_loading.xml", { "attribs": Engine.GetReplayAttributes(g_GameData.gui.replayDirectory), "playerAssignments": { "local": { "name": singleplayerName(), "player": -1 } }, "savedGUIData": "", "isReplay": true, "replaySelectionData": g_GameData.gui.replaySelectionData }); } function initGUILabels() { let assignedState = g_GameData.sim.playerStates[g_GameData.gui.assignedPlayer || -1]; Engine.GetGUIObjectByName("summaryText").caption = g_GameData.gui.isInGame ? translate("Current Scores") : g_GameData.gui.isReplay ? translate("Scores at the end of the game.") : g_GameData.gui.disconnected ? translate("You have been disconnected.") : !assignedState ? translate("You have left the game.") : assignedState.state == "won" ? translate("You have won the battle!") : assignedState.state == "defeated" ? translate("You have been defeated…") : translate("You have abandoned the game."); Engine.GetGUIObjectByName("timeElapsed").caption = sprintf( translate("Game time elapsed: %(time)s"), { "time": timeToString(g_GameData.sim.timeElapsed) }); let mapType = g_Settings.MapTypes.find(type => type.Name == g_GameData.sim.mapSettings.mapType); let mapSize = g_Settings.MapSizes.find(size => size.Tiles == g_GameData.sim.mapSettings.Size || 0); Engine.GetGUIObjectByName("mapName").caption = sprintf( translate("%(mapName)s - %(mapType)s"), { - "mapName": translate(g_GameData.sim.mapSettings.Name), + "mapName": translate(g_GameData.sim.mapSettings.mapName), "mapType": mapSize ? mapSize.Name : (mapType ? mapType.Title : "") }); } function initGUIButtons() { let replayButton = Engine.GetGUIObjectByName("replayButton"); replayButton.hidden = g_GameData.gui.isInGame || !g_GameData.gui.replayDirectory; let lobbyButton = Engine.GetGUIObjectByName("lobbyButton"); lobbyButton.tooltip = colorizeHotkey(translate("%(hotkey)s: Toggle the multiplayer lobby in a dialog window."), "lobby"); lobbyButton.hidden = g_GameData.gui.isInGame || !Engine.HasXmppClient(); // Right-align lobby button let lobbyButtonSize = lobbyButton.size; let lobbyButtonWidth = lobbyButtonSize.right - lobbyButtonSize.left; lobbyButtonSize.right = (replayButton.hidden ? Engine.GetGUIObjectByName("continueButton").size.left : replayButton.size.left) - 10; lobbyButtonSize.left = lobbyButtonSize.right - lobbyButtonWidth; lobbyButton.size = lobbyButtonSize; let allPanelsData = g_ScorePanelsData.concat(g_ChartPanelsData); for (let tab in allPanelsData) allPanelsData[tab].tooltip = sprintf(translate("Focus the %(name)s summary tab."), { "name": allPanelsData[tab].label }); placeTabButtons( allPanelsData, true, g_TabButtonWidth, g_TabButtonDist, selectPanel, selectPanelGUI); } function initTeamData() { // Panels g_PlayerCount = g_GameData.sim.playerStates.length - 1; if (g_GameData.sim.mapSettings.LockTeams) { // Count teams for (let player = 1; player <= g_PlayerCount; ++player) { let playerTeam = g_GameData.sim.playerStates[player].team; if (!g_Teams[playerTeam]) g_Teams[playerTeam] = []; g_Teams[playerTeam].push(player); } if (g_Teams.every(team => team && team.length < 2)) g_Teams = false; // Each player has his own team. Displaying teams makes no sense. } else g_Teams = false; // Erase teams data if teams are not displayed if (!g_Teams) for (let p = 0; p < g_PlayerCount; ++p) g_GameData.sim.playerStates[p+1].team = -1; }