Index: binaries/data/mods/public/gui/common/gamedescription.js =================================================================== --- binaries/data/mods/public/gui/common/gamedescription.js +++ binaries/data/mods/public/gui/common/gamedescription.js @@ -377,6 +377,15 @@ g_GameAttributes.settings.PopulationCap)] }); + if (g_GameAttributes.settings.WorldPopulationCap !== undefined) + titles.push({ + "label": translate("World Population Cap"), + "value": + g_WorldPopulationCapacities.Title[ + g_WorldPopulationCapacities.Population.indexOf( + g_GameAttributes.settings.WorldPopulationCap)] + }); + titles.push({ "label": translate("Treasures"), "value": g_GameAttributes.settings.DisableTreasures ? Index: binaries/data/mods/public/gui/common/settings.js =================================================================== --- binaries/data/mods/public/gui/common/settings.js +++ binaries/data/mods/public/gui/common/settings.js @@ -44,6 +44,7 @@ "Biomes": loadBiomes(), "PlayerDefaults": loadPlayerDefaults(), "PopulationCapacities": loadPopulationCapacities(), + "WorldPopulationCapacities": loadWorldPopulationCapacities(), "StartingResources": loadSettingValuesFile("starting_resources.json"), "VictoryConditions": loadVictoryConditions(), "TriggerDifficulties": loadSettingValuesFile("trigger_difficulties.json") @@ -269,6 +270,29 @@ } /** + * Loads available world population capacities. + * + * @returns {Object[]|undefined} - An array of the world population capacities in the form: + * { "Population": number, "Default": number, "Title": number|String }. + */ +function loadWorldPopulationCapacities() +{ + let json = Engine.ReadJSONFile(g_SettingsDirectory + "world_population_capacities.json"); + + if (!json || json.Default === undefined || !json.WorldPopulationCapacities || !Array.isArray(json.WorldPopulationCapacities)) + { + error("Could not load population_capacities.json"); + return undefined; + } + + return json.WorldPopulationCapacities.map(population => ({ + "Population": population, + "Default": population == json.Default, + "Title": population < 10000 ? population : translate("Unlimited") + })); +} + +/** * Creates an object with all values of that property of the given setting and * finds the index of the default value. * @@ -364,11 +388,19 @@ /** * Returns title or placeholder. * - * @param {Number} population - for example 300 + * @param {Number} population + * @param {boolean} world - Whether the entry has world population enabled. * @returns {string} */ -function translatePopulationCapacity(population) +function translatePopulationCapacity(population, world) { + if (world) + { + let popCap = g_Settings.WorldPopulationCapacities.find(p => p.Population == population); + return popCap ? popCap.Title + " " + translateWithContext("population capacity addendum", "(world)") : + translateWithContext("population capacity", "Unknown"); + } + let popCap = g_Settings.PopulationCapacities.find(p => p.Population == population); return popCap ? popCap.Title : translateWithContext("population capacity", "Unknown"); } Index: binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/GameSettingsLayout.js =================================================================== --- binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/GameSettingsLayout.js +++ binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/GameSettingsLayout.js @@ -26,7 +26,9 @@ "label": translateWithContext("Match settings tab name", "Player"), "settings": [ "PlayerCount", + "WorldPopulation", "PopulationCap", + "WorldPopulationCap", "StartingResources", "Spies", "Cheats" Index: binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/Checkboxes/WorldPopulation.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/Checkboxes/WorldPopulation.js @@ -0,0 +1,49 @@ +GameSettingControls.WorldPopulation = class extends GameSettingControlCheckbox +{ + onMapChange(mapData) + { + let mapValue = + mapData && + mapData.settings && + mapData.settings.WorldPopulation || undefined; + + if (mapValue !== undefined && mapValue != g_GameAttributes.settings.WorldPopulation) + { + g_GameAttributes.settings.WorldPopulation = mapValue; + this.gameSettingsControl.updateGameAttributes(); + } + } + + onGameAttributesChange() + { + if (!g_GameAttributes.mapType) + return; + + if (g_GameAttributes.settings.WorldPopulation === undefined) + { + g_GameAttributes.settings.WorldPopulation = false; + this.gameSettingsControl.updateGameAttributes(); + } + } + + onGameAttributesBatchChange() + { + if (!g_GameAttributes.mapType) + return; + + this.setChecked(g_GameAttributes.settings.WorldPopulation); + } + + onPress(checked) + { + g_GameAttributes.settings.WorldPopulation = checked; + this.gameSettingsControl.updateGameAttributes(); + this.gameSettingsControl.setNetworkGameAttributes(); + } +}; + +GameSettingControls.WorldPopulation.prototype.TitleCaption = + translate("World population"); + +GameSettingControls.WorldPopulation.prototype.Tooltip = + translate("When checked the Population Cap will be evenly distributed over all living players."); Index: binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/Dropdowns/PopulationCap.js =================================================================== --- binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/Dropdowns/PopulationCap.js +++ binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/Dropdowns/PopulationCap.js @@ -40,10 +40,19 @@ onGameAttributesChange() { - if (g_GameAttributes.settings.PopulationCap === undefined) + if (g_GameAttributes.settings.WorldPopulation) { - g_GameAttributes.settings.PopulationCap = g_PopulationCapacities.Population[g_PopulationCapacities.Default]; - this.gameSettingsControl.updateGameAttributes(); + 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(); + } } } Index: binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/Dropdowns/WorldPopulationCap.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/Dropdowns/WorldPopulationCap.js @@ -0,0 +1,91 @@ +GameSettingControls.WorldPopulationCap = class extends GameSettingControlDropdown +{ + constructor(...args) + { + super(...args); + + this.dropdown.list = g_WorldPopulationCapacities.Title; + this.dropdown.list_data = g_WorldPopulationCapacities.Population; + + this.sprintfArgs = {}; + } + + onMapChange(mapData) + { + let mapValue = + mapData && + mapData.settings && + mapData.settings.WorldPopulationCap || undefined; + + if (mapValue !== undefined && mapValue != g_GameAttributes.settings.WorldPopulationCap) + { + g_GameAttributes.settings.WorldPopulationCap = mapValue; + this.gameSettingsControl.updateGameAttributes(); + } + + this.setEnabled(g_GameAttributes.mapType != "scenario"); + } + + onGameAttributesChange() + { + if (g_GameAttributes.settings.WorldPopulation) + { + this.setHidden(false); + if (g_GameAttributes.settings.WorldPopulationCap === undefined) + { + g_GameAttributes.settings.WorldPopulationCap = g_WorldPopulationCapacities.Population[g_WorldPopulationCapacities.Default]; + this.gameSettingsControl.updateGameAttributes(); + } + } + else + { + this.setHidden(true); + g_GameAttributes.settings.WorldPopulationCap = undefined; + } + } + + onGameAttributesBatchChange() + { + this.setSelectedValue(g_GameAttributes.settings.WorldPopulationCap); + } + + onHoverChange() + { + let tooltip = this.Tooltip; + if (this.dropdown.hovered != -1) + { + let popCap = g_WorldPopulationCapacities.Population[this.dropdown.hovered]; + if (popCap >= this.WorldPopulationCapacityRecommendation) + { + this.sprintfArgs.popCap = popCap; + tooltip = setStringTags(sprintf(this.HoverTooltip, this.sprintfArgs), this.HoverTags); + } + } + this.dropdown.tooltip = tooltip; + } + + onSelectionChange(itemIdx) + { + g_GameAttributes.settings.WorldPopulationCap = g_WorldPopulationCapacities.Population[itemIdx]; + this.gameSettingsControl.updateGameAttributes(); + this.gameSettingsControl.setNetworkGameAttributes(); + } +}; + +GameSettingControls.WorldPopulationCap.prototype.TitleCaption = + translate("World Population Cap"); + +GameSettingControls.WorldPopulationCap.prototype.Tooltip = + translate("Select world population limit."); + +GameSettingControls.WorldPopulationCap.prototype.HoverTooltip = + translate("Warning: There might be performance issues if %(popCap)s population is reached."); + +GameSettingControls.WorldPopulationCap.prototype.HoverTags = { + "color": "orange" +}; + +/** + * Total number of units that the engine can run with smoothly. + */ +GameSettingControls.WorldPopulationCap.prototype.WorldPopulationCapacityRecommendation = 1200; Index: binaries/data/mods/public/gui/gamesetup/gamesetup.js =================================================================== --- binaries/data/mods/public/gui/gamesetup/gamesetup.js +++ binaries/data/mods/public/gui/gamesetup/gamesetup.js @@ -2,6 +2,7 @@ const g_MapSizes = prepareForDropdown(g_Settings && g_Settings.MapSizes); const g_MapTypes = prepareForDropdown(g_Settings && g_Settings.MapTypes); const g_PopulationCapacities = prepareForDropdown(g_Settings && g_Settings.PopulationCapacities); +const g_WorldPopulationCapacities = prepareForDropdown(g_Settings && g_Settings.WorldPopulationCapacities); const g_StartingResources = prepareForDropdown(g_Settings && g_Settings.StartingResources); const g_VictoryConditions = g_Settings && g_Settings.VictoryConditions; Index: binaries/data/mods/public/gui/replaymenu/replay_filters.js =================================================================== --- binaries/data/mods/public/gui/replaymenu/replay_filters.js +++ binaries/data/mods/public/gui/replaymenu/replay_filters.js @@ -16,6 +16,7 @@ * 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 Index: binaries/data/mods/public/gui/replaymenu/replay_menu.js =================================================================== --- binaries/data/mods/public/gui/replaymenu/replay_menu.js +++ binaries/data/mods/public/gui/replaymenu/replay_menu.js @@ -227,7 +227,7 @@ return { "directories": replay.directory, "months": compatibilityColor(getReplayDateTime(replay), works), - "popCaps": compatibilityColor(translatePopulationCapacity(replay.attribs.settings.PopulationCap), 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), Index: binaries/data/mods/public/gui/session/session.js =================================================================== --- binaries/data/mods/public/gui/session/session.js +++ binaries/data/mods/public/gui/session/session.js @@ -5,6 +5,7 @@ const g_MapSizes = prepareForDropdown(g_Settings && g_Settings.MapSizes); const g_MapTypes = prepareForDropdown(g_Settings && g_Settings.MapTypes); const g_PopulationCapacities = prepareForDropdown(g_Settings && g_Settings.PopulationCapacities); +const g_WorldPopulationCapacities = prepareForDropdown(g_Settings && g_Settings.WorldPopulationCapacities); const g_StartingResources = prepareForDropdown(g_Settings && g_Settings.StartingResources); const g_VictoryConditions = g_Settings && g_Settings.VictoryConditions; Index: binaries/data/mods/public/simulation/components/GuiInterface.js =================================================================== --- binaries/data/mods/public/simulation/components/GuiInterface.js +++ binaries/data/mods/public/simulation/components/GuiInterface.js @@ -57,7 +57,8 @@ "players": [] }; - let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); + let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); + let numPlayers = cmpPlayerManager.GetNumPlayers(); for (let i = 0; i < numPlayers; ++i) { let cmpPlayer = QueryPlayerIDInterface(i); @@ -153,6 +154,8 @@ ret.victoryConditions = cmpEndGameManager.GetVictoryConditions(); ret.alliedVictory = cmpEndGameManager.GetAlliedVictory(); + ret.maxWorldPopulation = cmpPlayerManager.GetMaxWorldPopulation(); + for (let i = 0; i < numPlayers; ++i) { let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker); Index: binaries/data/mods/public/simulation/components/PlayerManager.js =================================================================== --- binaries/data/mods/public/simulation/components/PlayerManager.js +++ binaries/data/mods/public/simulation/components/PlayerManager.js @@ -5,7 +5,11 @@ PlayerManager.prototype.Init = function() { - this.playerEntities = []; // list of player entity IDs + // List of player entity IDs. + this.playerEntities = []; + + // Maximum world population (if applicable will be distributed amongst living players). + this.maxWorldPopulation = undefined; }; PlayerManager.prototype.AddPlayer = function(ent) @@ -165,4 +169,35 @@ Engine.DestroyEntity(lastId); }; +PlayerManager.prototype.SetMaxWorldPopulation = function(max) +{ + this.maxWorldPopulation = max; + this.RedistributeWorldPopulation(); +}; + +PlayerManager.prototype.GetMaxWorldPopulation = function() +{ + return this.maxWorldPopulation; +}; + +PlayerManager.prototype.RedistributeWorldPopulation = function() +{ + let worldPopulation = this.GetMaxWorldPopulation(); + if (worldPopulation) + { + let activePlayers = this.GetActivePlayers(); + + // We need to default to "1" here because when a game is won all players + // become "inactive" letting an observer see an infinite max pop cap. + let newMaxPopulation = worldPopulation / (activePlayers.length || 1); + for (let playerID of activePlayers) + QueryPlayerIDInterface(playerID).SetMaxPopulation(newMaxPopulation); + } +}; + +PlayerManager.prototype.OnGlobalPlayerDefeated = function(msg) +{ + this.RedistributeWorldPopulation(); +}; + Engine.RegisterSystemComponentType(IID_PlayerManager, "PlayerManager", PlayerManager); Index: binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js +++ binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js @@ -73,7 +73,8 @@ AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetNumPlayers": function() { return 2; }, - "GetPlayerByID": function(id) { TS_ASSERT(id === 0 || id === 1); return 100 + id; } + "GetPlayerByID": function(id) { TS_ASSERT(id === 0 || id === 1); return 100 + id; }, + "GetMaxWorldPopulation": function() {} }); AddMock(SYSTEM_ENTITY, IID_RangeManager, { @@ -368,7 +369,8 @@ "circularMap": false, "timeElapsed": 0, "victoryConditions": ["conquest", "wonder"], - "alliedVictory": false + "alliedVictory": false, + "maxWorldPopulation": undefined }); TS_ASSERT_UNEVAL_EQUALS(cmp.GetExtendedSimulationState(), { @@ -521,7 +523,8 @@ "circularMap": false, "timeElapsed": 0, "victoryConditions": ["conquest", "wonder"], - "alliedVictory": false + "alliedVictory": false, + "maxWorldPopulation": undefined }); Index: binaries/data/mods/public/simulation/data/settings/world_population_capacities.json =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/data/settings/world_population_capacities.json @@ -0,0 +1,4 @@ +{ + "WorldPopulationCapacities": [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 40320], + "Default": 600 +} Index: binaries/data/mods/public/simulation/helpers/InitGame.js =================================================================== --- binaries/data/mods/public/simulation/helpers/InitGame.js +++ binaries/data/mods/public/simulation/helpers/InitGame.js @@ -65,8 +65,14 @@ "Cost/BuildTime": [{ "affects": ["Unit", "Structure"], "multiply": time[AIDiff] }], }, cmpPlayer.entity); } + + if (settings.PopulationCap) + cmpPlayer.SetMaxPopulation(settings.PopulationCap); } - // Map or player data (handicap...) dependent initialisations of components (i.e. garrisoned units) + if (settings.WorldPopulationCap) + Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).SetMaxWorldPopulation(settings.WorldPopulationCap); + + // Map or player data (handicap...) dependent initialisations of components (i.e. garrisoned units). Engine.BroadcastMessage(MT_InitGame, {}); cmpAIManager.TryLoadSharedComponent();