Index: ps/trunk/binaries/data/mods/public/gui/gamesetup/Controls/GameSettingsControl.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesetup/Controls/GameSettingsControl.js (revision 24420) +++ ps/trunk/binaries/data/mods/public/gui/gamesetup/Controls/GameSettingsControl.js (revision 24421) @@ -1,269 +1,286 @@ /** * This class provides a property independent interface to g_GameAttributes events. * Classes may use this interface in order to react to changing g_GameAttributes. */ class GameSettingsControl { constructor(setupWindow, netMessages, startGameControl, mapCache) { this.startGameControl = startGameControl; this.mapCache = mapCache; - this.gameSettingsFile = new GameSettingsFile(setupWindow); + this.gameSettingsFile = new GameSettingsFile(this); this.previousMap = undefined; this.depth = 0; // This property may be read from publicly this.autostart = false; this.gameAttributesChangeHandlers = new Set(); this.gameAttributesBatchChangeHandlers = new Set(); this.gameAttributesFinalizeHandlers = new Set(); this.pickRandomItemsHandlers = new Set(); this.assignPlayerHandlers = new Set(); this.mapChangeHandlers = new Set(); setupWindow.registerLoadHandler(this.onLoad.bind(this)); setupWindow.registerGetHotloadDataHandler(this.onGetHotloadData.bind(this)); startGameControl.registerLaunchGameHandler(this.onLaunchGame.bind(this)); + setupWindow.registerClosePageHandler(this.onClose.bind(this)); + if (g_IsNetworked) netMessages.registerNetMessageHandler("gamesetup", this.onGamesetupMessage.bind(this)); } registerMapChangeHandler(handler) { this.mapChangeHandlers.add(handler); } unregisterMapChangeHandler(handler) { this.mapChangeHandlers.delete(handler); } /** * This message is triggered everytime g_GameAttributes change. * Handlers may subsequently change g_GameAttributes and trigger this message again. */ registerGameAttributesChangeHandler(handler) { this.gameAttributesChangeHandlers.add(handler); } unregisterGameAttributesChangeHandler(handler) { this.gameAttributesChangeHandlers.delete(handler); } /** * This message is triggered after g_GameAttributes changed and recursed gameAttributesChangeHandlers finished. * The use case for this is to update GUI objects which do not change g_GameAttributes but only display the attributes. */ registerGameAttributesBatchChangeHandler(handler) { this.gameAttributesBatchChangeHandlers.add(handler); } unregisterGameAttributesBatchChangeHandler(handler) { this.gameAttributesBatchChangeHandlers.delete(handler); } registerGameAttributesFinalizeHandler(handler) { this.gameAttributesFinalizeHandlers.add(handler); } unregisterGameAttributesFinalizeHandler(handler) { this.gameAttributesFinalizeHandlers.delete(handler); } registerAssignPlayerHandler(handler) { this.assignPlayerHandlers.add(handler); } unregisterAssignPlayerHandler(handler) { this.assignPlayerHandlers.delete(handler); } registerPickRandomItemsHandler(handler) { this.pickRandomItemsHandlers.add(handler); } unregisterPickRandomItemsHandler(handler) { this.pickRandomItemsHandlers.delete(handler); } onLoad(initData, hotloadData) { if (initData && initData.map && initData.mapType) { Object.defineProperty(this, "autostart", { "value": true, "writable": false, "configurable": false }); // TODO: Fix g_GameAttributes, g_GameAttributes.settings, // g_GameAttributes.settings.PlayerData object references and // copy over each attribute individually when receiving // settings from the server or the local file. g_GameAttributes = { "mapType": initData.mapType, "map": initData.map }; this.updateGameAttributes(); // Don't launchGame before all Load handlers finished } else { if (hotloadData) g_GameAttributes = hotloadData.gameAttributes; else if (g_IsController && this.gameSettingsFile.enabled) g_GameAttributes = this.gameSettingsFile.loadFile(); this.updateGameAttributes(); this.setNetworkGameAttributes(); } } + onClose() + { + if (!this.autostart) + this.gameSettingsFile.saveFile(); + } + onGetHotloadData(object) { object.gameAttributes = g_GameAttributes; } onGamesetupMessage(message) { if (!message.data) return; g_GameAttributes = message.data; this.updateGameAttributes(); } /** * This is to be called whenever g_GameAttributes has been changed except on gameAttributes finalization. */ updateGameAttributes() { if (this.depth == 0) Engine.ProfileStart("updateGameAttributes"); if (this.depth >= this.MaxDepth) { error("Infinite loop: " + new Error().stack); Engine.ProfileStop(); return; } ++this.depth; // Basic sanitization { if (!g_GameAttributes.settings) g_GameAttributes.settings = {}; if (!g_GameAttributes.settings.PlayerData) g_GameAttributes.settings.PlayerData = new Array(this.DefaultPlayerCount); for (let i = 0; i < g_GameAttributes.settings.PlayerData.length; ++i) if (!g_GameAttributes.settings.PlayerData[i]) g_GameAttributes.settings.PlayerData[i] = {}; } // Map change handlers are triggered first, so that GameSettingControls can update their // gameAttributes model prior to applying that model in their gameAttributesChangeHandler. if (g_GameAttributes.map && this.previousMap != g_GameAttributes.map && g_GameAttributes.mapType) { this.previousMap = g_GameAttributes.map; - let mapData = this.mapCache.getMapData(g_GameAttributes.mapType, g_GameAttributes.map); - for (let handler of this.mapChangeHandlers) - handler(mapData); + // Use a try..catch to avoid completely failing in case of an error + // as this prevents even going back to the main menu. + try + { + let mapData = this.mapCache.getMapData(g_GameAttributes.mapType, g_GameAttributes.map); + for (let handler of this.mapChangeHandlers) + handler(mapData); + } catch(err) { + // Report the error regardless so that the underlying bug gets fixed. + error(err); + error(err.stack); + } } for (let handler of this.gameAttributesChangeHandlers) handler(); --this.depth; if (this.depth == 0) { for (let handler of this.gameAttributesBatchChangeHandlers) handler(); Engine.ProfileStop(); } } /** * 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. */ setNetworkGameAttributes() { if (g_IsNetworked) Engine.SetNetworkGameAttributes(g_GameAttributes); } getPlayerData(gameAttributes, playerIndex) { return gameAttributes && gameAttributes.settings && gameAttributes.settings.PlayerData && gameAttributes.settings.PlayerData[playerIndex] || undefined; } assignPlayer(sourcePlayerIndex, playerIndex) { if (playerIndex == -1) return; let target = this.getPlayerData(g_GameAttributes, playerIndex); let source = this.getPlayerData(g_GameAttributes, sourcePlayerIndex); for (let handler of this.assignPlayerHandlers) handler(source, target); this.updateGameAttributes(); this.setNetworkGameAttributes(); } /** * This function is called everytime a random setting selection was resolved, * so that subsequent random settings are triggered too, * for example picking a random biome after picking a random map. */ pickRandomItems() { for (let handler of this.pickRandomItemsHandlers) handler(); } onLaunchGame() { if (!this.autostart) this.gameSettingsFile.saveFile(); this.pickRandomItems(); for (let handler of this.gameAttributesFinalizeHandlers) handler(); this.setNetworkGameAttributes(); } } GameSettingsControl.prototype.MaxDepth = 512; /** * This number is used when selecting the random map type, which doesn't provide PlayerData. */ GameSettingsControl.prototype.DefaultPlayerCount = 4; Index: ps/trunk/binaries/data/mods/public/gui/gamesetup/Controls/GameSettingsFile.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesetup/Controls/GameSettingsFile.js (revision 24420) +++ ps/trunk/binaries/data/mods/public/gui/gamesetup/Controls/GameSettingsFile.js (revision 24421) @@ -1,64 +1,62 @@ /** * This class provides a way to save g_GameAttributes to a file and load them. */ class GameSettingsFile { - constructor(setupWindow) + constructor(GameSettingsControl) { this.filename = g_IsNetworked ? this.GameAttributesFileMultiplayer : this.GameAttributesFileSingleplayer; this.engineInfo = Engine.GetEngineInfo(); this.enabled = Engine.ConfigDB_GetValue("user", this.ConfigName) == "true"; - - setupWindow.registerClosePageHandler(this.saveFile.bind(this)); } loadFile() { Engine.ProfileStart("loadPersistMatchSettingsFile"); let data = this.enabled && g_IsController && Engine.FileExists(this.filename) && Engine.ReadJSONFile(this.filename); let gameAttributes = data && data.attributes && data.engine_info && data.engine_info.engine_version == this.engineInfo.engine_version && hasSameMods(data.engine_info.mods, this.engineInfo.mods) && data.attributes || {}; Engine.ProfileStop(); return gameAttributes; } /** * Delete settings if disabled, so that players are not confronted with old settings after enabling the setting again. */ saveFile() { if (!g_IsController) return; Engine.ProfileStart("savePersistMatchSettingsFile"); Engine.WriteJSONFile(this.filename, { "attributes": this.enabled ? g_GameAttributes : {}, "engine_info": this.engineInfo }); Engine.ProfileStop(); } } GameSettingsFile.prototype.ConfigName = "persistmatchsettings"; GameSettingsFile.prototype.GameAttributesFileSingleplayer = "config/matchsettings.json"; GameSettingsFile.prototype.GameAttributesFileMultiplayer = "config/matchsettings.mp.json"; Index: ps/trunk/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/Dropdowns/PopulationCap.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/Dropdowns/PopulationCap.js (revision 24420) +++ ps/trunk/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/Dropdowns/PopulationCap.js (revision 24421) @@ -1,111 +1,111 @@ GameSettingControls.PopulationCap = class extends GameSettingControlDropdown { constructor(...args) { super(...args); this.perPlayer = false; this.dropdown.list = g_PopulationCapacities.Title; this.dropdown.list_data = g_PopulationCapacities.Population; this.sprintfArgs = {}; } onMapChange(mapData) { let mapValue; if (mapData && mapData.settings && mapData.settings.PopulationCap !== undefined) mapValue = mapData.settings.PopulationCap; if (mapValue !== undefined && mapValue != g_GameAttributes.settings.PopulationCap) { g_GameAttributes.settings.PopulationCap = mapValue; this.gameSettingsControl.updateGameAttributes(); } let isScenario = g_GameAttributes.mapType == "scenario"; this.perPlayer = isScenario && - mapData.settings.PlayerData && + mapData && mapData.settings && mapData.settings.PlayerData && mapData.settings.PlayerData.some(pData => pData && pData.PopulationLimit !== undefined); this.setEnabled(!isScenario && !this.perPlayer); if (this.perPlayer) this.label.caption = this.PerPlayerCaption; } onGameAttributesChange() { if (g_GameAttributes.settings.WorldPopulation) { this.setHidden(true); g_GameAttributes.settings.PopulationCap = undefined; } else { this.setHidden(false); if (g_GameAttributes.settings.PopulationCap === undefined) { g_GameAttributes.settings.PopulationCap = g_PopulationCapacities.Population[g_PopulationCapacities.Default]; this.gameSettingsControl.updateGameAttributes(); } } } onGameAttributesBatchChange() { if (!this.perPlayer) this.setSelectedValue(g_GameAttributes.settings.PopulationCap); } onHoverChange() { let tooltip = this.Tooltip; if (this.dropdown.hovered != -1) { let popCap = g_PopulationCapacities.Population[this.dropdown.hovered]; let players = g_GameAttributes.settings.PlayerData.length; if (popCap * players >= this.PopulationCapacityRecommendation) { this.sprintfArgs.players = players; this.sprintfArgs.popCap = popCap; tooltip = setStringTags(sprintf(this.HoverTooltip, this.sprintfArgs), this.HoverTags); } } this.dropdown.tooltip = tooltip; } onSelectionChange(itemIdx) { g_GameAttributes.settings.PopulationCap = g_PopulationCapacities.Population[itemIdx]; this.gameSettingsControl.updateGameAttributes(); this.gameSettingsControl.setNetworkGameAttributes(); } }; GameSettingControls.PopulationCap.prototype.TitleCaption = translate("Population Cap"); GameSettingControls.PopulationCap.prototype.Tooltip = translate("Select population limit."); GameSettingControls.PopulationCap.prototype.PerPlayerCaption = translateWithContext("population limit", "Per Player"); GameSettingControls.PopulationCap.prototype.HoverTooltip = translate("Warning: There might be performance issues if all %(players)s players reach %(popCap)s population."); GameSettingControls.PopulationCap.prototype.HoverTags = { "color": "orange" }; /** * Total number of units that the engine can run with smoothly. * It means a 4v4 with 150 population can still run nicely, but more than that might "lag". */ GameSettingControls.PopulationCap.prototype.PopulationCapacityRecommendation = 1200; Index: ps/trunk/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/Dropdowns/StartingResources.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/Dropdowns/StartingResources.js (revision 24420) +++ ps/trunk/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/Dropdowns/StartingResources.js (revision 24421) @@ -1,93 +1,93 @@ GameSettingControls.StartingResources = class extends GameSettingControlDropdown { constructor(...args) { super(...args); this.dropdown.list = g_StartingResources.Title; this.dropdown.list_data = g_StartingResources.Resources; this.perPlayer = false; this.sprintfArgs = {}; } onHoverChange() { let tooltip = this.Tooltip; if (this.dropdown.hovered != -1) { this.sprintfArgs.resources = g_StartingResources.Resources[this.dropdown.hovered]; tooltip = sprintf(this.HoverTooltip, this.sprintfArgs); } this.dropdown.tooltip = tooltip; } onMapChange(mapData) { let mapValue; if (mapData && mapData.settings && mapData.settings.StartingResources !== undefined) mapValue = mapData.settings.StartingResources; if (mapValue !== undefined && mapValue != g_GameAttributes.settings.StartingResources) { g_GameAttributes.settings.StartingResources = mapValue; this.gameSettingsControl.updateGameAttributes(); } let isScenario = g_GameAttributes.mapType == "scenario"; this.perPlayer = isScenario && - mapData.settings.PlayerData && + mapData && mapData.settings && mapData.settings.PlayerData && mapData.settings.PlayerData.some(pData => pData && pData.Resources !== undefined); this.setEnabled(!isScenario && !this.perPlayer); if (this.perPlayer) this.label.caption = this.PerPlayerCaption; } onGameAttributesChange() { if (g_GameAttributes.settings.StartingResources === undefined) { g_GameAttributes.settings.StartingResources = g_StartingResources.Resources[g_StartingResources.Default]; this.gameSettingsControl.updateGameAttributes(); } } onGameAttributesBatchChange() { if (!this.perPlayer) this.setSelectedValue(g_GameAttributes.settings.StartingResources); } getAutocompleteEntries() { return g_StartingResources.Title; } onSelectionChange(itemIdx) { g_GameAttributes.settings.StartingResources = g_StartingResources.Resources[itemIdx]; this.gameSettingsControl.updateGameAttributes(); this.gameSettingsControl.setNetworkGameAttributes(); } }; GameSettingControls.StartingResources.prototype.TitleCaption = translate("Starting Resources"); GameSettingControls.StartingResources.prototype.Tooltip = translate("Select the game's starting resources."); GameSettingControls.StartingResources.prototype.HoverTooltip = translate("Initial amount of each resource: %(resources)s."); GameSettingControls.StartingResources.prototype.PerPlayerCaption = translateWithContext("starting resources", "Per Player"); GameSettingControls.StartingResources.prototype.AutocompleteOrder = 0;