chore(purity): Replace extra @LocalState with declaring that the classes are well known local state classes

This commit is contained in:
yairm210 2025-08-05 08:12:21 +03:00
parent 152acba973
commit 68894cdac5
26 changed files with 71 additions and 72 deletions

View File

@ -38,7 +38,7 @@ plugins {
// This is *with* gradle 8.2 downloaded according the project specs, no idea what that's about // This is *with* gradle 8.2 downloaded according the project specs, no idea what that's about
kotlin("multiplatform") version "1.9.24" kotlin("multiplatform") version "1.9.24"
kotlin("plugin.serialization") version "1.9.24" kotlin("plugin.serialization") version "1.9.24"
id("io.github.yairm210.purity-plugin") version "0.0.43" apply(false) id("io.github.yairm210.purity-plugin") version "0.0.45" apply(false)
} }
allprojects { allprojects {
@ -49,28 +49,32 @@ allprojects {
apply(plugin = "io.github.yairm210.purity-plugin") apply(plugin = "io.github.yairm210.purity-plugin")
configure<yairm210.purity.PurityConfiguration>{ configure<yairm210.purity.PurityConfiguration>{
wellKnownPureFunctions = setOf( wellKnownPureFunctions = setOf(
"kotlin.assert", "kotlin.lazy", // moved
"kotlin.lazy", "kotlin.getValue", // moved
"kotlin.getValue", "kotlin.error", // moved
"kotlin.error",
) )
wellKnownReadonlyFunctions = setOf( wellKnownReadonlyFunctions = setOf(
"com.badlogic.gdx.math.Vector2.len", "com.badlogic.gdx.math.Vector2.len",
"com.badlogic.gdx.math.Vector2.cpy", "com.badlogic.gdx.math.Vector2.cpy",
"com.badlogic.gdx.math.Vector2.hashCode", "com.badlogic.gdx.math.Vector2.hashCode",
"java.lang.reflect.Field.getAnnotation", // not sure if generic enough to be useful globally
"java.lang.Class.getField",
// Looks like the Collection.contains is not considered overridden :thunk:
"kotlin.Array.get",
"kotlin.collections.mutableSetOf",
"kotlin.collections.withIndex", // applicable to sequence as well
"kotlin.collections.intersect",
"kotlin.collections.maxOfOrNull",
"kotlin.collections.minOfOrNull",
"kotlin.reflect.KMutableProperty0.get", // also 1 and 2
) )
wellKnownPureClasses = setOf( wellKnownPureClasses = setOf<String>(
)
wellKnownInternalStateClasses = setOf<String>(
// Moved all
"kotlin.collections.MutableList",
"kotlin.collections.MutableSet",
"kotlin.collections.MutableMap",
"kotlin.collections.List",
"kotlin.collections.Set",
"kotlin.collections.Map",
"kotlin.collections.ArrayDequeue",
"com.unciv.models.stats.Stats",
"com.unciv.models.Counter",
"com.unciv.models.ruleset.tile.ResourceSupplyList",
"com.badlogic.gdx.math.Vector2",
) )
warnOnPossibleAnnotations = true warnOnPossibleAnnotations = true
} }

View File

@ -42,8 +42,7 @@ object MultiFilter {
fun getAllSingleFilters(input: String): Sequence<String> = when { fun getAllSingleFilters(input: String): Sequence<String> = when {
input.hasSurrounding(andPrefix, andSuffix) && input.contains(andSeparator) -> { input.hasSurrounding(andPrefix, andSuffix) && input.contains(andSeparator) -> {
// Resolve "AND" filters // Resolve "AND" filters
@LocalState @LocalState val filters = input.removeSurrounding(andPrefix, andSuffix)
val filters = input.removeSurrounding(andPrefix, andSuffix)
.splitToSequence(andSeparator) .splitToSequence(andSeparator)
filters.flatMap { getAllSingleFilters(it) } filters.flatMap { getAllSingleFilters(it) }
} }

View File

@ -183,11 +183,13 @@ object Automation {
} }
@Suppress("MemberVisibilityCanBePrivate") @Suppress("MemberVisibilityCanBePrivate")
@Readonly
fun providesUnneededCarryingSlots(baseUnit: BaseUnit, civInfo: Civilization): Boolean { fun providesUnneededCarryingSlots(baseUnit: BaseUnit, civInfo: Civilization): Boolean {
// Simplified, will not work for crazy mods with more than one carrying filter for a unit // Simplified, will not work for crazy mods with more than one carrying filter for a unit
val carryUnique = baseUnit.getMatchingUniques(UniqueType.CarryAirUnits).first() val carryUnique = baseUnit.getMatchingUniques(UniqueType.CarryAirUnits).first()
val carryFilter = carryUnique.params[1] val carryFilter = carryUnique.params[1]
@Readonly
fun getCarryAmount(mapUnit: MapUnit): Int { fun getCarryAmount(mapUnit: MapUnit): Int {
val mapUnitCarryUnique = val mapUnitCarryUnique =
mapUnit.getMatchingUniques(UniqueType.CarryAirUnits).firstOrNull() ?: return 0 mapUnit.getMatchingUniques(UniqueType.CarryAirUnits).firstOrNull() ?: return 0
@ -338,6 +340,7 @@ object Automation {
/** Determines whether the AI should be willing to spend strategic resources to build /** Determines whether the AI should be willing to spend strategic resources to build
* [construction] for [civInfo], assumes that we are actually able to do so. */ * [construction] for [civInfo], assumes that we are actually able to do so. */
@Readonly
fun allowSpendingResource(civInfo: Civilization, construction: INonPerpetualConstruction, cityInfo: City? = null): Boolean { fun allowSpendingResource(civInfo: Civilization, construction: INonPerpetualConstruction, cityInfo: City? = null): Boolean {
// City states do whatever they want // City states do whatever they want
if (civInfo.isCityState) if (civInfo.isCityState)
@ -418,13 +421,14 @@ object Automation {
else -> ThreatLevel.Medium else -> ThreatLevel.Medium
} }
} }
@Readonly
private fun improvementIsRemovable(city: City, tile: Tile): Boolean { private fun improvementIsRemovable(city: City, tile: Tile): Boolean {
val gameContext = GameContext(city.civ, city, tile = tile) val gameContext = GameContext(city.civ, city, tile = tile)
return (tile.getTileImprovement()?.hasUnique(UniqueType.AutomatedUnitsWillNotReplace, gameContext) == false && tile.getTileImprovement()?.hasUnique(UniqueType.Irremovable, gameContext) == false) return (tile.getTileImprovement()?.hasUnique(UniqueType.AutomatedUnitsWillNotReplace, gameContext) == false && tile.getTileImprovement()?.hasUnique(UniqueType.Irremovable, gameContext) == false)
} }
/** Support [UniqueType.CreatesOneImprovement] unique - find best tile for placement automation */ /** Support [UniqueType.CreatesOneImprovement] unique - find best tile for placement automation */
@Readonly
fun getTileForConstructionImprovement(city: City, improvement: TileImprovement): Tile? { fun getTileForConstructionImprovement(city: City, improvement: TileImprovement): Tile? {
val localUniqueCache = LocalUniqueCache() val localUniqueCache = LocalUniqueCache()
val civ = city.civ val civ = city.civ
@ -441,6 +445,7 @@ object Automation {
} }
// Ranks a tile for any purpose except the expansion algorithm of cities // Ranks a tile for any purpose except the expansion algorithm of cities
@Readonly
internal fun rankTile(tile: Tile?, civInfo: Civilization, internal fun rankTile(tile: Tile?, civInfo: Civilization,
localUniqueCache: LocalUniqueCache): Float { localUniqueCache: LocalUniqueCache): Float {
if (tile == null) return 0f if (tile == null) return 0f
@ -460,6 +465,7 @@ object Automation {
} }
// Ranks a tile for the expansion algorithm of cities // Ranks a tile for the expansion algorithm of cities
@Readonly
internal fun rankTileForExpansion(tile: Tile, city: City, internal fun rankTileForExpansion(tile: Tile, city: City,
localUniqueCache: LocalUniqueCache): Int { localUniqueCache: LocalUniqueCache): Int {
// https://github.com/Gedemon/Civ5-DLL/blob/aa29e80751f541ae04858b6d2a2c7dcca454201e/CvGameCoreDLL_Expansion1/CvCity.cpp#L10301 // https://github.com/Gedemon/Civ5-DLL/blob/aa29e80751f541ae04858b6d2a2c7dcca454201e/CvGameCoreDLL_Expansion1/CvCity.cpp#L10301
@ -513,6 +519,7 @@ object Automation {
return score return score
} }
@Readonly
fun rankStatsValue(stats: Stats, civInfo: Civilization): Float { fun rankStatsValue(stats: Stats, civInfo: Civilization): Float {
var rank = 0.0f var rank = 0.0f
rank += stats.food * 1.2f //food get more value to keep city growing rank += stats.food * 1.2f //food get more value to keep city growing

View File

@ -35,7 +35,7 @@ object BattleDamage {
@Readonly @Readonly
private fun getGeneralModifiers(combatant: ICombatant, enemy: ICombatant, combatAction: CombatAction, tileToAttackFrom: Tile): Counter<String> { private fun getGeneralModifiers(combatant: ICombatant, enemy: ICombatant, combatAction: CombatAction, tileToAttackFrom: Tile): Counter<String> {
@LocalState val modifiers = Counter<String>() val modifiers = Counter<String>()
val conditionalState = getStateForConditionals(combatAction, combatant, enemy) val conditionalState = getStateForConditionals(combatAction, combatant, enemy)
val civInfo = combatant.getCivInfo() val civInfo = combatant.getCivInfo()
@ -101,7 +101,7 @@ object BattleDamage {
private fun getUnitUniqueModifiers(combatant: MapUnitCombatant, enemy: ICombatant, conditionalState: GameContext, private fun getUnitUniqueModifiers(combatant: MapUnitCombatant, enemy: ICombatant, conditionalState: GameContext,
tileToAttackFrom: Tile): Counter<String> { tileToAttackFrom: Tile): Counter<String> {
val civInfo = combatant.getCivInfo() val civInfo = combatant.getCivInfo()
@LocalState val modifiers = Counter<String>() val modifiers = Counter<String>()
for (unique in combatant.getMatchingUniques(UniqueType.Strength, conditionalState, true)) { for (unique in combatant.getMatchingUniques(UniqueType.Strength, conditionalState, true)) {
modifiers.add(getModifierStringFromUnique(unique), unique.params[0].toInt()) modifiers.add(getModifierStringFromUnique(unique), unique.params[0].toInt())
@ -179,7 +179,7 @@ object BattleDamage {
@Readonly @Readonly
private fun getTerrainAttackModifiers(attacker: MapUnitCombatant, defender: ICombatant, tileToAttackFrom: Tile): Counter<String> { private fun getTerrainAttackModifiers(attacker: MapUnitCombatant, defender: ICombatant, tileToAttackFrom: Tile): Counter<String> {
@LocalState val modifiers = Counter<String>() val modifiers = Counter<String>()
if (attacker.unit.isEmbarked() && defender.getTile().isLand if (attacker.unit.isEmbarked() && defender.getTile().isLand
&& !attacker.unit.hasUnique(UniqueType.AttackAcrossCoast) && !attacker.unit.hasUnique(UniqueType.AttackAcrossCoast)
) )
@ -219,7 +219,7 @@ object BattleDamage {
fun getAirSweepAttackModifiers( fun getAirSweepAttackModifiers(
attacker: ICombatant attacker: ICombatant
): Counter<String> { ): Counter<String> {
@LocalState val modifiers = Counter<String>() val modifiers = Counter<String>()
if (attacker is MapUnitCombatant) { if (attacker is MapUnitCombatant) {
for (unique in attacker.unit.getMatchingUniques(UniqueType.StrengthWhenAirsweep)) { for (unique in attacker.unit.getMatchingUniques(UniqueType.StrengthWhenAirsweep)) {

View File

@ -39,7 +39,7 @@ object CityResources {
@Readonly @Readonly
private fun getResourcesGeneratedByCityNotIncludingBuildings(city: City, resourceModifers: Map<String, Float>): ResourceSupplyList { private fun getResourcesGeneratedByCityNotIncludingBuildings(city: City, resourceModifers: Map<String, Float>): ResourceSupplyList {
@LocalState val cityResources = ResourceSupplyList() val cityResources = ResourceSupplyList()
cityResources.add(getResourcesFromTiles(city, resourceModifers)) cityResources.add(getResourcesFromTiles(city, resourceModifers))
cityResources.add(getResourceFromUniqueImprovedTiles(city, resourceModifers)) cityResources.add(getResourceFromUniqueImprovedTiles(city, resourceModifers))
@ -79,7 +79,7 @@ object CityResources {
@Readonly @Readonly
private fun getResourcesFromTiles(city: City, resourceModifer: Map<String, Float>): ResourceSupplyList { private fun getResourcesFromTiles(city: City, resourceModifer: Map<String, Float>): ResourceSupplyList {
@LocalState val resourceSupplyList = ResourceSupplyList() val resourceSupplyList = ResourceSupplyList()
for (tileInfo in city.getTiles().filter { it.resource != null }) { for (tileInfo in city.getTiles().filter { it.resource != null }) {
val resource = tileInfo.tileResource val resource = tileInfo.tileResource
val amount = getTileResourceAmount(city, tileInfo) * resourceModifer[resource.name]!! val amount = getTileResourceAmount(city, tileInfo) * resourceModifer[resource.name]!!
@ -90,7 +90,7 @@ object CityResources {
@Readonly @Readonly
private fun getResourceFromUniqueImprovedTiles(city: City, resourceModifer: Map<String, Float>): ResourceSupplyList { private fun getResourceFromUniqueImprovedTiles(city: City, resourceModifer: Map<String, Float>): ResourceSupplyList {
@LocalState val resourceSupplyList = ResourceSupplyList() val resourceSupplyList = ResourceSupplyList()
for (tileInfo in city.getTiles().filter { it.getUnpillagedImprovement() != null }) { for (tileInfo in city.getTiles().filter { it.getUnpillagedImprovement() != null }) {
val gameContext = GameContext(city.civ, city, tile = tileInfo) val gameContext = GameContext(city.civ, city, tile = tileInfo)
val tileImprovement = tileInfo.getUnpillagedTileImprovement() val tileImprovement = tileInfo.getUnpillagedTileImprovement()
@ -114,7 +114,7 @@ object CityResources {
@Readonly @Readonly
private fun getNegativeCityResourcesRequiredByBuildings(city: City): ResourceSupplyList { private fun getNegativeCityResourcesRequiredByBuildings(city: City): ResourceSupplyList {
@LocalState val resourceSupplyList = ResourceSupplyList() val resourceSupplyList = ResourceSupplyList()
val freeBuildings = city.civ.civConstructions.getFreeBuildingNames(city) val freeBuildings = city.civ.civConstructions.getFreeBuildingNames(city)
for (building in city.cityConstructions.getBuiltBuildings()) { for (building in city.cityConstructions.getBuiltBuildings()) {
// Free buildings cost no resources // Free buildings cost no resources
@ -126,7 +126,7 @@ object CityResources {
@Readonly @Readonly
private fun getCityResourcesFromCiv(city: City, resourceModifers: HashMap<String, Float>): ResourceSupplyList { private fun getCityResourcesFromCiv(city: City, resourceModifers: HashMap<String, Float>): ResourceSupplyList {
@LocalState val resourceSupplyList = ResourceSupplyList() val resourceSupplyList = ResourceSupplyList()
// This includes the uniques from buildings, from this and all other cities // This includes the uniques from buildings, from this and all other cities
for (unique in city.getMatchingUniques(UniqueType.ProvidesResources, city.state)) { // E.G "Provides [1] [Iron]" for (unique in city.getMatchingUniques(UniqueType.ProvidesResources, city.state)) { // E.G "Provides [1] [Iron]"
val resource = city.getRuleset().tileResources[unique.params[1]] val resource = city.getRuleset().tileResources[unique.params[1]]

View File

@ -5,7 +5,6 @@ import com.unciv.models.Counter
import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.unique.Unique import com.unciv.models.ruleset.unique.Unique
import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unique.UniqueType
import yairm210.purity.annotations.LocalState
import yairm210.purity.annotations.Readonly import yairm210.purity.annotations.Readonly
/** Manages calculating Great Person Points per City for nextTurn. See public constructor(city) below for details. */ /** Manages calculating Great Person Points per City for nextTurn. See public constructor(city) below for details. */
@ -134,13 +133,11 @@ class GreatPersonPointsBreakdown private constructor(private val ruleset: Rulese
@Readonly @Readonly
fun sum(): Counter<String> { fun sum(): Counter<String> {
// Accumulate base points as fake "fixed-point" // Accumulate base points as fake "fixed-point"
@LocalState
val result = Counter<String>() val result = Counter<String>()
for (entry in basePoints) for (entry in basePoints)
result.add(entry.counter * fixedPointFactor) result.add(entry.counter * fixedPointFactor)
// Accumulate percentage bonuses additively not multiplicatively // Accumulate percentage bonuses additively not multiplicatively
@LocalState
val bonuses = Counter<String>() val bonuses = Counter<String>()
for (entry in percentBonuses) { for (entry in percentBonuses) {
bonuses.add(entry.counter) bonuses.add(entry.counter)

View File

@ -10,7 +10,6 @@ import com.unciv.models.Religion
import com.unciv.models.ruleset.unique.Unique import com.unciv.models.ruleset.unique.Unique
import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.ui.components.extensions.toPercent import com.unciv.ui.components.extensions.toPercent
import yairm210.purity.annotations.LocalState
import yairm210.purity.annotations.Readonly import yairm210.purity.annotations.Readonly
class CityReligionManager : IsPartOfGameInfoSerialization { class CityReligionManager : IsPartOfGameInfoSerialization {
@ -282,7 +281,7 @@ class CityReligionManager : IsPartOfGameInfoSerialization {
/** Doesn't update the pressures, only returns what they are if the update were to happen right now */ /** Doesn't update the pressures, only returns what they are if the update were to happen right now */
@Readonly @Readonly
fun getPressuresFromSurroundingCities(): Counter<String> { fun getPressuresFromSurroundingCities(): Counter<String> {
@LocalState val addedPressure = Counter<String>() val addedPressure = Counter<String>()
if (city.isHolyCity()) { if (city.isHolyCity()) {
addedPressure[religionThisIsTheHolyCityOf!!] = 5 * pressureFromAdjacentCities addedPressure[religionThisIsTheHolyCityOf!!] = 5 * pressureFromAdjacentCities
} }

View File

@ -56,7 +56,6 @@ import com.unciv.ui.components.extensions.toPercent
import com.unciv.ui.screens.victoryscreen.RankingType import com.unciv.ui.screens.victoryscreen.RankingType
import org.jetbrains.annotations.VisibleForTesting import org.jetbrains.annotations.VisibleForTesting
import yairm210.purity.annotations.Cache import yairm210.purity.annotations.Cache
import yairm210.purity.annotations.LocalState
import yairm210.purity.annotations.Readonly import yairm210.purity.annotations.Readonly
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@ -750,7 +749,6 @@ class Civilization : IsPartOfGameInfoSerialization {
@Readonly @Readonly
fun calculateScoreBreakdown(): HashMap<String,Double> { fun calculateScoreBreakdown(): HashMap<String,Double> {
@LocalState
val scoreBreakdown = hashMapOf<String,Double>() val scoreBreakdown = hashMapOf<String,Double>()
// 1276 is the number of tiles in a medium sized map. The original uses 4160 for this, // 1276 is the number of tiles in a medium sized map. The original uses 4160 for this,
// but they have bigger maps // but they have bigger maps

View File

@ -27,7 +27,6 @@ import com.unciv.models.ruleset.unit.BaseUnit
import com.unciv.models.stats.Stat import com.unciv.models.stats.Stat
import com.unciv.ui.screens.victoryscreen.RankingType import com.unciv.ui.screens.victoryscreen.RankingType
import com.unciv.utils.randomWeighted import com.unciv.utils.randomWeighted
import yairm210.purity.annotations.LocalState
import yairm210.purity.annotations.Readonly import yairm210.purity.annotations.Readonly
import kotlin.math.min import kotlin.math.min
import kotlin.math.pow import kotlin.math.pow
@ -418,7 +417,6 @@ class CityStateFunctions(val civInfo: Civilization) {
@Readonly @Readonly
fun getTributeModifiers(demandingCiv: Civilization, demandingWorker: Boolean = false, requireWholeList: Boolean = false): HashMap<String, Int> { fun getTributeModifiers(demandingCiv: Civilization, demandingWorker: Boolean = false, requireWholeList: Boolean = false): HashMap<String, Int> {
@LocalState
val modifiers = LinkedHashMap<String, Int>() // Linked to preserve order when presenting the modifiers table val modifiers = LinkedHashMap<String, Int>() // Linked to preserve order when presenting the modifiers table
// Can't bully major civs or unsettled CS's // Can't bully major civs or unsettled CS's
if (!civInfo.isCityState) { if (!civInfo.isCityState) {

View File

@ -128,11 +128,9 @@ object HexMath {
@Readonly @Readonly
fun hex2WorldCoords(hexCoord: Vector2): Vector2 { fun hex2WorldCoords(hexCoord: Vector2): Vector2 {
// Distance between cells = 2* normal of triangle = 2* (sqrt(3)/2) = sqrt(3) // Distance between cells = 2* normal of triangle = 2* (sqrt(3)/2) = sqrt(3)
@LocalState
val xVector = getVectorByClockHour(10) val xVector = getVectorByClockHour(10)
xVector.scl(sqrt(3.0).toFloat() * hexCoord.x) xVector.scl(sqrt(3.0).toFloat() * hexCoord.x)
@LocalState
val yVector = getVectorByClockHour(2) val yVector = getVectorByClockHour(2)
yVector.scl(sqrt(3.0).toFloat() * hexCoord.y) yVector.scl(sqrt(3.0).toFloat() * hexCoord.y)
@ -143,10 +141,8 @@ object HexMath {
@Readonly @Readonly
fun world2HexCoords(worldCoord: Vector2): Vector2 { fun world2HexCoords(worldCoord: Vector2): Vector2 {
// D: diagonal, A: antidiagonal versors // D: diagonal, A: antidiagonal versors
@LocalState
val D = getVectorByClockHour(10) val D = getVectorByClockHour(10)
D.scl(sqrt(3.0).toFloat()) D.scl(sqrt(3.0).toFloat())
@LocalState
val A = getVectorByClockHour(2) val A = getVectorByClockHour(2)
A.scl(sqrt(3.0).toFloat()) A.scl(sqrt(3.0).toFloat())
val den = D.x * A.y - D.y * A.x val den = D.x * A.y - D.y * A.x
@ -219,7 +215,6 @@ object HexMath {
@Readonly @Readonly
fun getVectorsAtDistance(origin: Vector2, distance: Int, maxDistance: Int, worldWrap: Boolean): List<Vector2> { fun getVectorsAtDistance(origin: Vector2, distance: Int, maxDistance: Int, worldWrap: Boolean): List<Vector2> {
@LocalState
val vectors = mutableListOf<Vector2>() val vectors = mutableListOf<Vector2>()
if (distance == 0) { if (distance == 0) {
return listOf(origin.cpy()) return listOf(origin.cpy())
@ -259,7 +254,6 @@ object HexMath {
@Readonly @Readonly
fun getVectorsInDistance(origin: Vector2, distance: Int, worldWrap: Boolean): List<Vector2> { fun getVectorsInDistance(origin: Vector2, distance: Int, worldWrap: Boolean): List<Vector2> {
@LocalState
val hexesToReturn = mutableListOf<Vector2>() val hexesToReturn = mutableListOf<Vector2>()
for (i in 0..distance) { for (i in 0..distance) {
hexesToReturn += getVectorsAtDistance(origin, i, distance, worldWrap) hexesToReturn += getVectorsAtDistance(origin, i, distance, worldWrap)

View File

@ -405,7 +405,6 @@ class TileMap(initialCapacity: Int = 10) : IsPartOfGameInfoSerialization {
@Readonly @Readonly
fun getViewableTiles(position: Vector2, sightDistance: Int, forAttack: Boolean = false): List<Tile> { fun getViewableTiles(position: Vector2, sightDistance: Int, forAttack: Boolean = false): List<Tile> {
val aUnitHeight = get(position).unitHeight val aUnitHeight = get(position).unitHeight
@LocalState
val viewableTiles = mutableListOf(ViewableTile( val viewableTiles = mutableListOf(ViewableTile(
get(position), get(position),
aUnitHeight, aUnitHeight,
@ -416,7 +415,6 @@ class TileMap(initialCapacity: Int = 10) : IsPartOfGameInfoSerialization {
for (i in 1..sightDistance+1) { // in each layer, for (i in 1..sightDistance+1) { // in each layer,
// This is so we don't use tiles in the same distance to "see over", // This is so we don't use tiles in the same distance to "see over",
// that is to say, the "viewableTiles.contains(it) check will return false for neighbors from the same distance // that is to say, the "viewableTiles.contains(it) check will return false for neighbors from the same distance
@LocalState
val tilesToAddInDistanceI = ArrayList<ViewableTile>() val tilesToAddInDistanceI = ArrayList<ViewableTile>()
for (cTile in getTilesAtDistance(position, i)) { // for each tile in that layer, for (cTile in getTilesAtDistance(position, i)) { // for each tile in that layer,

View File

@ -329,7 +329,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
* StateForConditionals is assumed to regarding this mapUnit*/ * StateForConditionals is assumed to regarding this mapUnit*/
@Readonly @Readonly
fun getResourceRequirementsPerTurn(): Counter<String> { fun getResourceRequirementsPerTurn(): Counter<String> {
@LocalState val resourceRequirements = Counter<String>() val resourceRequirements = Counter<String>()
if (baseUnit.requiredResource != null) resourceRequirements[baseUnit.requiredResource!!] = 1 if (baseUnit.requiredResource != null) resourceRequirements[baseUnit.requiredResource!!] = 1
for (unique in getMatchingUniques(UniqueType.ConsumesResources, cache.state)) for (unique in getMatchingUniques(UniqueType.ConsumesResources, cache.state))
resourceRequirements.add(unique.params[1], unique.params[0].toInt()) resourceRequirements.add(unique.params[1], unique.params[0].toInt())

View File

@ -738,7 +738,6 @@ class Tile : IsPartOfGameInfoSerialization, Json.Serializable {
@Readonly @Readonly
fun getRulesetIncompatibility(ruleset: Ruleset): HashSet<String> { fun getRulesetIncompatibility(ruleset: Ruleset): HashSet<String> {
@LocalState
val out = HashSet<String>() val out = HashSet<String>()
if (!ruleset.terrains.containsKey(baseTerrain)) if (!ruleset.terrains.containsKey(baseTerrain))
out.add("Base terrain [$baseTerrain] does not exist in ruleset!") out.add("Base terrain [$baseTerrain] does not exist in ruleset!")

View File

@ -11,8 +11,11 @@ import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.stats.Stat import com.unciv.models.stats.Stat
import com.unciv.models.stats.Stats import com.unciv.models.stats.Stats
import com.unciv.ui.components.extensions.toPercent import com.unciv.ui.components.extensions.toPercent
import yairm210.purity.annotations.LocalState
import yairm210.purity.annotations.Readonly
import java.util.EnumMap import java.util.EnumMap
@Readonly
fun List<Pair<String, Stats>>.toStats(): Stats { fun List<Pair<String, Stats>>.toStats(): Stats {
val stats = Stats() val stats = Stats()
for ((_, statsToAdd) in this) for ((_, statsToAdd) in this)
@ -28,6 +31,7 @@ class TileStatFunctions(val tile: Tile) {
localUniqueCache: LocalUniqueCache = LocalUniqueCache(false) localUniqueCache: LocalUniqueCache = LocalUniqueCache(false)
): Stats = getTileStats(tile.getCity(), observingCiv, localUniqueCache) ): Stats = getTileStats(tile.getCity(), observingCiv, localUniqueCache)
@Readonly @Suppress("purity") // requires "for @LocalState X"
fun getTileStats( fun getTileStats(
city: City?, observingCiv: Civilization?, city: City?, observingCiv: Civilization?,
localUniqueCache: LocalUniqueCache = LocalUniqueCache(false) localUniqueCache: LocalUniqueCache = LocalUniqueCache(false)
@ -51,21 +55,22 @@ class TileStatFunctions(val tile: Tile) {
return statsBreakdown.toStats() return statsBreakdown.toStats()
} }
@Readonly
fun getTileStatsBreakdown(city: City?, observingCiv: Civilization?, fun getTileStatsBreakdown(city: City?, observingCiv: Civilization?,
localUniqueCache: LocalUniqueCache = LocalUniqueCache(false) localUniqueCache: LocalUniqueCache = LocalUniqueCache(false)
): List<Pair<String, Stats>> { ): List<Pair<String, Stats>> {
val gameContext = GameContext(civInfo = observingCiv, city = city, tile = tile) val gameContext = GameContext(civInfo = observingCiv, city = city, tile = tile)
val listOfStats = getTerrainStatsBreakdown(gameContext) @LocalState val listOfStats = getTerrainStatsBreakdown(gameContext)
val otherYieldsIgnored = tile.allTerrains.any { it.hasUnique(UniqueType.NullifyYields, gameContext) } val otherYieldsIgnored = tile.allTerrains.any { it.hasUnique(UniqueType.NullifyYields, gameContext) }
val improvement = if (otherYieldsIgnored) null // Treat it as if there is no improvement val improvement = if (otherYieldsIgnored) null // Treat it as if there is no improvement
else tile.getUnpillagedTileImprovement() else tile.getUnpillagedTileImprovement()
val improvementStats = improvement?.cloneStats() ?: Stats.ZERO // If improvement==null, will never be added to @LocalState val improvementStats = improvement?.cloneStats() ?: Stats.ZERO // If improvement==null, will never be added to
val road = if (otherYieldsIgnored) null val road = if (otherYieldsIgnored) null
else tile.getUnpillagedRoadImprovement() else tile.getUnpillagedRoadImprovement()
val roadStats = road?.cloneStats() ?: Stats.ZERO @LocalState val roadStats = road?.cloneStats() ?: Stats.ZERO
if (city != null) { if (city != null) {
val statsFromTilesUniques = val statsFromTilesUniques =
@ -135,6 +140,7 @@ class TileStatFunctions(val tile: Tile) {
} }
/** Ensures each stat is >= [minimumStats].stat - modifies in place */ /** Ensures each stat is >= [minimumStats].stat - modifies in place */
@Readonly
private fun missingFromMinimum(current: Stats, minimumStats: Stats): Stats { private fun missingFromMinimum(current: Stats, minimumStats: Stats): Stats {
// Note: Not `for ((stat, value) in other)` - that would skip zero values // Note: Not `for ((stat, value) in other)` - that would skip zero values
val missingStats = Stats() val missingStats = Stats()
@ -148,6 +154,7 @@ class TileStatFunctions(val tile: Tile) {
/** Gets stats of a single Terrain, unifying the Stats class a Terrain inherits and the Stats Unique /** Gets stats of a single Terrain, unifying the Stats class a Terrain inherits and the Stats Unique
* @return A Stats reference, must not be mutated * @return A Stats reference, must not be mutated
*/ */
@Readonly
private fun getSingleTerrainStats(terrain: Terrain, gameContext: GameContext): ArrayList<Pair<String, Stats>> { private fun getSingleTerrainStats(terrain: Terrain, gameContext: GameContext): ArrayList<Pair<String, Stats>> {
val list = arrayListOf(terrain.name to (terrain as Stats)) val list = arrayListOf(terrain.name to (terrain as Stats))
@ -158,8 +165,10 @@ class TileStatFunctions(val tile: Tile) {
} }
/** Gets basic stats to start off [getTileStats] or [getTileStartYield], independently mutable result */ /** Gets basic stats to start off [getTileStats] or [getTileStartYield], independently mutable result */
@Readonly
fun getTerrainStatsBreakdown(gameContext: GameContext = GameContext()): ArrayList<Pair<String, Stats>> { fun getTerrainStatsBreakdown(gameContext: GameContext = GameContext()): ArrayList<Pair<String, Stats>> {
var list = ArrayList<Pair<String, Stats>>() // needs to be marked, because it's a var
@LocalState var list = ArrayList<Pair<String, Stats>>()
// allTerrains iterates over base, natural wonder, then features // allTerrains iterates over base, natural wonder, then features
for (terrain in tile.allTerrains) { for (terrain in tile.allTerrains) {
@ -183,7 +192,8 @@ class TileStatFunctions(val tile: Tile) {
} }
// Only gets the tile percentage bonus, not the improvement percentage bonus // Only gets the tile percentage bonus, not the improvement percentage bonus
@Suppress("MemberVisibilityCanBePrivate") @Suppress("MemberVisibilityCanBePrivate", "purity")
@Readonly
fun getTilePercentageStats(observingCiv: Civilization?, city: City?, uniqueCache: LocalUniqueCache): EnumMap<TilePercentageCategory, Stats> { fun getTilePercentageStats(observingCiv: Civilization?, city: City?, uniqueCache: LocalUniqueCache): EnumMap<TilePercentageCategory, Stats> {
val terrainStats = Stats() val terrainStats = Stats()
val gameContext = GameContext(civInfo = observingCiv, city = city, tile = tile) val gameContext = GameContext(civInfo = observingCiv, city = city, tile = tile)
@ -274,6 +284,7 @@ class TileStatFunctions(val tile: Tile) {
/** Returns the extra stats that we would get if we switched to this improvement /** Returns the extra stats that we would get if we switched to this improvement
* Can be negative if we're switching to a worse improvement */ * Can be negative if we're switching to a worse improvement */
@Readonly
fun getStatDiffForImprovement( fun getStatDiffForImprovement(
improvement: TileImprovement, improvement: TileImprovement,
observingCiv: Civilization, observingCiv: Civilization,
@ -285,16 +296,17 @@ class TileStatFunctions(val tile: Tile) {
val currentStats = currentTileStats val currentStats = currentTileStats
?: getTileStats(city, observingCiv, cityUniqueCache) ?: getTileStats(city, observingCiv, cityUniqueCache)
val tileClone = tile.clone(addUnits = false) @LocalState val tileClone = tile.clone(addUnits = false)
tileClone.setTerrainTransients() tileClone.setTerrainTransients()
tileClone.setImprovement(improvement.name) tileClone.setImprovement(improvement.name)
val futureStats = tileClone.stats.getTileStats(city, observingCiv, cityUniqueCache) @LocalState val futureStats = tileClone.stats.getTileStats(city, observingCiv, cityUniqueCache)
return futureStats.minus(currentStats) return futureStats.minus(currentStats)
} }
// Also multiplies the stats by the percentage bonus for improvements (but not for tiles) // Also multiplies the stats by the percentage bonus for improvements (but not for tiles)
@Readonly
private fun getExtraImprovementStats( private fun getExtraImprovementStats(
improvement: TileImprovement, improvement: TileImprovement,
observingCiv: Civilization, observingCiv: Civilization,

View File

@ -52,7 +52,6 @@ open class Counter<K>(
@Readonly @Readonly
/** Creates a new instance (does not modify) */ /** Creates a new instance (does not modify) */
operator fun times(amount: Int): Counter<K> { operator fun times(amount: Int): Counter<K> {
@LocalState
val newCounter = Counter<K>() val newCounter = Counter<K>()
for (key in keys) newCounter[key] = this[key] * amount for (key in keys) newCounter[key] = this[key] * amount
return newCounter return newCounter

View File

@ -556,7 +556,7 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction {
state ?: GameContext.EmptyState) state ?: GameContext.EmptyState)
if (uniques.none() && requiredResource == null) return Counter.ZERO if (uniques.none() && requiredResource == null) return Counter.ZERO
@LocalState val resourceRequirements = Counter<String>() val resourceRequirements = Counter<String>()
if (requiredResource != null) resourceRequirements[requiredResource!!] = 1 if (requiredResource != null) resourceRequirements[requiredResource!!] = 1
for (unique in uniques) for (unique in uniques)
resourceRequirements.add(unique.params[1], unique.params[0].toInt()) resourceRequirements.add(unique.params[1], unique.params[0].toInt())

View File

@ -13,7 +13,6 @@ import com.unciv.models.stats.Stat
import com.unciv.models.stats.Stat.Companion.statsUsableToBuy import com.unciv.models.stats.Stat.Companion.statsUsableToBuy
import com.unciv.ui.components.extensions.toPercent import com.unciv.ui.components.extensions.toPercent
import com.unciv.ui.components.fonts.Fonts import com.unciv.ui.components.fonts.Fonts
import yairm210.purity.annotations.LocalState
import yairm210.purity.annotations.Pure import yairm210.purity.annotations.Pure
import yairm210.purity.annotations.Readonly import yairm210.purity.annotations.Readonly
import kotlin.math.pow import kotlin.math.pow
@ -133,7 +132,7 @@ interface INonPerpetualConstruction : IConstruction, INamed, IHasUniques {
@Readonly @Readonly
override fun getStockpiledResourceRequirements(state: GameContext): Counter<String> { override fun getStockpiledResourceRequirements(state: GameContext): Counter<String> {
@LocalState val counter = Counter<String>() val counter = Counter<String>()
for (unique in getMatchingUniquesNotConflicting(UniqueType.CostsResources, state)){ for (unique in getMatchingUniquesNotConflicting(UniqueType.CostsResources, state)){
var amount = unique.params[0].toInt() var amount = unique.params[0].toInt()
if (unique.isModifiedByGameSpeed()) amount = (amount * state.gameInfo!!.speed.modifier).toInt() if (unique.isModifiedByGameSpeed()) amount = (amount * state.gameInfo!!.speed.modifier).toInt()

View File

@ -14,7 +14,6 @@ import com.unciv.models.stats.Stats
import com.unciv.ui.objectdescriptions.uniquesToCivilopediaTextLines import com.unciv.ui.objectdescriptions.uniquesToCivilopediaTextLines
import com.unciv.ui.screens.civilopediascreen.FormattedLine import com.unciv.ui.screens.civilopediascreen.FormattedLine
import yairm210.purity.annotations.Cache import yairm210.purity.annotations.Cache
import yairm210.purity.annotations.LocalState
import yairm210.purity.annotations.Readonly import yairm210.purity.annotations.Readonly
class TileResource : RulesetStatsObject(), GameResource { class TileResource : RulesetStatsObject(), GameResource {
@ -61,7 +60,7 @@ class TileResource : RulesetStatsObject(), GameResource {
val ruleset = this.ruleset val ruleset = this.ruleset
?: throw IllegalStateException("No ruleset on TileResource when initializing improvements") ?: throw IllegalStateException("No ruleset on TileResource when initializing improvements")
@LocalState val allImprovementsLocal = mutableSetOf<String>() val allImprovementsLocal = mutableSetOf<String>()
if (improvement != null) allImprovementsLocal += improvement!! if (improvement != null) allImprovementsLocal += improvement!!
allImprovementsLocal.addAll(improvedBy) allImprovementsLocal.addAll(improvedBy)

View File

@ -11,7 +11,6 @@ import com.unciv.models.translations.getModifiers
import com.unciv.models.translations.getPlaceholderParameters import com.unciv.models.translations.getPlaceholderParameters
import com.unciv.models.translations.getPlaceholderText import com.unciv.models.translations.getPlaceholderText
import com.unciv.models.translations.removeConditionals import com.unciv.models.translations.removeConditionals
import yairm210.purity.annotations.LocalState
import yairm210.purity.annotations.Readonly import yairm210.purity.annotations.Readonly
import kotlin.math.max import kotlin.math.max
@ -181,12 +180,12 @@ class Unique(val text: String, val sourceObjectType: UniqueTarget? = null, val s
// note this is only done for the replacement, not the deprecated unique, thus parameters of // note this is only done for the replacement, not the deprecated unique, thus parameters of
// conditionals on the deprecated unique are ignored // conditionals on the deprecated unique are ignored
@LocalState val finalPossibleUniques = ArrayList<String>() val finalPossibleUniques = ArrayList<String>()
for (possibleUnique in possibleUniques) { for (possibleUnique in possibleUniques) {
var resultingUnique = possibleUnique var resultingUnique = possibleUnique
@LocalState val timesParameterWasSeen = Counter<String>() val timesParameterWasSeen = Counter<String>()
for (parameter in possibleUnique.replace('<', ' ').getPlaceholderParameters()) { for (parameter in possibleUnique.replace('<', ' ').getPlaceholderParameters()) {
val parameterHasSign = parameter.startsWith('-') || parameter.startsWith('+') val parameterHasSign = parameter.startsWith('-') || parameter.startsWith('+')
val parameterUnsigned = if (parameterHasSign) parameter.drop(1) else parameter val parameterUnsigned = if (parameterHasSign) parameter.drop(1) else parameter

View File

@ -13,7 +13,6 @@ import com.unciv.models.ruleset.validation.Suppression
import com.unciv.models.stats.Stat import com.unciv.models.stats.Stat
import com.unciv.models.stats.SubStat import com.unciv.models.stats.SubStat
import com.unciv.models.translations.TranslationFileWriter import com.unciv.models.translations.TranslationFileWriter
import yairm210.purity.annotations.LocalState
import yairm210.purity.annotations.Pure import yairm210.purity.annotations.Pure
import yairm210.purity.annotations.Readonly import yairm210.purity.annotations.Readonly
@ -714,7 +713,7 @@ enum class UniqueParameterType(
} }
@Readonly @Readonly
private fun scanExistingValues(type: UniqueParameterType, ruleset: Ruleset): Set<String> { private fun scanExistingValues(type: UniqueParameterType, ruleset: Ruleset): Set<String> {
@LocalState val result = mutableSetOf<String>() val result = mutableSetOf<String>()
for (unique in ruleset.allUniques()) { for (unique in ruleset.allUniques()) {
val parameterMap = unique.type?.parameterTypeMap ?: continue val parameterMap = unique.type?.parameterTypeMap ?: continue
for ((index, param) in unique.params.withIndex()) { for ((index, param) in unique.params.withIndex()) {

View File

@ -1489,7 +1489,7 @@ enum class UniqueType(
* For 95% of cases, auto-matching is fine. */ * For 95% of cases, auto-matching is fine. */
@Readonly @Readonly
open fun parameterTypeMapInitializer(): ArrayList<List<UniqueParameterType>> { open fun parameterTypeMapInitializer(): ArrayList<List<UniqueParameterType>> {
@LocalState val map = ArrayList<List<UniqueParameterType>>() val map = ArrayList<List<UniqueParameterType>>()
for (placeholder in text.getPlaceholderParameters()) { for (placeholder in text.getPlaceholderParameters()) {
val matchingParameterTypes = placeholder val matchingParameterTypes = placeholder
.split('/') .split('/')

View File

@ -26,7 +26,6 @@ import com.unciv.ui.objectdescriptions.BaseUnitDescriptions
import com.unciv.ui.screens.civilopediascreen.FormattedLine import com.unciv.ui.screens.civilopediascreen.FormattedLine
import com.unciv.utils.yieldIfNotNull import com.unciv.utils.yieldIfNotNull
import yairm210.purity.annotations.Cache import yairm210.purity.annotations.Cache
import yairm210.purity.annotations.LocalState
import yairm210.purity.annotations.Readonly import yairm210.purity.annotations.Readonly
import kotlin.math.pow import kotlin.math.pow
@ -478,7 +477,7 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction {
/** Returns resource requirements from both uniques and requiredResource field */ /** Returns resource requirements from both uniques and requiredResource field */
override fun getResourceRequirementsPerTurn(state: GameContext?): Counter<String> { override fun getResourceRequirementsPerTurn(state: GameContext?): Counter<String> {
@LocalState val resourceRequirements = Counter<String>() val resourceRequirements = Counter<String>()
if (requiredResource != null) resourceRequirements[requiredResource!!] = 1 if (requiredResource != null) resourceRequirements[requiredResource!!] = 1
for (unique in getMatchingUniques(UniqueType.ConsumesResources, state ?: GameContext.EmptyState)) for (unique in getMatchingUniques(UniqueType.ConsumesResources, state ?: GameContext.EmptyState))
resourceRequirements.add(unique.params[1], unique.params[0].toInt()) resourceRequirements.add(unique.params[1], unique.params[0].toInt())

View File

@ -349,7 +349,7 @@ class UniqueValidator(val ruleset: Ruleset) {
unique: Unique, unique: Unique,
): List<UniqueComplianceError> { ): List<UniqueComplianceError> {
if (unique.type == null) return emptyList() if (unique.type == null) return emptyList()
@LocalState val errorList = ArrayList<UniqueComplianceError>() val errorList = ArrayList<UniqueComplianceError>()
for ((index, param) in unique.params.withIndex()) { for ((index, param) in unique.params.withIndex()) {
// Trying to catch the error at #11404 // Trying to catch the error at #11404
if (unique.type.parameterTypeMap.size != unique.params.size) { if (unique.type.parameterTypeMap.size != unique.params.size) {

View File

@ -125,6 +125,7 @@ open class Stats(
/** **Non-Mutating function** /** **Non-Mutating function**
* @return a new [Stats] instance with the result of multiplying each value of this instance by [number] as a new instance */ * @return a new [Stats] instance with the result of multiplying each value of this instance by [number] as a new instance */
@Readonly
operator fun times(number: Float) = Stats( operator fun times(number: Float) = Stats(
production * number, production * number,
food * number, food * number,

View File

@ -483,7 +483,7 @@ fun String.getPlaceholderParameters(): List<String> {
val stringToParse = this.removeConditionals() val stringToParse = this.removeConditionals()
@LocalState val parameters = ArrayList<String>() val parameters = ArrayList<String>()
var depthOfBraces = 0 var depthOfBraces = 0
var startOfCurrentParameter = -1 var startOfCurrentParameter = -1
stringToParse.indices.forEach { i -> stringToParse.indices.forEach { i ->

View File

@ -9,7 +9,6 @@ import com.unciv.models.translations.removeConditionals
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
import com.unciv.ui.components.fonts.FontRulesetIcons import com.unciv.ui.components.fonts.FontRulesetIcons
import com.unciv.ui.components.fonts.Fonts import com.unciv.ui.components.fonts.Fonts
import yairm210.purity.annotations.LocalState
import yairm210.purity.annotations.Readonly import yairm210.purity.annotations.Readonly
import kotlin.math.ceil import kotlin.math.ceil
@ -180,7 +179,7 @@ object UnitActionModifiers {
if (maxUsages!=null) effects += "${usagesLeft(unit, actionUnique)}/$maxUsages" if (maxUsages!=null) effects += "${usagesLeft(unit, actionUnique)}/$maxUsages"
if (actionUnique.hasModifier(UniqueType.UnitActionStatsCost)) { if (actionUnique.hasModifier(UniqueType.UnitActionStatsCost)) {
@LocalState val statCost = Stats() val statCost = Stats()
for (conditional in actionUnique.getModifiers(UniqueType.UnitActionStatsCost)) for (conditional in actionUnique.getModifiers(UniqueType.UnitActionStatsCost))
statCost.add(conditional.stats) statCost.add(conditional.stats)
effects += statCost.toStringOnlyIcons(false) effects += statCost.toStringOnlyIcons(false)