City construction speedup with caching stats from tiles (#5536)

* City construction speedup with caching stats from tiles

* Reduced cityStats.update to only one cityConstructions.getStats() call, improving performance by another 30% approx
This commit is contained in:
Yair Morgenstern 2021-10-24 09:05:05 +03:00 committed by GitHub
parent 9a7ea263d6
commit 6ac3547b8e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 57 additions and 41 deletions

View File

@ -337,7 +337,9 @@ class GameInfo {
civInfo.initialSetCitiesConnectedToCapitalTransients() civInfo.initialSetCitiesConnectedToCapitalTransients()
// We need to determine the GLOBAL happiness state in order to determine the city stats // We need to determine the GLOBAL happiness state in order to determine the city stats
for (cityInfo in civInfo.cities) cityInfo.cityStats.updateCityHappiness() for (cityInfo in civInfo.cities) cityInfo.cityStats.updateCityHappiness(
cityInfo.cityConstructions.getStats()
)
for (cityInfo in civInfo.cities) { for (cityInfo in civInfo.cities) {
/** We remove constructions from the queue that aren't defined in the ruleset. /** We remove constructions from the queue that aren't defined in the ruleset.

View File

@ -317,8 +317,9 @@ class CityConstructions {
SO, we create an entirely new CityStats and iterate there - problem solve! SO, we create an entirely new CityStats and iterate there - problem solve!
*/ */
val cityStats = CityStats(cityInfo) val cityStats = CityStats(cityInfo)
cityStats.statsFromTiles = cityInfo.cityStats.statsFromTiles // take as-is
val construction = cityInfo.cityConstructions.getConstruction(constructionName) val construction = cityInfo.cityConstructions.getConstruction(constructionName)
cityStats.update(construction) cityStats.update(construction, false)
cityStatsForConstruction = cityStats.currentCityStats cityStatsForConstruction = cityStats.currentCityStats
} }

View File

@ -34,6 +34,8 @@ class CityStats(val cityInfo: CityInfo) {
var happinessList = LinkedHashMap<String, Float>() var happinessList = LinkedHashMap<String, Float>()
var statsFromTiles = Stats()
var foodEaten = 0f var foodEaten = 0f
var currentCityStats: Stats = Stats() // This is so we won't have to calculate this multiple times - takes a lot of time, especially on phones var currentCityStats: Stats = Stats() // This is so we won't have to calculate this multiple times - takes a lot of time, especially on phones
@ -41,16 +43,6 @@ class CityStats(val cityInfo: CityInfo) {
//endregion //endregion
//region Pure Functions //region Pure Functions
private fun getStatsFromTiles(): Stats {
val stats = Stats()
for (cell in cityInfo.tilesInRange
.filter { cityInfo.location == it.position || cityInfo.isWorked(it) ||
it.owningCity == cityInfo && (it.getTileImprovement()?.hasUnique(UniqueType.TileProvidesYieldWithoutPopulation) == true ||
it.hasUnique(UniqueType.TileProvidesYieldWithoutPopulation))
})
stats.add(cell.getTileStats(cityInfo, cityInfo.civInfo))
return stats
}
private fun getStatsFromTradeRoute(): Stats { private fun getStatsFromTradeRoute(): Stats {
val stats = Stats() val stats = Stats()
@ -127,9 +119,9 @@ class CityStats(val cityInfo: CityInfo) {
stats.food += 2 stats.food += 2
} else { } else {
for (bonus in eraInfo.getCityStateBonuses(otherCiv.cityStateType, relationshipLevel)) { for (bonus in eraInfo.getCityStateBonuses(otherCiv.cityStateType, relationshipLevel)) {
if (bonus.isOfType(UniqueType.CityStateStatsPerCity) if (bonus.isOfType(UniqueType.CityStateStatsPerCity)
&& cityInfo.matchesFilter(bonus.params[1]) && cityInfo.matchesFilter(bonus.params[1])
&& bonus.conditionalsApply(otherCiv, cityInfo) && bonus.conditionalsApply(otherCiv, cityInfo)
) stats.add(bonus.stats) ) stats.add(bonus.stats)
} }
} }
@ -212,8 +204,8 @@ class CityStats(val cityInfo: CityInfo) {
stats.add(unique.stats) stats.add(unique.stats)
} }
if (unique.isOfType(UniqueType.StatsPerCity) if (unique.isOfType(UniqueType.StatsPerCity)
&& cityInfo.matchesFilter(unique.params[1]) && cityInfo.matchesFilter(unique.params[1])
&& unique.conditionalsApply(cityInfo.civInfo, cityInfo) && unique.conditionalsApply(cityInfo.civInfo, cityInfo)
) { ) {
stats.add(unique.stats) stats.add(unique.stats)
@ -254,12 +246,12 @@ class CityStats(val cityInfo: CityInfo) {
val uniques = uniqueSequence.toList().asSequence() val uniques = uniqueSequence.toList().asSequence()
// Since this is sometimes run from a different thread (getConstructionButtonDTOs), // Since this is sometimes run from a different thread (getConstructionButtonDTOs),
// this helps mitigate concurrency problems. // this helps mitigate concurrency problems.
for (unique in uniques.filter { it.isOfType(UniqueType.StatPercentBonus) }) { for (unique in uniques.filter { it.isOfType(UniqueType.StatPercentBonus) }) {
if (!unique.conditionalsApply(cityInfo.civInfo, cityInfo)) continue if (!unique.conditionalsApply(cityInfo.civInfo, cityInfo)) continue
stats.add(Stat.valueOf(unique.params[1]), unique.params[0].toFloat()) stats.add(Stat.valueOf(unique.params[1]), unique.params[0].toFloat())
} }
// Deprecated since 3.17.0 // Deprecated since 3.17.0
// For instance "+[50]% [Production] // For instance "+[50]% [Production]
for (unique in uniques.filter { it.placeholderText == "+[]% [] in all cities"}) for (unique in uniques.filter { it.placeholderText == "+[]% [] in all cities"})
@ -273,16 +265,16 @@ class CityStats(val cityInfo: CityInfo) {
if (cityInfo.matchesFilter(unique.params[2])) if (cityInfo.matchesFilter(unique.params[2]))
stats.add(Stat.valueOf(unique.params[1]), unique.params[0].toFloat()) stats.add(Stat.valueOf(unique.params[1]), unique.params[0].toFloat())
// //
for (unique in uniques.filter { it.isOfType(UniqueType.StatPercentBonusCities) }) { for (unique in uniques.filter { it.isOfType(UniqueType.StatPercentBonusCities) }) {
if (!unique.conditionalsApply(StateForConditionals(civInfo = cityInfo.civInfo, cityInfo = cityInfo))) continue if (!unique.conditionalsApply(StateForConditionals(civInfo = cityInfo.civInfo, cityInfo = cityInfo))) continue
if (cityInfo.matchesFilter(unique.params[2])) if (cityInfo.matchesFilter(unique.params[2]))
stats.add(Stat.valueOf(unique.params[1]), unique.params[0].toFloat()) stats.add(Stat.valueOf(unique.params[1]), unique.params[0].toFloat())
} }
val uniquesToCheck = val uniquesToCheck =
if (currentConstruction is Building && !currentConstruction.isAnyWonder()) { if (currentConstruction is Building && !currentConstruction.isAnyWonder()) {
uniques.filter { it.isOfType(UniqueType.PercentProductionWonders) } uniques.filter { it.isOfType(UniqueType.PercentProductionWonders) }
} else if (currentConstruction is Building && currentConstruction.isAnyWonder()) { } else if (currentConstruction is Building && currentConstruction.isAnyWonder()) {
uniques.filter { it.isOfType(UniqueType.PercentProductionBuildings) } uniques.filter { it.isOfType(UniqueType.PercentProductionBuildings) }
} else if (currentConstruction is BaseUnit) { } else if (currentConstruction is BaseUnit) {
@ -312,13 +304,13 @@ class CityStats(val cityInfo: CityInfo) {
if (constructionMatchesFilter(currentConstruction, unique.params[1])) if (constructionMatchesFilter(currentConstruction, unique.params[1]))
stats.production += unique.params[0].toInt() stats.production += unique.params[0].toInt()
} }
// "+[amount]% Production when constructing [constructionFilter] [cityFilter]" // "+[amount]% Production when constructing [constructionFilter] [cityFilter]"
for (unique in uniques.filter { it.isOfType(UniqueType.PercentProductionConstructionsCities) }) { for (unique in uniques.filter { it.isOfType(UniqueType.PercentProductionConstructionsCities) }) {
if (constructionMatchesFilter(currentConstruction, unique.params[1]) && cityInfo.matchesFilter(unique.params[2])) if (constructionMatchesFilter(currentConstruction, unique.params[1]) && cityInfo.matchesFilter(unique.params[2]))
stats.production += unique.params[0].toInt() stats.production += unique.params[0].toInt()
} }
// "+[amount]% Production when constructing [unitFilter] units [cityFilter]" // "+[amount]% Production when constructing [unitFilter] units [cityFilter]"
for (unique in uniques.filter { it.isOfType(UniqueType.PercentProductionUnitsDeprecated) }) { for (unique in uniques.filter { it.isOfType(UniqueType.PercentProductionUnitsDeprecated) }) {
if (constructionMatchesFilter(currentConstruction, unique.params[1]) && cityInfo.matchesFilter(unique.params[2])) if (constructionMatchesFilter(currentConstruction, unique.params[1]) && cityInfo.matchesFilter(unique.params[2]))
@ -332,12 +324,12 @@ class CityStats(val cityInfo: CityInfo) {
stats.add(Stat.valueOf(unique.params[1]), unique.params[0].toFloat()) stats.add(Stat.valueOf(unique.params[1]), unique.params[0].toFloat())
} }
// //
for (unique in uniques.filter { it.placeholderText == "[]% [] from every follower, up to []%" }) for (unique in uniques.filter { it.placeholderText == "[]% [] from every follower, up to []%" })
stats.add( stats.add(
Stat.valueOf(unique.params[1]), Stat.valueOf(unique.params[1]),
min( min(
unique.params[0].toFloat() * cityInfo.religion.getFollowersOfMajorityReligion(), unique.params[0].toFloat() * cityInfo.religion.getFollowersOfMajorityReligion(),
unique.params[2].toFloat() unique.params[2].toFloat()
) )
) )
@ -363,8 +355,8 @@ class CityStats(val cityInfo: CityInfo) {
if (cityInfo.civInfo.cities.count() < 2) return false// first city! if (cityInfo.civInfo.cities.count() < 2) return false// first city!
// Railroad, or harbor from railroad // Railroad, or harbor from railroad
return if (roadType == RoadStatus.Railroad) return if (roadType == RoadStatus.Railroad)
cityInfo.isConnectedToCapital { cityInfo.isConnectedToCapital {
roadTypes -> roadTypes ->
roadTypes.any { it.contains(RoadStatus.Railroad.name) } roadTypes.any { it.contains(RoadStatus.Railroad.name) }
} }
@ -389,9 +381,24 @@ class CityStats(val cityInfo: CityInfo) {
//endregion //endregion
//region State-Changing Methods //region State-Changing Methods
fun updateTileStats() {
val stats = Stats()
for (cell in cityInfo.tilesInRange
.filter {
cityInfo.location == it.position
|| cityInfo.isWorked(it)
|| it.owningCity == cityInfo && (it.getTileImprovement()
?.hasUnique(UniqueType.TileProvidesYieldWithoutPopulation) == true
|| it.hasUnique(UniqueType.TileProvidesYieldWithoutPopulation))
})
stats.add(cell.getTileStats(cityInfo, cityInfo.civInfo))
statsFromTiles = stats
}
// needs to be a separate function because we need to know the global happiness state // needs to be a separate function because we need to know the global happiness state
// in order to determine how much food is produced in a city! // in order to determine how much food is produced in a city!
fun updateCityHappiness() { fun updateCityHappiness(statsFromBuildings: Stats) {
val civInfo = cityInfo.civInfo val civInfo = cityInfo.civInfo
val newHappinessList = LinkedHashMap<String, Float>() val newHappinessList = LinkedHashMap<String, Float>()
var unhappinessModifier = civInfo.getDifficulty().unhappinessModifier var unhappinessModifier = civInfo.getDifficulty().unhappinessModifier
@ -439,8 +446,7 @@ class CityStats(val cityInfo: CityInfo) {
val happinessFromSpecialists = getStatsFromSpecialists(cityInfo.population.getNewSpecialists()).happiness.toInt().toFloat() val happinessFromSpecialists = getStatsFromSpecialists(cityInfo.population.getNewSpecialists()).happiness.toInt().toFloat()
if (happinessFromSpecialists > 0) newHappinessList["Specialists"] = happinessFromSpecialists if (happinessFromSpecialists > 0) newHappinessList["Specialists"] = happinessFromSpecialists
val happinessFromBuildings = cityInfo.cityConstructions.getStats().happiness.toInt().toFloat() newHappinessList["Buildings"] = statsFromBuildings.happiness.toInt().toFloat()
newHappinessList["Buildings"] = happinessFromBuildings
newHappinessList["National ability"] = getStatsFromUniques(cityInfo.civInfo.nation.uniqueObjects.asSequence()).happiness newHappinessList["National ability"] = getStatsFromUniques(cityInfo.civInfo.nation.uniqueObjects.asSequence()).happiness
@ -448,14 +454,14 @@ class CityStats(val cityInfo: CityInfo) {
newHappinessList["Religion"] = getStatsFromUniques(cityInfo.religion.getUniques()).happiness newHappinessList["Religion"] = getStatsFromUniques(cityInfo.religion.getUniques()).happiness
newHappinessList["Tile yields"] = getStatsFromTiles().happiness newHappinessList["Tile yields"] = statsFromTiles.happiness
// we don't want to modify the existing happiness list because that leads // we don't want to modify the existing happiness list because that leads
// to concurrency problems if we iterate on it while changing // to concurrency problems if we iterate on it while changing
happinessList = newHappinessList happinessList = newHappinessList
} }
private fun updateBaseStatList() { private fun updateBaseStatList(statsFromBuildings: Stats) {
val newBaseStatList = LinkedHashMap<String, Stats>() // we don't edit the existing baseStatList directly, in order to avoid concurrency exceptions val newBaseStatList = LinkedHashMap<String, Stats>() // we don't edit the existing baseStatList directly, in order to avoid concurrency exceptions
val civInfo = cityInfo.civInfo val civInfo = cityInfo.civInfo
@ -463,10 +469,10 @@ class CityStats(val cityInfo: CityInfo) {
science = cityInfo.population.population.toFloat(), science = cityInfo.population.population.toFloat(),
production = cityInfo.population.getFreePopulation().toFloat() production = cityInfo.population.getFreePopulation().toFloat()
) )
newBaseStatList["Tile yields"] = getStatsFromTiles() newBaseStatList["Tile yields"] = statsFromTiles
newBaseStatList["Specialists"] = getStatsFromSpecialists(cityInfo.population.getNewSpecialists()) newBaseStatList["Specialists"] = getStatsFromSpecialists(cityInfo.population.getNewSpecialists())
newBaseStatList["Trade routes"] = getStatsFromTradeRoute() newBaseStatList["Trade routes"] = getStatsFromTradeRoute()
newBaseStatList["Buildings"] = cityInfo.cityConstructions.getStats() newBaseStatList["Buildings"] = statsFromBuildings
newBaseStatList["Policies"] = getStatsFromUniques(civInfo.policies.policyUniques.getAllUniques()) newBaseStatList["Policies"] = getStatsFromUniques(civInfo.policies.policyUniques.getAllUniques())
newBaseStatList["National ability"] = getStatsFromNationUnique() newBaseStatList["National ability"] = getStatsFromNationUnique()
newBaseStatList["Wonders"] = getStatsFromUniques(civInfo.getCivWideBuildingUniques(cityInfo)) newBaseStatList["Wonders"] = getStatsFromUniques(civInfo.getCivWideBuildingUniques(cityInfo))
@ -503,7 +509,11 @@ class CityStats(val cityInfo: CityInfo) {
statPercentBonusList = newStatPercentBonusList statPercentBonusList = newStatPercentBonusList
} }
fun update(currentConstruction: IConstruction = cityInfo.cityConstructions.getCurrentConstruction()) { /** Does not update tile stats - instead, updating tile stats updates this */
fun update(currentConstruction: IConstruction = cityInfo.cityConstructions.getCurrentConstruction(),
updateTileStats:Boolean = true) {
if (updateTileStats) updateTileStats()
// We calculate this here for concurrency reasons // We calculate this here for concurrency reasons
// If something needs this, we pass this through as a parameter // If something needs this, we pass this through as a parameter
val localBuildingUniques = cityInfo.cityConstructions.builtBuildingUniqueMap.getAllUniques() val localBuildingUniques = cityInfo.cityConstructions.builtBuildingUniqueMap.getAllUniques()
@ -516,8 +526,10 @@ class CityStats(val cityInfo: CityInfo) {
val citySpecificUniques = cityInfo.getAllLocalUniques() val citySpecificUniques = cityInfo.getAllLocalUniques()
// We need to compute Tile yields before happiness // We need to compute Tile yields before happiness
updateBaseStatList()
updateCityHappiness() val statsFromBuildings = cityInfo.cityConstructions.getStats() // this is performance heavy, so calculate once
updateBaseStatList(statsFromBuildings)
updateCityHappiness(statsFromBuildings)
updateStatPercentBonusList(currentConstruction, localBuildingUniques) updateStatPercentBonusList(currentConstruction, localBuildingUniques)
updateFinalStatList(currentConstruction, citySpecificUniques) // again, we don't edit the existing currentCityStats directly, in order to avoid concurrency exceptions updateFinalStatList(currentConstruction, citySpecificUniques) // again, we don't edit the existing currentCityStats directly, in order to avoid concurrency exceptions

View File

@ -628,7 +628,7 @@ open class TileInfo {
if (isCityCenter()) { if (isCityCenter()) {
val city = getCity()!! val city = getCity()!!
var cityString = city.name.tr() var cityString = city.name.tr()
if (isViewableToPlayer) cityString += " (" + city.health + ")" if (isViewableToPlayer) cityString += " (${city.health})"
lineList += FormattedLine(cityString) lineList += FormattedLine(cityString)
if (UncivGame.Current.viewEntireMapForDebug || city.civInfo == viewingCiv) if (UncivGame.Current.viewEntireMapForDebug || city.civInfo == viewingCiv)
lineList += city.cityConstructions.getProductionMarkup(ruleset) lineList += city.cityConstructions.getProductionMarkup(ruleset)

View File

@ -161,6 +161,8 @@ class CityConstructionsTable(private val cityScreen: CityScreen) {
val constructionsSequence = city.getRuleset().units.values.asSequence() + val constructionsSequence = city.getRuleset().units.values.asSequence() +
city.getRuleset().buildings.values.asSequence() city.getRuleset().buildings.values.asSequence()
city.cityStats.updateTileStats() // only once
for (entry in constructionsSequence.filter { it.shouldBeDisplayed(cityConstructions) }) { for (entry in constructionsSequence.filter { it.shouldBeDisplayed(cityConstructions) }) {
val useStoredProduction = entry is Building || !cityConstructions.isBeingConstructedOrEnqueued(entry.name) val useStoredProduction = entry is Building || !cityConstructions.isBeingConstructedOrEnqueued(entry.name)
var buttonText = entry.name.tr() + cityConstructions.getTurnsToConstructionString(entry.name, useStoredProduction) var buttonText = entry.name.tr() + cityConstructions.getTurnsToConstructionString(entry.name, useStoredProduction)

View File

@ -81,7 +81,6 @@ class CityInfoTable(private val cityScreen: CityScreen) : Table(CameraStageBaseS
YesNoPopup("Are you sure you want to sell this [${building.name}]?".tr(), YesNoPopup("Are you sure you want to sell this [${building.name}]?".tr(),
{ {
cityScreen.city.sellBuilding(building.name) cityScreen.city.sellBuilding(building.name)
cityScreen.city.cityStats.update()
cityScreen.update() cityScreen.update()
}, cityScreen, }, cityScreen,
{ {