Index: ps/trunk/binaries/data/mods/public/gui/gamesettings/GameSettings.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesettings/GameSettings.js (revision 26534) +++ ps/trunk/binaries/data/mods/public/gui/gamesettings/GameSettings.js (revision 26535) @@ -1,141 +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) 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) + const components = Object.keys(this); + + // When we have looped components.length + 1 times, we are considered stuck. + for (let i = 0; i <= components.length; ++i) { - throw new Error("Infinite loop picking random items, remains : " + uneval(components)); + // Re-pick if any random setting was unrandomised, to make sure dependencies are cleared. + let rePick = false; + for (const comp in this) + if (this[comp].pickRandomItems) + rePick = this[comp].pickRandomItems() || rePick; + if (!rePick) + return; } + + throw new Error("Infinite loop picking random items detected, components: " + 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(); // 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); 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.finalizedAttributes); else 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/Biome.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/Biome.js (revision 26534) +++ ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/Biome.js (revision 26535) @@ -1,93 +1,91 @@ GameSettings.prototype.Attributes.Biome = class Biome extends GameSetting { init() { this.biomes = loadBiomes(); this.biomeData = {}; for (const biome of this.biomes) this.biomeData[biome.Id] = biome; this.cachedMapData = undefined; this.biome = undefined; // NB: random is always available. this.available = new Set(); this.settings.map.watch(() => this.onMapChange(), ["map"]); } toInitAttributes(attribs) { if (!this.biome) return; attribs.settings.Biome = this.biome; } fromInitAttributes(attribs) { if (!this.getLegacySetting(attribs, "Biome")) this.setBiome(undefined); else this.setBiome(this.getLegacySetting(attribs, "Biome")); } filterBiome(available) { if (typeof available === "string") return biome => biome.Id.startsWith(available); return biome => available.indexOf(biome.Id) !== -1; } onMapChange() { const mapData = this.settings.map.data; if (mapData && mapData.settings && mapData.settings.SupportedBiomes !== undefined) { if (mapData.settings.SupportedBiomes === this.cachedMapData) return; this.cachedMapData = mapData.settings.SupportedBiomes; this.available = new Set(this.biomes.filter(this.filterBiome(mapData.settings.SupportedBiomes)) .map(biome => biome.Id)); this.biome = "random"; } else if (this.cachedMapData !== undefined) { this.cachedMapData = undefined; this.available = new Set(); this.biome = undefined; } } setBiome(biome) { // TODO: more validation. if (this.available.size) this.biome = biome || "random"; else this.biome = undefined; } getAvailableBiomeData() { return Array.from(this.available).map(biome => this.biomeData[biome]); } getData() { if (!this.biome) return undefined; return this.biomeData[this.biome]; } pickRandomItems() { // If the map is random, we need to wait until it selects to know if we need to pick a biome. - if (this.settings.map.map === "random") - return true; - - if (this.biome !== "random") + if (this.settings.map.map === "random" || this.biome !== "random") return false; + this.biome = pickRandom(Array.from(this.available)); return true; } }; Index: ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/Daytime.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/Daytime.js (revision 26534) +++ ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/Daytime.js (revision 26535) @@ -1,68 +1,66 @@ GameSettings.prototype.Attributes.Daytime = class Daytime extends GameSetting { init() { this.setDataValueHelper(undefined, undefined); this.settings.map.watch(() => this.onMapChange(), ["map"]); } toInitAttributes(attribs) { if (this.value) attribs.settings.Daytime = this.value; } fromInitAttributes(attribs) { if (!this.getLegacySetting(attribs, "Daytime")) this.setValue(undefined); else this.setValue(this.getLegacySetting(attribs, "Daytime")); } onMapChange() { let mapData = this.settings.map.data; if (!mapData || !mapData.settings || !mapData.settings.Daytime) { this.setDataValueHelper(undefined, undefined); return; } // TODO: validation this.setDataValueHelper(mapData.settings.Daytime, "random"); } setValue(val) { // TODO: more validation. if (this.data) this.value = val || "random"; else this.value = undefined; } 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") + if (this.settings.map.map === "random" || this.value !== "random") return false; + this.value = pickRandom(this.data).Id; return true; } /** * Helper function to ensure this.data and this.value * are assigned in the correct order to prevent * crashes in the renderer. * @param {object} data - The day time option data. * @param {string} value - The option's key. */ setDataValueHelper(data, value) { this.data = data; this.value = value; } }; Index: ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/Landscape.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/Landscape.js (revision 26534) +++ ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/Landscape.js (revision 26535) @@ -1,78 +1,75 @@ GameSettings.prototype.Attributes.Landscape = class Landscape extends GameSetting { init() { this.data = undefined; this.value = undefined; this.settings.map.watch(() => this.onMapChange(), ["map"]); } toInitAttributes(attribs) { if (this.value) attribs.settings.Landscape = this.value; } fromInitAttributes(attribs) { if (!this.getLegacySetting(attribs, "Landscape")) this.setValue(undefined); else this.setValue(this.getLegacySetting(attribs, "Landscape")); } onMapChange() { if (!this.getMapSetting("Landscapes")) { this.value = undefined; this.data = undefined; return; } // TODO: validation this.data = this.getMapSetting("Landscapes"); this.value = "random"; } setValue(val) { // TODO: more validation. if (this.data) this.value = val || "random"; else this.value = undefined; } getPreviewFilename() { if (!this.value) return undefined; for (let group of this.data) for (let item of group.Items) if (item.Id == this.value) return item.Preview; return undefined; } 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 || !this.value.startsWith("random")) + if (this.settings.map.map === "random" || !this.value || !this.value.startsWith("random")) return false; let items = []; if (this.value.indexOf("_") !== -1) { let subgroup = this.data.find(x => x.Id == this.value); items = subgroup.Items.map(x => x.Id); } else items = this.data.map(x => x.Items.map(item => item.Id)).flat(); this.value = pickRandom(items); return true; } }; Index: ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/TeamPlacement.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/TeamPlacement.js (revision 26534) +++ ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/TeamPlacement.js (revision 26535) @@ -1,75 +1,73 @@ 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 !== 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") + if (this.settings.map.map === "random" || 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."), } ];