chore: Readonly 4

This commit is contained in:
yairm210 2025-07-14 13:16:21 +03:00
parent 7e1cc64ca7
commit 3ba87902a5
11 changed files with 52 additions and 1 deletions

View File

@ -54,6 +54,9 @@ allprojects {
"kotlin.apply", "kotlin.apply",
"kotlin.takeIf", "kotlin.takeIf",
"kotlin.takeUnless", "kotlin.takeUnless",
"kotlin.ranges.coerceIn",
"com.unciv.logic.civilization.diplomacy.RelationshipLevel.compareTo",
// "kotlin.Enum.compareTo", // so overrides are considered pure as well :think:
) )
wellKnownReadonlyFunctions = setOf( wellKnownReadonlyFunctions = setOf(
"kotlin.collections.any", "kotlin.collections.any",
@ -62,6 +65,11 @@ allprojects {
"kotlin.ranges.coerceAtLeast", "kotlin.ranges.coerceAtLeast",
// Looks like the Collection.contains is not considered overridden :thunk: // Looks like the Collection.contains is not considered overridden :thunk:
"java.util.AbstractCollection.contains", "java.util.AbstractCollection.contains",
"kotlin.collections.sum",
)
wellKnownPureClasses = setOf(
"kotlin.enums.EnumEntries",
"kotlin.Enum",
) )
} }

View File

@ -47,6 +47,7 @@ import com.unciv.ui.screens.savescreens.Gzip
import com.unciv.ui.screens.worldscreen.status.NextTurnProgress import com.unciv.ui.screens.worldscreen.status.NextTurnProgress
import com.unciv.utils.DebugUtils import com.unciv.utils.DebugUtils
import com.unciv.utils.debug import com.unciv.utils.debug
import yairm210.purity.annotations.Readonly
import java.security.MessageDigest import java.security.MessageDigest
import java.util.UUID import java.util.UUID
@ -231,6 +232,7 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion
val civMap by lazy { civilizations.associateBy { it.civName } } val civMap by lazy { civilizations.associateBy { it.civName } }
/** Get a civ by name /** Get a civ by name
* @throws NoSuchElementException if no civ of that name is in the game (alive or dead)! */ * @throws NoSuchElementException if no civ of that name is in the game (alive or dead)! */
@Readonly
fun getCivilization(civName: String) = civMap[civName] fun getCivilization(civName: String) = civMap[civName]
?: civilizations.first { it.civName == civName } // This is for spectators who are added in later, artificially ?: civilizations.first { it.civName == civName } // This is for spectators who are added in later, artificially
fun getCurrentPlayerCivilization() = currentPlayerCiv fun getCurrentPlayerCivilization() = currentPlayerCiv
@ -241,6 +243,7 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion
fun getDifficulty() = difficultyObject fun getDifficulty() = difficultyObject
/** Access a cached `GlobalUniques` that combines the [ruleset]'s [globalUniques][Ruleset.globalUniques] /** Access a cached `GlobalUniques` that combines the [ruleset]'s [globalUniques][Ruleset.globalUniques]
* with the Uniques of the chosen [speed] and [difficulty][getDifficulty] */ * with the Uniques of the chosen [speed] and [difficulty][getDifficulty] */
@Readonly @Suppress("purity") // This should be autorecognized!!
fun getGlobalUniques() = combinedGlobalUniques fun getGlobalUniques() = combinedGlobalUniques
/** @return Sequence of all cities in game, both major civilizations and city states */ /** @return Sequence of all cities in game, both major civilizations and city states */

View File

