perf: Unique mapping overhaul - don't multiply when checking 'has unique', prep for EnumMap for typed uniques

This commit is contained in:
yairm210
2024-09-08 00:23:51 +03:00
parent eff0a5bebb
commit 4d640a254c
12 changed files with 78 additions and 75 deletions

View File

@ -47,7 +47,7 @@ class VictoryManager : IsPartOfGameInfoSerialization {
fun getUNBuildingAndOwnerNames(): Pair<String?, String?> = getVotingCivs()
.flatMap { civ -> civ.cities.asSequence()
.flatMap { it.cityConstructions.getBuiltBuildings() }
.filter { it.hasUnique(UniqueType.OneTimeTriggerVoting, stateForConditionals = StateForConditionals.IgnoreConditionals) }
.filter { it.hasUnique(UniqueType.OneTimeTriggerVoting, StateForConditionals.IgnoreConditionals) }
.map { it.name to civ.civName }
}.firstOrNull() ?: (null to null)

View File

@ -49,13 +49,14 @@ class CivInfoTransientCache(val civInfo: Civilization) {
fun setTransients() {
val ruleset = civInfo.gameInfo.ruleset
val state = StateForConditionals(civInfo)
val buildingsToRequiredResources = ruleset.buildings.values
.filter { civInfo.getEquivalentBuilding(it) == it }
.associateWith { it.requiredResources() }
.associateWith { it.requiredResources(state) }
val unitsToRequiredResources = ruleset.units.values
.filter { civInfo.getEquivalentUnit(it) == it }
.associateWith { it.requiredResources() }
.associateWith { it.requiredResources(state) }
for (resource in ruleset.tileResources.values.asSequence().filter { it.resourceType == ResourceType.Strategic }.map { it.name }) {
val applicableBuildings = buildingsToRequiredResources.filter { it.value.contains(resource) }.map { it.key }

View File

@ -276,7 +276,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
return !(isFortified() || isExploring() || isSleeping() || isAutomated() || isMoving())
}
fun getUniques(): Sequence<Unique> = tempUniquesMap.values.asSequence().flatten()
fun getUniques(): Sequence<Unique> = tempUniquesMap.getAllUniques()
fun getMatchingUniques(
uniqueType: UniqueType,
@ -577,7 +577,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
else -> {
if (baseUnit.matchesFilter(filter)) return true
if (civ.matchesFilter(filter)) return true
if (tempUniquesMap.containsKey(filter)) return true
if (tempUniquesMap.hasTagUnique(filter)) return true
if (promotions.promotions.contains(filter)) return true
return false
}

View File

@ -536,10 +536,11 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction {
fun isSellable() = !isAnyWonder() && !hasUnique(UniqueType.Unsellable)
override fun getResourceRequirementsPerTurn(stateForConditionals: StateForConditionals?): Counter<String> {
override fun getResourceRequirementsPerTurn(state: StateForConditionals?): Counter<String> {
val resourceRequirements = Counter<String>()
if (requiredResource != null) resourceRequirements[requiredResource!!] = 1
for (unique in getMatchingUniques(UniqueType.ConsumesResources, stateForConditionals))
for (unique in getMatchingUniques(UniqueType.ConsumesResources,
state ?: StateForConditionals.EmptyState))
resourceRequirements[unique.params[1]] += unique.params[0].toInt()
return resourceRequirements
}

View File

@ -20,9 +20,9 @@ interface IConstruction : INamed {
fun isBuildable(cityConstructions: CityConstructions): Boolean
fun shouldBeDisplayed(cityConstructions: CityConstructions): Boolean
/** Gets *per turn* resource requirements - does not include immediate costs for stockpiled resources.
* Uses [stateForConditionals] to determine which civ or city this is built for*/
fun getResourceRequirementsPerTurn(stateForConditionals: StateForConditionals? = null): Counter<String>
fun requiredResources(stateForConditionals: StateForConditionals? = null): Set<String>
* Uses [state] to determine which civ or city this is built for*/
fun getResourceRequirementsPerTurn(state: StateForConditionals? = null): Counter<String>
fun requiredResources(state: StateForConditionals = StateForConditionals.EmptyState): Set<String>
/** We can't call this getMatchingUniques because then it would conflict with IHasUniques */
fun getMatchingUniquesNotConflicting(uniqueType: UniqueType, stateForConditionals: StateForConditionals) = sequenceOf<Unique>()
}
@ -105,9 +105,9 @@ interface INonPerpetualConstruction : IConstruction, INamed, IHasUniques {
override fun getMatchingUniquesNotConflicting(uniqueType: UniqueType, stateForConditionals: StateForConditionals): Sequence<Unique> =
getMatchingUniques(uniqueType, stateForConditionals)
override fun requiredResources(stateForConditionals: StateForConditionals?): Set<String> {
return getResourceRequirementsPerTurn(stateForConditionals).keys +
getMatchingUniques(UniqueType.CostsResources, stateForConditionals).map { it.params[1] }
override fun requiredResources(state: StateForConditionals): Set<String> {
return getResourceRequirementsPerTurn(state).keys +
getMatchingUniques(UniqueType.CostsResources, state).map { it.params[1] }
}
}
@ -256,9 +256,9 @@ open class PerpetualConstruction(override var name: String, val description: Str
override fun isBuildable(cityConstructions: CityConstructions): Boolean =
throw Exception("Impossible!")
override fun getResourceRequirementsPerTurn(stateForConditionals: StateForConditionals?) = Counter.ZERO
override fun getResourceRequirementsPerTurn(state: StateForConditionals?) = Counter.ZERO
override fun requiredResources(stateForConditionals: StateForConditionals?): Set<String> = emptySet()
override fun requiredResources(state: StateForConditionals): Set<String> = emptySet()
}
open class PerpetualStatConversion(val stat: Stat) :

View File

@ -96,8 +96,8 @@ class Nation : RulesetObject() {
innerColorObject = if (innerColor == null) Color.BLACK
else colorFromRGB(innerColor!!)
forestsAndJunglesAreRoads = uniqueMap.containsKey(UniqueType.ForestsAndJunglesAreRoads.placeholderText)
ignoreHillMovementCost = uniqueMap.containsKey(UniqueType.IgnoreHillMovementCost.placeholderText)
forestsAndJunglesAreRoads = uniqueMap.hasUnique(UniqueType.ForestsAndJunglesAreRoads)
ignoreHillMovementCost = uniqueMap.hasUnique(UniqueType.IgnoreHillMovementCost)
}

View File

@ -45,7 +45,7 @@ class TileImprovement : RulesetStatsObject() {
fun getShortDecription() = ImprovementDescriptions.getShortDescription(this)
fun isGreatImprovement() = hasUnique(UniqueType.GreatImprovement)
fun isRoad() = RoadStatus.values().any { it != RoadStatus.None && it.name == this.name }
fun isRoad() = RoadStatus.entries.any { it != RoadStatus.None && it.name == this.name }
fun isAncientRuinsEquivalent() = hasUnique(UniqueType.IsAncientRuinsEquivalent)
fun canBeBuiltOn(terrain: String): Boolean {
@ -83,8 +83,7 @@ class TileImprovement : RulesetStatsObject() {
"Improvement" -> true // For situations involving tileFilter
"All Road" -> isRoad()
"Great Improvement", "Great" -> isGreatImprovement()
in uniqueMap -> true
else -> false
else -> uniqueMap.hasTagUnique(filter)
}
}
@ -120,7 +119,8 @@ class TileImprovement : RulesetStatsObject() {
val terrainsCanBeBuiltOnTypes = sequence {
yieldAll(expandedTerrainsCanBeBuiltOn.asSequence()
.mapNotNull { ruleset.terrains[it]?.type })
yieldAll(TerrainType.values().asSequence()
yieldAll(
TerrainType.entries.asSequence()
.filter { it.name in expandedTerrainsCanBeBuiltOn })
}.filter { it.name !in cannotFilters }.toMutableSet()

View File

@ -18,7 +18,7 @@ interface IHasUniques : INamed {
// Every implementation should override these with the same `by lazy (::thingsProvider)`
// AND every implementation should annotate these with `@delegate:Transient`
val uniqueObjects: List<Unique>
val uniqueMap: Map<String, List<Unique>>
val uniqueMap: UniqueMap
fun uniqueObjectsProvider(): List<Unique> {
if (uniques.isEmpty()) return emptyList()
@ -36,24 +36,15 @@ interface IHasUniques : INamed {
* */
fun getUniqueTarget(): UniqueTarget
fun getMatchingUniques(uniqueTemplate: String, stateForConditionals: StateForConditionals? = null): Sequence<Unique> {
val matchingUniques = uniqueMap[uniqueTemplate]
?: return emptySequence()
val actualStateForConditionals = stateForConditionals ?: StateForConditionals()
val uniques = matchingUniques.asSequence().filter { it.conditionalsApply(actualStateForConditionals) }
return uniques.flatMap { it.getMultiplied(actualStateForConditionals) }
}
fun getMatchingUniques(uniqueType: UniqueType, stateForConditionals: StateForConditionals? = null) =
getMatchingUniques(uniqueType.placeholderText, stateForConditionals)
fun hasUnique(uniqueTemplate: String, stateForConditionals: StateForConditionals? = null) =
getMatchingUniques(uniqueTemplate, stateForConditionals).any()
fun hasUnique(uniqueType: UniqueType, stateForConditionals: StateForConditionals? = null) =
getMatchingUniques(uniqueType.placeholderText, stateForConditionals).any()
fun getMatchingUniques(uniqueType: UniqueType, state: StateForConditionals = StateForConditionals.EmptyState) =
uniqueMap.getMatchingUniques(uniqueType, state)
fun hasUnique(uniqueType: UniqueType, state: StateForConditionals? = null) =
uniqueMap.hasMatchingUnique(uniqueType, state ?: StateForConditionals.EmptyState)
fun hasUnique(tagUnique: String) =
uniqueMap.hasTagUnique(tagUnique)
fun availabilityUniques(): Sequence<Unique> = getMatchingUniques(UniqueType.OnlyAvailable, StateForConditionals.IgnoreConditionals) + getMatchingUniques(UniqueType.CanOnlyBeBuiltWhen, StateForConditionals.IgnoreConditionals)
fun techsRequiredByUniques(): Sequence<String> {

View File

@ -92,6 +92,7 @@ data class StateForConditionals(
companion object {
val IgnoreConditionals = StateForConditionals(ignoreConditionals = true)
val EmptyState = StateForConditionals()
}
/** Used ONLY for stateBasedRandom in [Conditionals.conditionalApplies] to prevent save scumming on [UniqueType.ConditionalChance] */

View File

@ -241,10 +241,12 @@ class LocalUniqueCache(val cache: Boolean = true) {
}
}
class UniqueMap() : HashMap<String, ArrayList<Unique>>() {
class UniqueMap() {
//todo Once all untyped Uniques are converted, this should be HashMap<UniqueType, *>
// For now, we can have both map types "side by side" each serving their own purpose,
// and gradually this one will be deprecated in favor of the other
private val innerUniqueMap = HashMap<String, ArrayList<Unique>>()
constructor(uniques: Sequence<Unique>) : this() {
addUniques(uniques.asIterable())
@ -252,9 +254,9 @@ class UniqueMap() : HashMap<String, ArrayList<Unique>>() {
/** Adds one [unique] unless it has a ConditionalTimedUnique conditional */
fun addUnique(unique: Unique) {
val existingArrayList = get(unique.placeholderText)
val existingArrayList = innerUniqueMap[unique.placeholderText]
if (existingArrayList != null) existingArrayList.add(unique)
else this[unique.placeholderText] = arrayListOf(unique)
else innerUniqueMap[unique.placeholderText] = arrayListOf(unique)
}
/** Calls [addUnique] on each item from [uniques] */
@ -263,18 +265,32 @@ class UniqueMap() : HashMap<String, ArrayList<Unique>>() {
}
fun removeUnique(unique: Unique) {
val existingArrayList = get(unique.placeholderText)
val existingArrayList = innerUniqueMap[unique.placeholderText]
existingArrayList?.remove(unique)
}
fun clear() = innerUniqueMap.clear()
// Pure functions
fun hasUnique(uniqueType: UniqueType, state: StateForConditionals = StateForConditionals.EmptyState) =
getUniques(uniqueType).any { it.conditionalsApply(state) && !it.isTimedTriggerable }
fun hasTagUnique(tagUnique: String) =
innerUniqueMap.containsKey(tagUnique)
fun getUniques(uniqueType: UniqueType) =
this[uniqueType.placeholderText]?.asSequence() ?: emptySequence()
innerUniqueMap[uniqueType.placeholderText]?.asSequence() ?: emptySequence()
fun getMatchingUniques(uniqueType: UniqueType, state: StateForConditionals) = getUniques(uniqueType)
.filter { it.conditionalsApply(state) && !it.isTimedTriggerable }
.flatMap { it.getMultiplied(state) }
fun getMatchingUniques(uniqueType: UniqueType, state: StateForConditionals = StateForConditionals.EmptyState) =
getUniques(uniqueType)
.filter { it.conditionalsApply(state) && !it.isTimedTriggerable }
.flatMap { it.getMultiplied(state) }
fun hasMatchingUnique(uniqueType: UniqueType, state: StateForConditionals = StateForConditionals.EmptyState) =
getUniques(uniqueType).any { it.conditionalsApply(state) }
fun getAllUniques() = this.asSequence().flatMap { it.value.asSequence() }
fun getAllUniques() = innerUniqueMap.values.asSequence().flatten()
fun getTriggeredUniques(trigger: UniqueType, stateForConditionals: StateForConditionals): Sequence<Unique> {
return getAllUniques().filter { unique ->

View File

@ -86,7 +86,7 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction {
super<INonPerpetualConstruction>.isHiddenBySettings(gameInfo) ||
(!gameInfo.gameParameters.nuclearWeaponsEnabled && isNuclearWeapon())
fun getUpgradeUnits(stateForConditionals: StateForConditionals? = null): Sequence<String> {
fun getUpgradeUnits(stateForConditionals: StateForConditionals = StateForConditionals.EmptyState): Sequence<String> {
return sequence {
yieldIfNotNull(upgradesTo)
for (unique in getMatchingUniques(UniqueType.CanUpgrade, stateForConditionals))
@ -94,7 +94,7 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction {
}
}
fun getRulesetUpgradeUnits(stateForConditionals: StateForConditionals? = null): Sequence<BaseUnit> {
fun getRulesetUpgradeUnits(stateForConditionals: StateForConditionals = StateForConditionals.EmptyState): Sequence<BaseUnit> {
return sequence {
for (unit in getUpgradeUnits(stateForConditionals))
yieldIfNotNull(ruleset.units[unit])
@ -115,18 +115,27 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction {
return unit
}
override fun hasUnique(uniqueType: UniqueType, state: StateForConditionals?): Boolean {
return super<RulesetObject>.hasUnique(uniqueType, state) || ::ruleset.isInitialized && type.hasUnique(uniqueType, state)
}
override fun hasUnique(tagUnique: String): Boolean {
return super<RulesetObject>.hasUnique(tagUnique) || ::ruleset.isInitialized && type.hasUnique(tagUnique)
}
/** Allows unique functions (getMatchingUniques, hasUnique) to "see" uniques from the UnitType */
override fun getMatchingUniques(uniqueType: UniqueType, stateForConditionals: StateForConditionals?): Sequence<Unique> {
val ourUniques = super<RulesetObject>.getMatchingUniques(uniqueType, stateForConditionals)
override fun getMatchingUniques(uniqueType: UniqueType, state: StateForConditionals): Sequence<Unique> {
val ourUniques = super<RulesetObject>.getMatchingUniques(uniqueType, state)
if (! ::ruleset.isInitialized) { // Not sure if this will ever actually happen, but better safe than sorry
return ourUniques
}
val typeUniques = type.getMatchingUniques(uniqueType, stateForConditionals)
val typeUniques = type.getMatchingUniques(uniqueType, state)
// Memory optimization - very rarely do we actually get uniques from both sources,
// and sequence addition is expensive relative to the rare case that we'll actually need it
if (ourUniques.none()) return typeUniques
if (typeUniques.none()) return ourUniques
return ourUniques + type.getMatchingUniques(uniqueType, stateForConditionals)
return ourUniques + type.getMatchingUniques(uniqueType, state)
}
override fun getProductionCost(civInfo: Civilization, city: City?): Int = costFunctions.getProductionCost(civInfo, city)
@ -140,15 +149,6 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction {
return super.canBePurchasedWithStat(city, stat)
}
/** Whenever we call .hasUniques() or .getMatchingUniques(), we also want to return the uniques from the unit type
* All of the IHasUniques functions converge to getMatchingUniques, so overriding this one function gives us all of them */
override fun getMatchingUniques(uniqueTemplate: String, stateForConditionals: StateForConditionals?): Sequence<Unique> {
val baseUnitMatchingUniques = super<RulesetObject>.getMatchingUniques(uniqueTemplate, stateForConditionals)
return if (::ruleset.isInitialized) baseUnitMatchingUniques +
type.getMatchingUniques(uniqueTemplate, stateForConditionals)
else baseUnitMatchingUniques // for e.g. Mod Checker, we may check a BaseUnit's uniques without initializing ruleset
}
override fun getBaseBuyCost(city: City, stat: Stat): Float? {
return sequence {
val baseCost = super.getBaseBuyCost(city, stat)
@ -413,7 +413,7 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction {
// "military units" --> "Military", using invariant locale
&& matchesFilter(filter.removeSuffix(" units").lowercase().replaceFirstChar { it.uppercaseChar() })
) return true
return uniqueMap.contains(filter)
return uniqueMap.hasTagUnique(filter)
}
}
}
@ -431,10 +431,10 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction {
val movesLikeAirUnits by lazy { type.getMovementType() == UnitMovementType.Air }
/** Returns resource requirements from both uniques and requiredResource field */
override fun getResourceRequirementsPerTurn(stateForConditionals: StateForConditionals?): Counter<String> {
override fun getResourceRequirementsPerTurn(state: StateForConditionals?): Counter<String> {
val resourceRequirements = Counter<String>()
if (requiredResource != null) resourceRequirements[requiredResource!!] = 1
for (unique in getMatchingUniques(UniqueType.ConsumesResources, stateForConditionals))
for (unique in getMatchingUniques(UniqueType.ConsumesResources, state ?: StateForConditionals.EmptyState))
resourceRequirements[unique.params[1]] += unique.params[0].toInt()
return resourceRequirements
}

View File

@ -6,12 +6,6 @@ import com.unciv.models.ruleset.unique.UniqueTarget
import com.unciv.ui.objectdescriptions.BaseUnitDescriptions.getUnitTypeCivilopediaTextLines
enum class UnitLayer { // The layer in which the unit moves
Civilian,
Military,
Air
}
enum class UnitMovementType { // The types of tiles the unit can by default enter
Land, // Only land tiles except when certain techs are researched
Water, // Only water tiles
@ -42,8 +36,7 @@ class UnitType() : RulesetObject() {
"Land" -> isLandUnit()
"Water" -> isWaterUnit()
"Air" -> isAirUnit()
in uniqueMap -> true
else -> false
else -> uniqueMap.hasTagUnique(filter)
}
}
@ -58,7 +51,7 @@ class UnitType() : RulesetObject() {
val City = UnitType("City", "Land")
fun getCivilopediaIterator(ruleset: Ruleset): Collection<UnitType> {
return UnitMovementType.values().map {
return UnitMovementType.entries.map {
// Create virtual UnitTypes to describe the movement domains - Civilopedia only.
// It is important that the name includes the [] _everywhere_
// (here, CivilopediaImageGetters, links, etc.) so translation comes as cheap as possible.