From f50b88c9a5b9f4955a2632e68000f4cd98d9973f Mon Sep 17 00:00:00 2001 From: Yair Morgenstern Date: Mon, 3 Apr 2023 10:32:09 +0300 Subject: [PATCH] Performance: Save civ tiles and neighbors in cache to update visible tiles faster --- core/src/com/unciv/logic/GameInfo.kt | 4 ++- .../logic/automation/unit/WorkerAutomation.kt | 8 +++++- .../city/managers/CityExpansionManager.kt | 8 +++--- .../managers/CityInfoConquestFunctions.kt | 19 +++++++------- .../transients/CivInfoTransientCache.kt | 26 +++++++++++++------ core/src/com/unciv/logic/trade/TradeLogic.kt | 4 +-- 6 files changed, 43 insertions(+), 26 deletions(-) diff --git a/core/src/com/unciv/logic/GameInfo.kt b/core/src/com/unciv/logic/GameInfo.kt index 4940d4d62a..b5b303001c 100644 --- a/core/src/com/unciv/logic/GameInfo.kt +++ b/core/src/com/unciv/logic/GameInfo.kt @@ -551,8 +551,10 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion ) { for (unit in civInfo.units.getCivUnits()) unit.updateVisibleTiles(false) // this needs to be done after all the units are assigned to their civs and all other transients are set - if(civInfo.playerType == PlayerType.Human) + if (civInfo.playerType == PlayerType.Human) civInfo.exploredRegion.setMapParameters(tileMap.mapParameters) // Required for the correct calculation of the explored region on world wrap maps + + civInfo.cache.updateOurTiles() civInfo.cache.updateSightAndResources() // only run ONCE and not for each unit - this is a huge performance saver! // Since this depends on the cities of ALL civilizations, diff --git a/core/src/com/unciv/logic/automation/unit/WorkerAutomation.kt b/core/src/com/unciv/logic/automation/unit/WorkerAutomation.kt index 3a7e77ec52..6477394d0e 100644 --- a/core/src/com/unciv/logic/automation/unit/WorkerAutomation.kt +++ b/core/src/com/unciv/logic/automation/unit/WorkerAutomation.kt @@ -17,6 +17,7 @@ import com.unciv.logic.map.tile.RoadStatus import com.unciv.logic.map.tile.Tile import com.unciv.models.ruleset.tile.Terrain import com.unciv.models.ruleset.tile.TileImprovement +import com.unciv.models.ruleset.unique.LocalUniqueCache import com.unciv.models.ruleset.unique.UniqueType import com.unciv.ui.screens.worldscreen.unit.actions.UnitActions import com.unciv.utils.Log @@ -370,9 +371,14 @@ class WorkerAutomation( } if (potentialTileImprovements.isEmpty()) return null + val cityUniqueCaches = HashMap() fun getRankingWithImprovement(improvementName: String): Float { val improvement = ruleSet.tileImprovements[improvementName]!! - val stats = tile.stats.getImprovementStats(improvement, civInfo, tile.getCity()) + val city = tile.getCity() + val cache = + if (city == null) LocalUniqueCache(false) + else cityUniqueCaches.getOrPut(city) { LocalUniqueCache() } + val stats = tile.stats.getImprovementStats(improvement, civInfo, tile.getCity(), cache) return Automation.rankStatsValue(stats, unit.civ) } diff --git a/core/src/com/unciv/logic/city/managers/CityExpansionManager.kt b/core/src/com/unciv/logic/city/managers/CityExpansionManager.kt index b3015d6459..823e5893bd 100644 --- a/core/src/com/unciv/logic/city/managers/CityExpansionManager.kt +++ b/core/src/com/unciv/logic/city/managers/CityExpansionManager.kt @@ -147,7 +147,7 @@ class CityExpansionManager : IsPartOfGameInfoSerialization { tile.setOwningCity(null) - city.civ.cache.updateCivResources() + city.civ.cache.updateOurTiles() city.cityStats.update() tile.history.recordRelinquishOwnership(tile) @@ -169,14 +169,13 @@ class CityExpansionManager : IsPartOfGameInfoSerialization { city.tiles = city.tiles.withItem(tile.position) tile.setOwningCity(city) city.population.autoAssignPopulation() - city.civ.cache.updateCivResources() + city.civ.cache.updateOurTiles() city.cityStats.update() for (unit in tile.getUnits().toList()) // toListed because we're modifying if (!unit.civ.diplomacyFunctions.canPassThroughTiles(city.civ)) unit.movement.teleportToClosestMoveableTile() - city.civ.cache.updateViewableTiles() tile.history.recordTakeOwnership(tile) } @@ -187,7 +186,8 @@ class CityExpansionManager : IsPartOfGameInfoSerialization { val location = addNewTileWithCulture() if (location != null) { val locations = LocationAction(location, city.location) - city.civ.addNotification("[" + city.name + "] has expanded its borders!", locations, NotificationCategory.Cities, NotificationIcon.Culture) + city.civ.addNotification("[${city.name}] has expanded its borders!", locations, + NotificationCategory.Cities, NotificationIcon.Culture) } } } diff --git a/core/src/com/unciv/logic/city/managers/CityInfoConquestFunctions.kt b/core/src/com/unciv/logic/city/managers/CityInfoConquestFunctions.kt index 1942897f8e..132c4c33fb 100644 --- a/core/src/com/unciv/logic/city/managers/CityInfoConquestFunctions.kt +++ b/core/src/com/unciv/logic/city/managers/CityInfoConquestFunctions.kt @@ -126,7 +126,6 @@ class CityInfoConquestFunctions(val city: City){ } } - conqueringCiv.cache.updateViewableTiles() // Might see new tiles from this city } @@ -266,18 +265,16 @@ class CityInfoConquestFunctions(val city: City){ } - fun moveToCiv(newCivInfo: Civilization) { + fun moveToCiv(newCiv: Civilization) { + val oldCiv = city.civ city.apply { - val oldCiv = civ - - // Remove/relocate palace for old Civ - need to do this BEFORE we move the cities between // civs so the capitalCityIndicator recognizes the unique buildings of the conquered civ if (oldCiv.getCapital() == this) oldCiv.moveCapitalToNextLargest() oldCiv.cities = oldCiv.cities.toMutableList().apply { remove(city) } - newCivInfo.cities = newCivInfo.cities.toMutableList().apply { add(city) } - civ = newCivInfo + newCiv.cities = newCiv.cities.toMutableList().apply { add(city) } + civ = newCiv hasJustBeenConquered = false turnAcquired = civ.gameInfo.turns previousOwner = oldCiv.civName @@ -297,8 +294,8 @@ class CityInfoConquestFunctions(val city: City){ // Place palace for newCiv if this is the only city they have // This needs to happen _before_ free buildings are added, as sometimes these should // only be placed in the capital, and then there needs to be a capital. - if (newCivInfo.cities.size == 1) { - newCivInfo.moveCapitalTo(this) + if (newCiv.cities.size == 1) { + newCiv.moveCapitalTo(this) } // Add our free buildings to this city and add free buildings provided by the city to other cities @@ -308,7 +305,7 @@ class CityInfoConquestFunctions(val city: City){ // Transfer unique buildings for (building in cityConstructions.getBuiltBuildings()) { - val civEquivalentBuilding = newCivInfo.getEquivalentBuilding(building.name) + val civEquivalentBuilding = newCiv.getEquivalentBuilding(building.name) if (building != civEquivalentBuilding) { cityConstructions.removeBuilding(building.name) cityConstructions.addBuilding(civEquivalentBuilding.name) @@ -329,6 +326,8 @@ class CityInfoConquestFunctions(val city: City){ tile.history.recordTakeOwnership(tile) } } + newCiv.cache.updateOurTiles() + oldCiv.cache.updateOurTiles() } } diff --git a/core/src/com/unciv/logic/civilization/transients/CivInfoTransientCache.kt b/core/src/com/unciv/logic/civilization/transients/CivInfoTransientCache.kt index 40d8279147..9a65983151 100644 --- a/core/src/com/unciv/logic/civilization/transients/CivInfoTransientCache.kt +++ b/core/src/com/unciv/logic/civilization/transients/CivInfoTransientCache.kt @@ -138,8 +138,24 @@ class CivInfoTransientCache(val civInfo: Civilization) { civInfo.viewableInvisibleUnitsTiles = newViewableInvisibleTiles } + private var ourTilesAndNeighboringTiles: Set = HashSet() + + /** Our tiles update pretty infrequently - most 'viewable tile' changes are due to unit movements, + * which means we can store this separately and use it 'as is' so we don't need to find the neighboring tiles every time + * a unit moves */ + fun updateOurTiles(){ + val newOurTilesAndNeighboring = HashSet() + val ownedTiles = civInfo.cities.asSequence().flatMap { it.getTiles() } + newOurTilesAndNeighboring.addAll(ownedTiles) + val neighboringUnownedTiles = ownedTiles.flatMap { tile -> tile.neighbors.filter { it.getOwner() != civInfo } } + newOurTilesAndNeighboring.addAll(neighboringUnownedTiles) + ourTilesAndNeighboringTiles = newOurTilesAndNeighboring + + updateViewableTiles() + updateCivResources() + } + private fun setNewViewableTiles() { - val newViewableTiles = HashSet() // while spectating all map is visible if (civInfo.isSpectator() || DebugUtils.VISIBLE_MAP) { @@ -149,13 +165,7 @@ class CivInfoTransientCache(val civInfo: Civilization) { return } - // There are a LOT of tiles usually. - // And making large lists of them just as intermediaries before we shove them into the hashset is very space-inefficient. - // And so, sequences to the rescue! - val ownedTiles = civInfo.cities.asSequence().flatMap { it.getTiles() } - newViewableTiles.addAll(ownedTiles) - val neighboringUnownedTiles = ownedTiles.flatMap { tile -> tile.neighbors.filter { it.getOwner() != civInfo } } - newViewableTiles.addAll(neighboringUnownedTiles) + val newViewableTiles = HashSet(ourTilesAndNeighboringTiles) newViewableTiles.addAll(civInfo.units.getCivUnits().flatMap { unit -> unit.viewableTiles.asSequence().filter { it.getOwner() != civInfo } }) for (otherCiv in civInfo.getKnownCivs()) { diff --git a/core/src/com/unciv/logic/trade/TradeLogic.kt b/core/src/com/unciv/logic/trade/TradeLogic.kt index 97bd523c00..4941e5beb4 100644 --- a/core/src/com/unciv/logic/trade/TradeLogic.kt +++ b/core/src/com/unciv/logic/trade/TradeLogic.kt @@ -105,8 +105,8 @@ class TradeLogic(val ourCivilization:Civilization, val otherCivilization: Civili unit.movement.teleportToClosestMoveableTile() } } - to.cache.updateViewableTiles() - from.cache.updateViewableTiles() + to.cache.updateOurTiles() + from.cache.updateOurTiles() // suggest an option to liberate the city if (to.isHuman()