@ -545,6 +545,7 @@ class City : IsPartOfGameInfoSerialization, INamed {
} }
// Uniques coming from this city, but that should be provided globally // Uniques coming from this city, but that should be provided globally
@Readonly
fun getMatchingUniquesWithNonLocalEffects(uniqueType: UniqueType, gameContext: GameContext = state): Sequence<Unique> { fun getMatchingUniquesWithNonLocalEffects(uniqueType: UniqueType, gameContext: GameContext = state): Sequence<Unique> {
val uniques = cityConstructions.builtBuildingUniqueMap.getUniques(uniqueType) val uniques = cityConstructions.builtBuildingUniqueMap.getUniques(uniqueType)
// Memory performance showed that this function was very memory intensive, thus we only create the filter if needed // Memory performance showed that this function was very memory intensive, thus we only create the filter if needed

View File

@ -339,18 +339,23 @@ class Civilization : IsPartOfGameInfoSerialization {
* city-states to contain the barbarians. Therefore, [getKnownCivs] will **not** list the barbarians * city-states to contain the barbarians. Therefore, [getKnownCivs] will **not** list the barbarians
* for major civs, but **will** do so for city-states after some gameplay. * for major civs, but **will** do so for city-states after some gameplay.
*/ */
@Readonly
fun getKnownCivs() = diplomacy.values.asSequence().map { it.otherCiv() } fun getKnownCivs() = diplomacy.values.asSequence().map { it.otherCiv() }
.filter { !it.isDefeated() && !it.isSpectator() } .filter { !it.isDefeated() && !it.isSpectator() }
fun getKnownCivsWithSpectators() = diplomacy.values.asSequence().map { it.otherCiv() } fun getKnownCivsWithSpectators() = diplomacy.values.asSequence().map { it.otherCiv() }
.filter { !it.isDefeated() } .filter { !it.isDefeated() }
@Readonly
fun knows(otherCivName: String) = diplomacy.containsKey(otherCivName) fun knows(otherCivName: String) = diplomacy.containsKey(otherCivName)
@Readonly
fun knows(otherCiv: Civilization) = knows(otherCiv.civName) fun knows(otherCiv: Civilization) = knows(otherCiv.civName)
fun getCapital(firstCityIfNoCapital: Boolean = true) = cities.firstOrNull { it.isCapital() } ?: fun getCapital(firstCityIfNoCapital: Boolean = true) = cities.firstOrNull { it.isCapital() } ?:
if (firstCityIfNoCapital) cities.firstOrNull() else null if (firstCityIfNoCapital) cities.firstOrNull() else null
@Readonly
fun isHuman() = playerType == PlayerType.Human fun isHuman() = playerType == PlayerType.Human
@Readonly
fun isAI() = playerType == PlayerType.AI fun isAI() = playerType == PlayerType.AI
fun isAIOrAutoPlaying(): Boolean { fun isAIOrAutoPlaying(): Boolean {
if (playerType == PlayerType.AI) return true if (playerType == PlayerType.AI) return true
@ -370,7 +375,9 @@ class Civilization : IsPartOfGameInfoSerialization {
@delegate:Transient @delegate:Transient
val isBarbarian by lazy { nation.isBarbarian } val isBarbarian by lazy { nation.isBarbarian }
@Readonly
fun isSpectator() = nation.isSpectator fun isSpectator() = nation.isSpectator
@Readonly
fun isAlive(): Boolean = !isDefeated() fun isAlive(): Boolean = !isDefeated()
@delegate:Transient @delegate:Transient
@ -524,11 +531,13 @@ class Civilization : IsPartOfGameInfoSerialization {
fun hasResource(resourceName: String): Boolean = getResourceAmount(resourceName) > 0 fun hasResource(resourceName: String): Boolean = getResourceAmount(resourceName) > 0
@Readonly
fun hasUnique(uniqueType: UniqueType, gameContext: GameContext = state) = fun hasUnique(uniqueType: UniqueType, gameContext: GameContext = state) =
getMatchingUniques(uniqueType, gameContext).any() getMatchingUniques(uniqueType, gameContext).any()
// Does not return local uniques, only global ones. // Does not return local uniques, only global ones.
/** Destined to replace getMatchingUniques, gradually, as we fill the enum */ /** Destined to replace getMatchingUniques, gradually, as we fill the enum */
@Readonly
fun getMatchingUniques( fun getMatchingUniques(
uniqueType: UniqueType, uniqueType: UniqueType,
gameContext: GameContext = state gameContext: GameContext = state
@ -653,16 +662,20 @@ class Civilization : IsPartOfGameInfoSerialization {
* If the civ has never controlled an original capital, it stays 'alive' as long as it has units (irrespective of non-original-capitals owned) * If the civ has never controlled an original capital, it stays 'alive' as long as it has units (irrespective of non-original-capitals owned)
* Otherwise, it stays 'alive' as long as it has cities (irrespective of settlers owned) * Otherwise, it stays 'alive' as long as it has cities (irrespective of settlers owned)
*/ */
@Readonly
fun isDefeated() = when { fun isDefeated() = when {
isBarbarian || isSpectator() -> false // Barbarians and voyeurs can't lose isBarbarian || isSpectator() -> false // Barbarians and voyeurs can't lose
hasEverOwnedOriginalCapital -> cities.isEmpty() hasEverOwnedOriginalCapital -> cities.isEmpty()
else -> units.getCivUnitsSize() == 0 else -> units.getCivUnitsSize() == 0
} }
@Readonly
fun getEra(): Era = tech.era fun getEra(): Era = tech.era
@Readonly
fun getEraNumber(): Int = getEra().eraNumber fun getEraNumber(): Int = getEra().eraNumber
@Readonly
fun isAtWarWith(otherCiv: Civilization) = diplomacyFunctions.isAtWarWith(otherCiv) fun isAtWarWith(otherCiv: Civilization) = diplomacyFunctions.isAtWarWith(otherCiv)
fun isAtWar() = diplomacy.values.any { it.diplomaticStatus == DiplomaticStatus.War && !it.otherCiv().isDefeated() } fun isAtWar() = diplomacy.values.any { it.diplomaticStatus == DiplomaticStatus.War && !it.otherCiv().isDefeated() }
@ -1039,6 +1052,7 @@ class Civilization : IsPartOfGameInfoSerialization {
fun getAllyCiv(): Civilization? = if (allyCivName == null) null fun getAllyCiv(): Civilization? = if (allyCivName == null) null
else gameInfo.getCivilization(allyCivName!!) else gameInfo.getCivilization(allyCivName!!)
@Readonly @Suppress("purity") // should be autorecognized!
fun getAllyCivName() = allyCivName fun getAllyCivName() = allyCivName
fun setAllyCiv(newAllyName: String?) { allyCivName = newAllyName } fun setAllyCiv(newAllyName: String?) { allyCivName = newAllyName }

View File

@ -27,6 +27,7 @@ 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.Readonly
import kotlin.math.min import kotlin.math.min
import kotlin.math.pow import kotlin.math.pow
import kotlin.random.Random import kotlin.random.Random
@ -408,10 +409,12 @@ class CityStateFunctions(val civInfo: Civilization) {
civInfo.destroy(notificationLocation) civInfo.destroy(notificationLocation)
} }
@Readonly
fun getTributeWillingness(demandingCiv: Civilization, demandingWorker: Boolean = false): Int { fun getTributeWillingness(demandingCiv: Civilization, demandingWorker: Boolean = false): Int {
return getTributeModifiers(demandingCiv, demandingWorker).values.sum() return getTributeModifiers(demandingCiv, demandingWorker).values.sum()
} }
@Readonly @Suppress("purity")
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> {
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
@ -789,6 +792,7 @@ class CityStateFunctions(val civInfo: Civilization) {
} }
// TODO: Optimize, update whenever status changes, otherwise retain the same list // TODO: Optimize, update whenever status changes, otherwise retain the same list
@Readonly
fun getUniquesProvidedByCityStates( fun getUniquesProvidedByCityStates(
uniqueType: UniqueType, uniqueType: UniqueType,
gameContext: GameContext gameContext: GameContext
@ -809,6 +813,7 @@ class CityStateFunctions(val civInfo: Civilization) {
} }
@Readonly
fun getCityStateBonuses(cityStateType: CityStateType, relationshipLevel: RelationshipLevel, uniqueType: UniqueType? = null): Sequence<Unique> { fun getCityStateBonuses(cityStateType: CityStateType, relationshipLevel: RelationshipLevel, uniqueType: UniqueType? = null): Sequence<Unique> {
val cityStateUniqueMap = when (relationshipLevel) { val cityStateUniqueMap = when (relationshipLevel) {
RelationshipLevel.Ally -> cityStateType.allyBonusUniqueMap RelationshipLevel.Ally -> cityStateType.allyBonusUniqueMap

View File

@ -11,6 +11,7 @@ import com.unciv.logic.map.tile.Tile
import com.unciv.models.ruleset.unique.UniqueType 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 yairm210.purity.annotations.Readonly
import kotlin.math.max import kotlin.math.max
class DiplomacyFunctions(val civInfo: Civilization) { class DiplomacyFunctions(val civInfo: Civilization) {
@ -74,7 +75,7 @@ class DiplomacyFunctions(val civInfo: Civilization) {
} }
} }
@Readonly
fun isAtWarWith(otherCiv: Civilization): Boolean { fun isAtWarWith(otherCiv: Civilization): Boolean {
return when { return when {
otherCiv == civInfo -> false otherCiv == civInfo -> false

View File

@ -17,6 +17,7 @@ import com.unciv.models.ruleset.unique.UniqueTriggerActivation
import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.translations.fillPlaceholders import com.unciv.models.translations.fillPlaceholders
import com.unciv.ui.components.extensions.toPercent import com.unciv.ui.components.extensions.toPercent
import yairm210.purity.annotations.Readonly
import kotlin.math.ceil import kotlin.math.ceil
import kotlin.math.max import kotlin.math.max
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -34,6 +35,7 @@ enum class RelationshipLevel(val color: Color) {
Friend(Color.ROYAL), Friend(Color.ROYAL),
Ally(Color.CYAN) Ally(Color.CYAN)
; ;
@Readonly
operator fun plus(delta: Int): RelationshipLevel { operator fun plus(delta: Int): RelationshipLevel {
val newOrdinal = (ordinal + delta).coerceIn(0, entries.size - 1) val newOrdinal = (ordinal + delta).coerceIn(0, entries.size - 1)
return entries[newOrdinal] return entries[newOrdinal]
@ -196,7 +198,9 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
} }
//region pure functions //region pure functions
@Readonly
fun otherCiv() = civInfo.gameInfo.getCivilization(otherCivName) fun otherCiv() = civInfo.gameInfo.getCivilization(otherCivName)
@Readonly
fun otherCivDiplomacy() = otherCiv().getDiplomacyManager(civInfo)!! fun otherCivDiplomacy() = otherCiv().getDiplomacyManager(civInfo)!!
fun turnsToPeaceTreaty(): Int { fun turnsToPeaceTreaty(): Int {
@ -206,6 +210,7 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
return 0 return 0
} }
@Readonly
fun opinionOfOtherCiv(): Float { fun opinionOfOtherCiv(): Float {
var modifierSum = diplomaticModifiers.values.sum() var modifierSum = diplomaticModifiers.values.sum()
// Angry about attacked CS and destroyed CS do not stack // Angry about attacked CS and destroyed CS do not stack
@ -224,6 +229,7 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
* @param comparesAs same as [RelationshipLevel.compareTo] * @param comparesAs same as [RelationshipLevel.compareTo]
* @return `true` if [relationshipLevel] ().compareTo([level]) == [comparesAs] - or: when [comparesAs] > 0 only if [relationshipLevel] > [level] and so on. * @return `true` if [relationshipLevel] ().compareTo([level]) == [comparesAs] - or: when [comparesAs] > 0 only if [relationshipLevel] > [level] and so on.
*/ */
@Readonly
private fun compareRelationshipLevel(level: RelationshipLevel, comparesAs: Int): Boolean { private fun compareRelationshipLevel(level: RelationshipLevel, comparesAs: Int): Boolean {
if (!civInfo.isCityState) if (!civInfo.isCityState)
return relationshipLevel().compareTo(level).sign == comparesAs return relationshipLevel().compareTo(level).sign == comparesAs
@ -259,6 +265,7 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
if (level == RelationshipLevel.Ally) true if (level == RelationshipLevel.Ally) true
else compareRelationshipLevel(level + 1, -1) else compareRelationshipLevel(level + 1, -1)
/** @see compareRelationshipLevel */ /** @see compareRelationshipLevel */
@Readonly
fun isRelationshipLevelGE(level: RelationshipLevel) = fun isRelationshipLevelGE(level: RelationshipLevel) =
if (level == RelationshipLevel.Unforgivable) true if (level == RelationshipLevel.Unforgivable) true
else compareRelationshipLevel(level + -1, 1) else compareRelationshipLevel(level + -1, 1)
@ -268,6 +275,7 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
* @see compareRelationshipLevel * @see compareRelationshipLevel
* @see relationshipIgnoreAfraid * @see relationshipIgnoreAfraid
*/ */
@Readonly
fun relationshipLevel(): RelationshipLevel { fun relationshipLevel(): RelationshipLevel {
val level = relationshipIgnoreAfraid() val level = relationshipIgnoreAfraid()
return when { return when {
@ -278,6 +286,7 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
} }
/** Same as [relationshipLevel] but omits the distinction Neutral/Afraid, which can be _much_ cheaper */ /** Same as [relationshipLevel] but omits the distinction Neutral/Afraid, which can be _much_ cheaper */
@Readonly
fun relationshipIgnoreAfraid(): RelationshipLevel { fun relationshipIgnoreAfraid(): RelationshipLevel {
if (civInfo.isHuman() && otherCiv().isHuman()) if (civInfo.isHuman() && otherCiv().isHuman())
return RelationshipLevel.Neutral // People make their own choices. return RelationshipLevel.Neutral // People make their own choices.
@ -365,6 +374,7 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
civInfo.cityStateFunctions.updateAllyCivForCityState() civInfo.cityStateFunctions.updateAllyCivForCityState()
} }
@Readonly
fun getInfluence() = if (civInfo.isAtWarWith(otherCiv())) MINIMUM_INFLUENCE else influence fun getInfluence() = if (civInfo.isAtWarWith(otherCiv())) MINIMUM_INFLUENCE else influence
// To be run from City-State DiplomacyManager, which holds the influence. Resting point for every major civ can be different. // To be run from City-State DiplomacyManager, which holds the influence. Resting point for every major civ can be different.
@ -549,12 +559,14 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
diplomaticModifiers[modifier.name] = amount diplomaticModifiers[modifier.name] = amount
} }
@Readonly
internal fun getModifier(modifier: DiplomaticModifiers): Float { internal fun getModifier(modifier: DiplomaticModifiers): Float {
if (!hasModifier(modifier)) return 0f if (!hasModifier(modifier)) return 0f
return diplomaticModifiers[modifier.name]!! return diplomaticModifiers[modifier.name]!!
} }
internal fun removeModifier(modifier: DiplomaticModifiers) = diplomaticModifiers.remove(modifier.name) internal fun removeModifier(modifier: DiplomaticModifiers) = diplomaticModifiers.remove(modifier.name)
@Readonly
fun hasModifier(modifier: DiplomaticModifiers) = diplomaticModifiers.containsKey(modifier.name) fun hasModifier(modifier: DiplomaticModifiers) = diplomaticModifiers.containsKey(modifier.name)
fun signDeclarationOfFriendship() { fun signDeclarationOfFriendship() {

View File

@ -13,6 +13,7 @@ import com.unciv.models.ruleset.unique.UniqueTarget
import com.unciv.models.ruleset.unique.UniqueTriggerActivation import com.unciv.models.ruleset.unique.UniqueTriggerActivation
import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.ruleset.unit.BaseUnit import com.unciv.models.ruleset.unit.BaseUnit
import yairm210.purity.annotations.Readonly
class UnitManager(val civInfo: Civilization) { class UnitManager(val civInfo: Civilization) {
@ -126,6 +127,7 @@ class UnitManager(val civInfo: Civilization) {
} }
return unit return unit
} }
@Readonly
fun getCivUnitsSize(): Int = unitList.size fun getCivUnitsSize(): Int = unitList.size
fun getCivUnits(): Sequence<MapUnit> = unitList.asSequence() fun getCivUnits(): Sequence<MapUnit> = unitList.asSequence()
fun getCivGreatPeople(): Sequence<MapUnit> = getCivUnits().filter { mapUnit -> mapUnit.isGreatPerson() } fun getCivGreatPeople(): Sequence<MapUnit> = getCivUnits().filter { mapUnit -> mapUnit.isGreatPerson() }

View File

@ -650,6 +650,7 @@ class Tile : IsPartOfGameInfoSerialization, Json.Serializable {
} }
} }
@Readonly @Suppress("purity") // sets cached value
fun isAdjacentToRiver(): Boolean { fun isAdjacentToRiver(): Boolean {
if (!isAdjacentToRiverKnown) { if (!isAdjacentToRiverKnown) {
isAdjacentToRiver = isAdjacentToRiver =

View File

@ -43,9 +43,11 @@ interface IHasUniques : INamed {
* */ * */
fun getUniqueTarget(): UniqueTarget fun getUniqueTarget(): UniqueTarget
@Readonly
fun getMatchingUniques(uniqueType: UniqueType, state: GameContext = GameContext.EmptyState) = fun getMatchingUniques(uniqueType: UniqueType, state: GameContext = GameContext.EmptyState) =
uniqueMap.getMatchingUniques(uniqueType, state) uniqueMap.getMatchingUniques(uniqueType, state)
@Readonly
fun getMatchingUniques(uniqueTag: String, state: GameContext = GameContext.EmptyState) = fun getMatchingUniques(uniqueTag: String, state: GameContext = GameContext.EmptyState) =
uniqueMap.getMatchingUniques(uniqueTag, state) uniqueMap.getMatchingUniques(uniqueTag, state)

View File

@ -1,6 +1,7 @@
package com.unciv.models.ruleset.unique package com.unciv.models.ruleset.unique
import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.logic.IsPartOfGameInfoSerialization
import yairm210.purity.annotations.Readonly
class TemporaryUnique() : IsPartOfGameInfoSerialization { class TemporaryUnique() : IsPartOfGameInfoSerialization {
@ -32,6 +33,7 @@ fun ArrayList<TemporaryUnique>.endTurn() {
removeAll { it.turnsLeft == 0 } removeAll { it.turnsLeft == 0 }
} }
@Readonly
fun ArrayList<TemporaryUnique>.getMatchingUniques(uniqueType: UniqueType, gameContext: GameContext): Sequence<Unique> { fun ArrayList<TemporaryUnique>.getMatchingUniques(uniqueType: UniqueType, gameContext: GameContext): Sequence<Unique> {
return this.asSequence() return this.asSequence()
.map { it.uniqueObject } .map { it.uniqueObject }