From 97b16d2b5fd13836b9888a2b6be193bbd612ec96 Mon Sep 17 00:00:00 2001 From: Yair Morgenstern Date: Thu, 31 Aug 2023 14:15:32 +0300 Subject: [PATCH] performance: Unique caching revamp! - Cache civ uniques ignoring conditionals, for better reuse - Cache civ uniques *when querying city uniques*, same This allows us to use the same UniqueCache between cities, and we still get the performance boost of "search once filter always", since the searching is the heavier part, and in any case we'll always have to do the filtering by conditionals either way --- core/src/com/unciv/logic/GameInfo.kt | 9 ++++--- .../com/unciv/logic/automation/Automation.kt | 2 +- core/src/com/unciv/logic/city/City.kt | 2 +- .../com/unciv/logic/city/CityConstructions.kt | 3 +-- core/src/com/unciv/logic/city/CityStats.kt | 11 ++++---- .../com/unciv/models/ruleset/unique/Unique.kt | 25 +++++++++++++------ 6 files changed, 33 insertions(+), 19 deletions(-) diff --git a/core/src/com/unciv/logic/GameInfo.kt b/core/src/com/unciv/logic/GameInfo.kt index 5fc0c12229..d9f439fe72 100644 --- a/core/src/com/unciv/logic/GameInfo.kt +++ b/core/src/com/unciv/logic/GameInfo.kt @@ -33,6 +33,7 @@ import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.RulesetCache import com.unciv.models.ruleset.Speed import com.unciv.models.ruleset.nation.Difficulty +import com.unciv.models.ruleset.unique.LocalUniqueCache import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.translations.tr import com.unciv.ui.audio.MusicMood @@ -669,10 +670,11 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion civInfo.cache.updateCitiesConnectedToCapital(true) // We need to determine the GLOBAL happiness state in order to determine the city stats + val localUniqueCache = LocalUniqueCache() for (city in civInfo.cities) { - city.cityStats.updateTileStats() // Some nat wonders can give happiness! + city.cityStats.updateTileStats(localUniqueCache) // Some nat wonders can give happiness! city.cityStats.updateCityHappiness( - city.cityConstructions.getStats() + city.cityConstructions.getStats(localUniqueCache) ) } @@ -689,7 +691,8 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion if (!ruleset.tileResources.containsKey(city.demandedResource)) city.demandedResource = "" - city.cityStats.update() + // No uniques have changed since the cache was created, so we can still use it + city.cityStats.update(localUniqueCache=localUniqueCache) } } } diff --git a/core/src/com/unciv/logic/automation/Automation.kt b/core/src/com/unciv/logic/automation/Automation.kt index f2a10c1c43..97bcbb1292 100644 --- a/core/src/com/unciv/logic/automation/Automation.kt +++ b/core/src/com/unciv/logic/automation/Automation.kt @@ -345,8 +345,8 @@ object Automation { return when { powerLevelComparison > 2 -> ThreatLevel.VeryHigh powerLevelComparison > 1.5f -> ThreatLevel.High - powerLevelComparison < (1 / 1.5f) -> ThreatLevel.Low powerLevelComparison < 0.5f -> ThreatLevel.VeryLow + powerLevelComparison < (1 / 1.5f) -> ThreatLevel.Low else -> ThreatLevel.Medium } } diff --git a/core/src/com/unciv/logic/city/City.kt b/core/src/com/unciv/logic/city/City.kt index c2978dcff0..e6d1aa18eb 100644 --- a/core/src/com/unciv/logic/city/City.kt +++ b/core/src/com/unciv/logic/city/City.kt @@ -622,7 +622,7 @@ class City : IsPartOfGameInfoSerialization { } // Uniques special to this city - private fun getLocalMatchingUniques(uniqueType: UniqueType, stateForConditionals: StateForConditionals = StateForConditionals(civ, this)): Sequence { + fun getLocalMatchingUniques(uniqueType: UniqueType, stateForConditionals: StateForConditionals = StateForConditionals(civ, this)): Sequence { return ( cityConstructions.builtBuildingUniqueMap.getUniques(uniqueType).filter { it.isLocalEffect } + religion.getUniques().filter { it.isOfType(uniqueType) } diff --git a/core/src/com/unciv/logic/city/CityConstructions.kt b/core/src/com/unciv/logic/city/CityConstructions.kt index cb601fda26..ffa5bfcdae 100644 --- a/core/src/com/unciv/logic/city/CityConstructions.kt +++ b/core/src/com/unciv/logic/city/CityConstructions.kt @@ -110,9 +110,8 @@ class CityConstructions : IsPartOfGameInfoSerialization { /** * @return [Stats] provided by all built buildings in city plus the bonus from Library */ - fun getStats(): StatTreeNode { + fun getStats(localUniqueCache: LocalUniqueCache): StatTreeNode { val stats = StatTreeNode() - val localUniqueCache = LocalUniqueCache() for (building in getBuiltBuildings()) stats.addStats(building.getStats(city, localUniqueCache), building.name) return stats diff --git a/core/src/com/unciv/logic/city/CityStats.kt b/core/src/com/unciv/logic/city/CityStats.kt index 3cd0b29d36..50701ad29c 100644 --- a/core/src/com/unciv/logic/city/CityStats.kt +++ b/core/src/com/unciv/logic/city/CityStats.kt @@ -354,9 +354,8 @@ class CityStats(val city: City) { //endregion //region State-Changing Methods - fun updateTileStats() { + fun updateTileStats(localUniqueCache:LocalUniqueCache = LocalUniqueCache()) { val stats = Stats() - val localUniqueCache = LocalUniqueCache() val workedTiles = city.tilesInRange.asSequence() .filter { city.location == it.position @@ -490,12 +489,14 @@ class CityStats(val city: City) { fun update(currentConstruction: IConstruction = city.cityConstructions.getCurrentConstruction(), updateTileStats:Boolean = true, - updateCivStats:Boolean = true) { - if (updateTileStats) updateTileStats() + updateCivStats:Boolean = true, + localUniqueCache:LocalUniqueCache = LocalUniqueCache()) { + + if (updateTileStats) updateTileStats(localUniqueCache) // We need to compute Tile yields before happiness - val statsFromBuildings = city.cityConstructions.getStats() // this is performance heavy, so calculate once + val statsFromBuildings = city.cityConstructions.getStats(localUniqueCache) // this is performance heavy, so calculate once updateBaseStatList(statsFromBuildings) updateCityHappiness(statsFromBuildings) updateStatPercentBonusList(currentConstruction) diff --git a/core/src/com/unciv/models/ruleset/unique/Unique.kt b/core/src/com/unciv/models/ruleset/unique/Unique.kt index 055e1e9f39..19f6dbb572 100644 --- a/core/src/com/unciv/models/ruleset/unique/Unique.kt +++ b/core/src/com/unciv/models/ruleset/unique/Unique.kt @@ -325,11 +325,18 @@ class LocalUniqueCache(val cache:Boolean = true) { uniqueType: UniqueType, ignoreConditionals: Boolean = false ): Sequence { - val stateForConditionals = if (ignoreConditionals) StateForConditionals.IgnoreConditionals - else StateForConditionals(city.civ, city) + // City uniques are a combination of *global civ* uniques plus *city relevant* uniques (see City.getMatchingUniques()) + // We can cache the civ uniques separately, so if we have several cities using the same cache, + // we can cache the list of *civ uniques* to reuse between cities. + // This is assuming that we're ignoring conditionals, because otherwise - + // the conditionals will render the the *filtered uniques* different anyway, so there's no reason to cache... + val uniques = if (!ignoreConditionals) city.getMatchingUniques(uniqueType, StateForConditionals(city.civ, city)) + else forCivGetMatchingUniques(city.civ, uniqueType, StateForConditionals.IgnoreConditionals) + + city.getLocalMatchingUniques(uniqueType, StateForConditionals.IgnoreConditionals) + return get( "city-${city.id}-${uniqueType.name}-${ignoreConditionals}", - city.getMatchingUniques(uniqueType, stateForConditionals) + uniques ) } @@ -340,12 +347,16 @@ class LocalUniqueCache(val cache:Boolean = true) { civ ) ): Sequence { - val sequence = civ.getMatchingUniques(uniqueType, stateForConditionals) - if (!cache) return sequence // So we don't need to toString the stateForConditionals + val sequence = civ.getMatchingUniques(uniqueType, StateForConditionals.IgnoreConditionals) + // The uniques CACHED are ALL civ uniques, regardless of conditional matching. + // The uniques RETURNED are uniques AFTER conditional matching. + // This allows reuse of the cached values, between runs with different conditionals - + // for example, iterate on all tiles and get StatPercentForObject uniques relevant for each tile, + // each tile will have different conditional state, but they will all reuse the same list of uniques for the civ return get( - "civ-${civ.civName}-${uniqueType.name}-${stateForConditionals}", + "civ-${civ.civName}-${uniqueType.name}", sequence - ) + ).filter { it.conditionalsApply(stateForConditionals) } } /** Get cached results as a sequence */