chore: purity - big batch

This commit is contained in:
yairm210 2025-07-18 11:29:57 +03:00
parent c1f0b97e2d
commit f6b71fc0af
19 changed files with 83 additions and 57 deletions

View File

@ -37,7 +37,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.25" apply(false) id("io.github.yairm210.purity-plugin") version "0.0.27" apply(false)
} }
allprojects { allprojects {
@ -51,7 +51,8 @@ allprojects {
"com.unciv.logic.civilization.diplomacy.RelationshipLevel.compareTo", "com.unciv.logic.civilization.diplomacy.RelationshipLevel.compareTo",
"kotlin.math.max", "kotlin.math.max",
"kotlin.math.min", "kotlin.math.min",
"kotlin.math.abs" "kotlin.math.abs",
"kotlin.internal.ir.noWhenBranchMatchedException",
) )
wellKnownReadonlyFunctions = setOf( wellKnownReadonlyFunctions = setOf(
// Looks like the Collection.contains is not considered overridden :thunk: // Looks like the Collection.contains is not considered overridden :thunk:

View File

@ -239,18 +239,17 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion
fun getCivilizationsAsPreviews() = civilizations.map { it.asPreview() }.toMutableList() fun getCivilizationsAsPreviews() = civilizations.map { it.asPreview() }.toMutableList()
/** Get barbarian civ /** Get barbarian civ
* @throws NoSuchElementException in no-barbarians games! */ * @throws NoSuchElementException in no-barbarians games! */
fun getBarbarianCivilization() = getCivilization(Constants.barbarians) @Readonly fun getBarbarianCivilization() = getCivilization(Constants.barbarians)
@Readonly @Suppress("purity") // This should be autorecognized!!
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!! @Readonly @Suppress("purity") // 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 */
fun getCities() = civilizations.asSequence().flatMap { it.cities } @Readonly fun getCities() = civilizations.asSequence().flatMap { it.cities }
fun getAliveCityStates() = civilizations.filter { it.isAlive() && it.isCityState } @Readonly fun getAliveCityStates() = civilizations.filter { it.isAlive() && it.isCityState }
fun getAliveMajorCivs() = civilizations.filter { it.isAlive() && it.isMajorCiv() } @Readonly fun getAliveMajorCivs() = civilizations.filter { it.isAlive() && it.isMajorCiv() }
/** Gets civilizations in their commonly used order - City-states last, /** Gets civilizations in their commonly used order - City-states last,
* otherwise alphabetically by culture and translation. [civToSortFirst] can be used to force * otherwise alphabetically by culture and translation. [civToSortFirst] can be used to force

View File

@ -10,6 +10,8 @@ import com.unciv.models.ruleset.unique.GameContext
import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.ruleset.unit.UnitType import com.unciv.models.ruleset.unit.UnitType
import com.unciv.ui.components.extensions.toPercent import com.unciv.ui.components.extensions.toPercent
import yairm210.purity.annotations.Pure
import yairm210.purity.annotations.Readonly
import kotlin.math.pow import kotlin.math.pow
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -19,10 +21,10 @@ class CityCombatant(val city: City) : ICombatant {
} }
override fun getHealth(): Int = city.health override fun getHealth(): Int = city.health
override fun getCivInfo(): Civilization = city.civ @Readonly override fun getCivInfo(): Civilization = city.civ
override fun getTile(): Tile = city.getCenterTile() override fun getTile(): Tile = city.getCenterTile()
override fun getName(): String = city.name override fun getName(): String = city.name
override fun isDefeated(): Boolean = city.health == 1 @Readonly override fun isDefeated(): Boolean = city.health == 1
override fun isInvisible(to: Civilization): Boolean = false override fun isInvisible(to: Civilization): Boolean = false
override fun canAttack(): Boolean = city.canBombard() override fun canAttack(): Boolean = city.canBombard()
override fun matchesFilter(filter: String, multiFilter: Boolean) = override fun matchesFilter(filter: String, multiFilter: Boolean) =
@ -37,12 +39,13 @@ class CityCombatant(val city: City) : ICombatant {
override fun getUnitType(): UnitType = UnitType.City override fun getUnitType(): UnitType = UnitType.City
override fun getAttackingStrength(): Int = (getCityStrength(CombatAction.Attack) * 0.75).roundToInt() override fun getAttackingStrength(): Int = (getCityStrength(CombatAction.Attack) * 0.75).roundToInt()
override fun getDefendingStrength(attackedByRanged: Boolean): Int { @Readonly override fun getDefendingStrength(attackedByRanged: Boolean): Int {
if (isDefeated()) return 1 if (isDefeated()) return 1
return getCityStrength() return getCityStrength()
} }
@Suppress("MemberVisibilityCanBePrivate") @Suppress("MemberVisibilityCanBePrivate")
@Readonly
fun getCityStrength(combatAction: CombatAction = CombatAction.Defend): Int { // Civ fanatics forum, from a modder who went through the original code fun getCityStrength(combatAction: CombatAction = CombatAction.Defend): Int { // Civ fanatics forum, from a modder who went through the original code
val modConstants = getCivInfo().gameInfo.ruleset.modOptions.constants val modConstants = getCivInfo().gameInfo.ruleset.modOptions.constants
var strength = modConstants.cityStrengthBase var strength = modConstants.cityStrengthBase

View File

@ -165,22 +165,21 @@ class City : IsPartOfGameInfoSerialization, INamed {
return toReturn return toReturn
} }
fun canBombard() = !attackedThisTurn && !isInResistance() @Readonly fun canBombard() = !attackedThisTurn && !isInResistance()
@Readonly @Suppress("purity") // should be autorecognized
fun getCenterTile(): Tile = centerTile fun getCenterTile(): Tile = centerTile
fun getCenterTileOrNull(): Tile? = if (::centerTile.isInitialized) centerTile else null @Readonly fun getCenterTileOrNull(): Tile? = if (::centerTile.isInitialized) centerTile else null
fun getTiles(): Sequence<Tile> = tiles.asSequence().map { tileMap[it] } @Readonly fun getTiles(): Sequence<Tile> = tiles.asSequence().map { tileMap[it] }
fun getWorkableTiles() = tilesInRange.asSequence().filter { it.getOwner() == civ } @Readonly fun getWorkableTiles() = tilesInRange.asSequence().filter { it.getOwner() == civ }
@Readonly @Readonly fun isWorked(tile: Tile) = workedTiles.contains(tile.position)
fun isWorked(tile: Tile) = workedTiles.contains(tile.position)
@Readonly
fun isCapital(): Boolean = cityConstructions.builtBuildingUniqueMap.hasUnique(UniqueType.IndicatesCapital, state)
@Readonly
fun isCoastal(): Boolean = centerTile.isCoastalTile()
fun getBombardRange(): Int = civ.gameInfo.ruleset.modOptions.constants.baseCityBombardRange @Readonly fun isCapital(): Boolean = cityConstructions.builtBuildingUniqueMap.hasUnique(UniqueType.IndicatesCapital, state)
fun getWorkRange(): Int = civ.gameInfo.ruleset.modOptions.constants.cityWorkRange @Readonly fun isCoastal(): Boolean = centerTile.isCoastalTile()
fun getExpandRange(): Int = civ.gameInfo.ruleset.modOptions.constants.cityExpandRange
@Readonly fun getBombardRange(): Int = civ.gameInfo.ruleset.modOptions.constants.baseCityBombardRange
@Readonly fun getWorkRange(): Int = civ.gameInfo.ruleset.modOptions.constants.cityWorkRange
@Readonly fun getExpandRange(): Int = civ.gameInfo.ruleset.modOptions.constants.cityExpandRange
fun isConnectedToCapital(connectionTypePredicate: (Set<String>) -> Boolean = { true }): Boolean { fun isConnectedToCapital(connectionTypePredicate: (Set<String>) -> Boolean = { true }): Boolean {
val mediumTypes = civ.cache.citiesConnectedToCapitalToMediums[this] ?: return false val mediumTypes = civ.cache.citiesConnectedToCapitalToMediums[this] ?: return false
@ -193,11 +192,11 @@ class City : IsPartOfGameInfoSerialization, INamed {
it.civ == this.civ && it.canGarrison() it.civ == this.civ && it.canGarrison()
} }
fun hasFlag(flag: CityFlags) = flagsCountdown.containsKey(flag.name) @Readonly fun hasFlag(flag: CityFlags) = flagsCountdown.containsKey(flag.name)
fun getFlag(flag: CityFlags) = flagsCountdown[flag.name]!! @Readonly fun getFlag(flag: CityFlags) = flagsCountdown[flag.name]!!
fun isWeLoveTheKingDayActive() = hasFlag(CityFlags.WeLoveTheKing) fun isWeLoveTheKingDayActive() = hasFlag(CityFlags.WeLoveTheKing)
fun isInResistance() = hasFlag(CityFlags.Resistance) @Readonly fun isInResistance() = hasFlag(CityFlags.Resistance)
fun isBlockaded(): Boolean { fun isBlockaded(): Boolean {
// Coastal cities are blocked if every adjacent water tile is blocked // Coastal cities are blocked if every adjacent water tile is blocked
if (!isCoastal()) return false if (!isCoastal()) return false
@ -281,7 +280,7 @@ class City : IsPartOfGameInfoSerialization, INamed {
internal fun getMaxHealth() = internal fun getMaxHealth() =
200 + cityConstructions.getBuiltBuildings().sumOf { it.cityHealth } 200 + cityConstructions.getBuiltBuildings().sumOf { it.cityHealth }
fun getStrength() = cityConstructions.getBuiltBuildings().sumOf { it.cityStrength }.toFloat() @Readonly fun getStrength() = cityConstructions.getBuiltBuildings().sumOf { it.cityStrength }.toFloat()
// This should probably be configurable // This should probably be configurable
@Transient @Transient

View File

@ -37,6 +37,7 @@ import com.unciv.ui.screens.civilopediascreen.FormattedLine
import com.unciv.ui.screens.pickerscreens.PromotionTree import com.unciv.ui.screens.pickerscreens.PromotionTree
import com.unciv.utils.withItem import com.unciv.utils.withItem
import com.unciv.utils.withoutItem import com.unciv.utils.withoutItem
import yairm210.purity.annotations.Readonly
import kotlin.math.ceil import kotlin.math.ceil
import kotlin.math.min import kotlin.math.min
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -253,7 +254,7 @@ class CityConstructions : IsPartOfGameInfoSerialization {
throw NotBuildingOrUnitException("$constructionName is not a building or a unit!") throw NotBuildingOrUnitException("$constructionName is not a building or a unit!")
} }
fun getBuiltBuildings(): Sequence<Building> = builtBuildingObjects.asSequence() @Readonly fun getBuiltBuildings(): Sequence<Building> = builtBuildingObjects.asSequence()
fun containsBuildingOrEquivalent(buildingNameOrUnique: String): Boolean = fun containsBuildingOrEquivalent(buildingNameOrUnique: String): Boolean =
isBuilt(buildingNameOrUnique) || getBuiltBuildings().any { it.replaces == buildingNameOrUnique || it.hasUnique(buildingNameOrUnique, city.state) } isBuilt(buildingNameOrUnique) || getBuiltBuildings().any { it.replaces == buildingNameOrUnique || it.hasUnique(buildingNameOrUnique, city.state) }

View File

@ -425,6 +425,7 @@ class Civilization : IsPartOfGameInfoSerialization {
stats.statsForNextTurn = newStats stats.statsForNextTurn = newStats
} }
@Readonly
fun getHappiness() = stats.happiness fun getHappiness() = stats.happiness
/** Note that for stockpiled resources, this gives by how much it grows per turn, not current amount */ /** Note that for stockpiled resources, this gives by how much it grows per turn, not current amount */
@ -694,7 +695,7 @@ class Civilization : IsPartOfGameInfoSerialization {
} }
} }
@Readonly
fun getStatForRanking(category: RankingType): Int { fun getStatForRanking(category: RankingType): Int {
return if (isDefeated()) 0 return if (isDefeated()) 0
else when (category) { else when (category) {
@ -711,12 +712,14 @@ class Civilization : IsPartOfGameInfoSerialization {
} }
} }
@Readonly @Suppress("purity") // caches
private fun getMilitaryMight(): Int { private fun getMilitaryMight(): Int {
if (cachedMilitaryMight < 0) if (cachedMilitaryMight < 0)
cachedMilitaryMight = calculateMilitaryMight() cachedMilitaryMight = calculateMilitaryMight()
return cachedMilitaryMight return cachedMilitaryMight
} }
@Readonly
private fun calculateMilitaryMight(): Int { private fun calculateMilitaryMight(): Int {
var sum = 1 // minimum value, so we never end up with 0 var sum = 1 // minimum value, so we never end up with 0
for (unit in units.getCivUnits()) { for (unit in units.getCivUnits()) {
@ -740,6 +743,7 @@ class Civilization : IsPartOfGameInfoSerialization {
} }
fun isLongCountDisplay() = hasLongCountDisplayUnique && isLongCountActive() fun isLongCountDisplay() = hasLongCountDisplayUnique && isLongCountActive()
@Readonly
fun calculateScoreBreakdown(): HashMap<String,Double> { fun calculateScoreBreakdown(): HashMap<String,Double> {
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,
@ -762,6 +766,7 @@ class Civilization : IsPartOfGameInfoSerialization {
return scoreBreakdown return scoreBreakdown
} }
@Readonly
fun calculateTotalScore() = calculateScoreBreakdown().values.sum() fun calculateTotalScore() = calculateScoreBreakdown().values.sum()
//endregion //endregion
@ -824,7 +829,7 @@ class Civilization : IsPartOfGameInfoSerialization {
fun getTurnsBetweenDiplomaticVotes() = (15 * gameInfo.speed.modifier).toInt() // Dunno the exact calculation, hidden in Lua files fun getTurnsBetweenDiplomaticVotes() = (15 * gameInfo.speed.modifier).toInt() // Dunno the exact calculation, hidden in Lua files
fun getTurnsTillNextDiplomaticVote() = flagsCountdown[CivFlags.TurnsTillNextDiplomaticVote.name] fun getTurnsTillNextDiplomaticVote() = flagsCountdown[CivFlags.TurnsTillNextDiplomaticVote.name]
fun getRecentBullyingCountdown() = flagsCountdown[CivFlags.RecentlyBullied.name] @Readonly fun getRecentBullyingCountdown() = flagsCountdown[CivFlags.RecentlyBullied.name]
fun getTurnsTillCallForBarbHelp() = flagsCountdown[CivFlags.TurnsTillCallForBarbHelp.name] fun getTurnsTillCallForBarbHelp() = flagsCountdown[CivFlags.TurnsTillCallForBarbHelp.name]
fun mayVoteForDiplomaticVictory() = fun mayVoteForDiplomaticVictory() =
@ -1046,7 +1051,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! @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.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
@ -214,6 +215,7 @@ class CityStateFunctions(val civInfo: Civilization) {
civInfo.questManager.receivedGoldGift(donorCiv) civInfo.questManager.receivedGoldGift(donorCiv)
} }
@Readonly
fun getProtectorCivs() : List<Civilization> { fun getProtectorCivs() : List<Civilization> {
if(civInfo.isMajorCiv()) return emptyList() if(civInfo.isMajorCiv()) return emptyList()
return civInfo.diplomacy.values return civInfo.diplomacy.values
@ -414,8 +416,9 @@ class CityStateFunctions(val civInfo: Civilization) {
return getTributeModifiers(demandingCiv, demandingWorker).values.sum() return getTributeModifiers(demandingCiv, demandingWorker).values.sum()
} }
@Readonly @Suppress("purity") // Local state update @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

@ -198,10 +198,9 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
} }
//region pure functions //region pure functions
@Readonly
fun otherCiv() = civInfo.gameInfo.getCivilization(otherCivName) @Readonly fun otherCiv() = civInfo.gameInfo.getCivilization(otherCivName)
@Readonly @Readonly fun otherCivDiplomacy() = otherCiv().getDiplomacyManager(civInfo)!!
fun otherCivDiplomacy() = otherCiv().getDiplomacyManager(civInfo)!!
fun turnsToPeaceTreaty(): Int { fun turnsToPeaceTreaty(): Int {
for (trade in trades) for (trade in trades)

View File

@ -96,7 +96,7 @@ class TechManager : IsPartOfGameInfoSerialization {
return toReturn return toReturn
} }
fun getNumberOfTechsResearched(): Int = techsResearched.size @Readonly fun getNumberOfTechsResearched(): Int = techsResearched.size
fun getOverflowScience(): Int = overflowScience fun getOverflowScience(): Int = overflowScience

View File

@ -127,10 +127,10 @@ class UnitManager(val civInfo: Civilization) {
} }
return unit return unit
} }
@Readonly
fun getCivUnitsSize(): Int = unitList.size @Readonly fun getCivUnitsSize(): Int = unitList.size
fun getCivUnits(): Sequence<MapUnit> = unitList.asSequence() @Readonly fun getCivUnits(): Sequence<MapUnit> = unitList.asSequence()
fun getCivGreatPeople(): Sequence<MapUnit> = getCivUnits().filter { mapUnit -> mapUnit.isGreatPerson() } @Readonly fun getCivGreatPeople(): Sequence<MapUnit> = getCivUnits().filter { mapUnit -> mapUnit.isGreatPerson() }
// Similar to getCivUnits(), but the returned list is rotated so that the // Similar to getCivUnits(), but the returned list is rotated so that the
// 'nextPotentiallyDueAt' unit is first here. // 'nextPotentiallyDueAt' unit is first here.

View File

@ -4,6 +4,7 @@ import com.unciv.logic.IsPartOfGameInfoSerialization
import com.unciv.logic.map.HexMath.getNumberOfTilesInHexagon import com.unciv.logic.map.HexMath.getNumberOfTilesInHexagon
import com.unciv.logic.map.mapgenerator.MapResourceSetting import com.unciv.logic.map.mapgenerator.MapResourceSetting
import com.unciv.models.metadata.BaseRuleset import com.unciv.models.metadata.BaseRuleset
import yairm210.purity.annotations.Readonly
object MapShape { object MapShape {
@ -172,6 +173,7 @@ class MapParameters : IsPartOfGameInfoSerialization {
yield(", {Water level}=" + waterThreshold.niceToString(2)) yield(", {Water level}=" + waterThreshold.niceToString(2))
}.joinToString("") }.joinToString("")
@Readonly
fun numberOfTiles() = fun numberOfTiles() =
if (shape == MapShape.hexagonal || shape == MapShape.flatEarth) { if (shape == MapShape.hexagonal || shape == MapShape.flatEarth) {
1 + 3 * mapSize.radius * (mapSize.radius - 1) 1 + 3 * mapSize.radius * (mapSize.radius - 1)

View File

@ -497,7 +497,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
// Only military land units can truly "garrison" // Only military land units can truly "garrison"
fun canGarrison() = isMilitary() && baseUnit.isLandUnit fun canGarrison() = isMilitary() && baseUnit.isLandUnit
fun isGreatPerson() = baseUnit.isGreatPerson @Readonly fun isGreatPerson() = baseUnit.isGreatPerson
fun isGreatPersonOfType(type: String) = baseUnit.isGreatPersonOfType(type) fun isGreatPersonOfType(type: String) = baseUnit.isGreatPersonOfType(type)
fun canIntercept(attackedTile: Tile): Boolean { fun canIntercept(attackedTile: Tile): Boolean {
@ -621,6 +621,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
return civ.gameInfo.religions[religion]!!.getReligionDisplayName() return civ.gameInfo.religions[religion]!!.getReligionDisplayName()
} }
@Readonly
fun getForceEvaluation(): Int { fun getForceEvaluation(): Int {
val promotionBonus = (promotions.numberOfPromotions + 1).toFloat().pow(0.3f) val promotionBonus = (promotions.numberOfPromotions + 1).toFloat().pow(0.3f)
var power = (baseUnit.getForceEvaluation() * promotionBonus).toInt() var power = (baseUnit.getForceEvaluation() * promotionBonus).toInt()

View File

@ -261,7 +261,6 @@ class Tile : IsPartOfGameInfoSerialization, Json.Serializable {
return null return null
} }
@Readonly @Suppress("purity") // should be autorecognized as readonly
fun getCity(): City? = owningCity fun getCity(): City? = owningCity
@Readonly internal fun getNaturalWonder(): Terrain = @Readonly internal fun getNaturalWonder(): Terrain =
@ -282,7 +281,6 @@ class Tile : IsPartOfGameInfoSerialization, Json.Serializable {
return exploredBy.contains(player.civName) return exploredBy.contains(player.civName)
} }
@Readonly @Suppress("purity") // should be autorecognized as readonly
fun isCityCenter(): Boolean = isCityCenterInternal fun isCityCenter(): Boolean = isCityCenterInternal
@Readonly fun isNaturalWonder(): Boolean = naturalWonder != null @Readonly fun isNaturalWonder(): Boolean = naturalWonder != null
@Readonly fun isImpassible() = lastTerrain.impassable @Readonly fun isImpassible() = lastTerrain.impassable
@ -583,7 +581,7 @@ class Tile : IsPartOfGameInfoSerialization, Json.Serializable {
} }
} }
@Readonly @Suppress("purity") // should be auto-recognized! @Readonly @Suppress("purity") // should be autorecognized
fun isCoastalTile() = _isCoastalTile fun isCoastalTile() = _isCoastalTile
@Readonly @Readonly
@ -743,7 +741,6 @@ class Tile : IsPartOfGameInfoSerialization, Json.Serializable {
return out return out
} }
@Readonly @Suppress("purity") // should be auto-recognized as readonly
fun getContinent() = continent fun getContinent() = continent
/** Checks if this tile is marked as target tile for a building with a [UniqueType.CreatesOneImprovement] unique */ /** Checks if this tile is marked as target tile for a building with a [UniqueType.CreatesOneImprovement] unique */

View File

@ -8,6 +8,7 @@ import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
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.Pure
open class Policy : RulesetObject() { open class Policy : RulesetObject() {
lateinit var branch: PolicyBranch // not in json - added in gameBasics lateinit var branch: PolicyBranch // not in json - added in gameBasics
@ -32,6 +33,7 @@ open class Policy : RulesetObject() {
/** Some tests to count policies by completion or not use only the String collection without instantiating them. /** Some tests to count policies by completion or not use only the String collection without instantiating them.
* To keep the hardcoding in one place, this is public and should be used instead of duplicating it. * To keep the hardcoding in one place, this is public and should be used instead of duplicating it.
*/ */
@Pure
fun isBranchCompleteByName(name: String) = name.endsWith(branchCompleteSuffix) fun isBranchCompleteByName(name: String) = name.endsWith(branchCompleteSuffix)
} }

View File

@ -48,12 +48,12 @@ class Unique(val text: String, val sourceObjectType: UniqueTarget? = null, val s
fun hasFlag(flag: UniqueFlag) = type != null && type.flags.contains(flag) fun hasFlag(flag: UniqueFlag) = type != null && type.flags.contains(flag)
fun isHiddenToUsers() = hasFlag(UniqueFlag.HiddenToUsers) || hasModifier(UniqueType.ModifierHiddenFromUsers) fun isHiddenToUsers() = hasFlag(UniqueFlag.HiddenToUsers) || hasModifier(UniqueType.ModifierHiddenFromUsers)
@Readonly fun getModifiers(type: UniqueType) = modifiersMap[type] ?: emptyList()
@Readonly fun hasModifier(type: UniqueType) = modifiersMap.containsKey(type)
@Readonly fun isModifiedByGameSpeed() = hasModifier(UniqueType.ModifiedByGameSpeed)
@Readonly fun isModifiedByGameProgress() = hasModifier(UniqueType.ModifiedByGameProgress)
@Readonly @Readonly
fun getModifiers(type: UniqueType) = modifiersMap[type] ?: emptyList()
@Readonly
fun hasModifier(type: UniqueType) = modifiersMap.containsKey(type)
fun isModifiedByGameSpeed() = hasModifier(UniqueType.ModifiedByGameSpeed)
fun isModifiedByGameProgress() = hasModifier(UniqueType.ModifiedByGameProgress)
fun getGameProgressModifier(civ: Civilization): Float { fun getGameProgressModifier(civ: Civilization): Float {
//According to: https://www.reddit.com/r/civ/comments/gvx44v/comment/fsrifc2/ //According to: https://www.reddit.com/r/civ/comments/gvx44v/comment/fsrifc2/
var modifier = 1f var modifier = 1f
@ -68,6 +68,7 @@ class Unique(val text: String, val sourceObjectType: UniqueTarget? = null, val s
//Mod creators likely expect this to stack multiplicatively, otherwise they'd use a single modifier //Mod creators likely expect this to stack multiplicatively, otherwise they'd use a single modifier
return modifier return modifier
} }
@Readonly
fun hasTriggerConditional(): Boolean { fun hasTriggerConditional(): Boolean {
if (modifiers.none()) return false if (modifiers.none()) return false
return modifiers.any { conditional -> return modifiers.any { conditional ->

View File

@ -1,5 +1,7 @@
package com.unciv.models.ruleset.unique package com.unciv.models.ruleset.unique
import yairm210.purity.annotations.Readonly
/** /**
* Expresses which RulesetObject types a UniqueType is applicable to. * Expresses which RulesetObject types a UniqueType is applicable to.
* *
@ -78,6 +80,7 @@ enum class UniqueTarget(
/** Checks whether a specific UniqueTarget `this` as e.g. given by [IHasUniques.getUniqueTarget] works with [uniqueTarget] as e.g. declared in UniqueType */ /** Checks whether a specific UniqueTarget `this` as e.g. given by [IHasUniques.getUniqueTarget] works with [uniqueTarget] as e.g. declared in UniqueType */
// Building.canAcceptUniqueTarget(Global) == true // Building.canAcceptUniqueTarget(Global) == true
// Global.canAcceptUniqueTarget(Building) == false // Global.canAcceptUniqueTarget(Building) == false
@Readonly
fun canAcceptUniqueTarget(uniqueTarget: UniqueTarget): Boolean { fun canAcceptUniqueTarget(uniqueTarget: UniqueTarget): Boolean {
if (this == uniqueTarget) return true if (this == uniqueTarget) return true
if (inheritsFrom != null) return inheritsFrom.canAcceptUniqueTarget(uniqueTarget) if (inheritsFrom != null) return inheritsFrom.canAcceptUniqueTarget(uniqueTarget)

View File

@ -136,30 +136,35 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction {
return unit return unit
} }
@Readonly
override fun hasUnique(uniqueType: UniqueType, state: GameContext?): Boolean { override fun hasUnique(uniqueType: UniqueType, state: GameContext?): Boolean {
val gameContext = state ?: GameContext.EmptyState val gameContext = state ?: GameContext.EmptyState
return if (::ruleset.isInitialized) rulesetUniqueMap.hasUnique(uniqueType, gameContext) return if (::ruleset.isInitialized) rulesetUniqueMap.hasUnique(uniqueType, gameContext)
else super<RulesetObject>.hasUnique(uniqueType, gameContext) else super<RulesetObject>.hasUnique(uniqueType, gameContext)
} }
@Readonly
override fun hasUnique(uniqueTag: String, state: GameContext?): Boolean { override fun hasUnique(uniqueTag: String, state: GameContext?): Boolean {
val gameContext = state ?: GameContext.EmptyState val gameContext = state ?: GameContext.EmptyState
return if (::ruleset.isInitialized) rulesetUniqueMap.hasUnique(uniqueTag, gameContext) return if (::ruleset.isInitialized) rulesetUniqueMap.hasUnique(uniqueTag, gameContext)
else super<RulesetObject>.hasUnique(uniqueTag, gameContext) else super<RulesetObject>.hasUnique(uniqueTag, gameContext)
} }
@Readonly
override fun hasTagUnique(tagUnique: String): Boolean { override fun hasTagUnique(tagUnique: String): Boolean {
return if (::ruleset.isInitialized) rulesetUniqueMap.hasTagUnique(tagUnique) return if (::ruleset.isInitialized) rulesetUniqueMap.hasTagUnique(tagUnique)
else super<RulesetObject>.hasTagUnique(tagUnique) else super<RulesetObject>.hasTagUnique(tagUnique)
} }
/** Allows unique functions (getMatchingUniques, hasUnique) to "see" uniques from the UnitType */ /** Allows unique functions (getMatchingUniques, hasUnique) to "see" uniques from the UnitType */
@Readonly
override fun getMatchingUniques(uniqueType: UniqueType, state: GameContext): Sequence<Unique> { override fun getMatchingUniques(uniqueType: UniqueType, state: GameContext): Sequence<Unique> {
return if (::ruleset.isInitialized) rulesetUniqueMap.getMatchingUniques(uniqueType, state) return if (::ruleset.isInitialized) rulesetUniqueMap.getMatchingUniques(uniqueType, state)
else super<RulesetObject>.getMatchingUniques(uniqueType, state) else super<RulesetObject>.getMatchingUniques(uniqueType, state)
} }
/** Allows unique functions (getMatchingUniques, hasUnique) to "see" uniques from the UnitType */ /** Allows unique functions (getMatchingUniques, hasUnique) to "see" uniques from the UnitType */
@Readonly
override fun getMatchingUniques(uniqueTag: String, state: GameContext): Sequence<Unique> { override fun getMatchingUniques(uniqueTag: String, state: GameContext): Sequence<Unique> {
return if (::ruleset.isInitialized) rulesetUniqueMap.getMatchingUniques(uniqueTag, state) return if (::ruleset.isInitialized) rulesetUniqueMap.getMatchingUniques(uniqueTag, state)
else super<RulesetObject>.getMatchingUniques(uniqueTag, state) else super<RulesetObject>.getMatchingUniques(uniqueTag, state)
@ -459,7 +464,7 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction {
fun isGreatPersonOfType(type: String) = getMatchingUniques(UniqueType.GreatPerson).any { it.params[0] == type } fun isGreatPersonOfType(type: String) = getMatchingUniques(UniqueType.GreatPerson).any { it.params[0] == type }
/** Has a MapUnit implementation that does not ignore conditionals, which should be usually used */ /** Has a MapUnit implementation that does not ignore conditionals, which should be usually used */
private fun isNuclearWeapon() = hasUnique(UniqueType.NuclearWeapon, GameContext.IgnoreConditionals) @Readonly private fun isNuclearWeapon() = hasUnique(UniqueType.NuclearWeapon, GameContext.IgnoreConditionals)
val movesLikeAirUnits by lazy { type.getMovementType() == UnitMovementType.Air } val movesLikeAirUnits by lazy { type.getMovementType() == UnitMovementType.Air }
@ -486,11 +491,13 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction {
&& getMatchingUniques(UniqueType.Strength, GameContext.IgnoreConditionals) && getMatchingUniques(UniqueType.Strength, GameContext.IgnoreConditionals)
.any { it.params[0].toInt() > 0 && it.hasModifier(UniqueType.ConditionalVsCity) } .any { it.params[0].toInt() > 0 && it.hasModifier(UniqueType.ConditionalVsCity) }
@Readonly
fun getForceEvaluation(): Int { fun getForceEvaluation(): Int {
if (cachedForceEvaluation < 0) evaluateForce() if (cachedForceEvaluation < 0) evaluateForce()
return cachedForceEvaluation return cachedForceEvaluation
} }
@Readonly @Suppress("purity") // reads from cache
private fun evaluateForce() { private fun evaluateForce() {
if (strength == 0 && rangedStrength == 0) { if (strength == 0 && rangedStrength == 0) {
cachedForceEvaluation = 0 cachedForceEvaluation = 0

View File

@ -14,6 +14,7 @@ import com.unciv.utils.Log
import com.unciv.utils.debug import com.unciv.utils.debug
import java.util.Locale import java.util.Locale
import org.jetbrains.annotations.VisibleForTesting import org.jetbrains.annotations.VisibleForTesting
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
@ -474,12 +475,13 @@ private fun String.translateIndividualWord(language: String, hideIcons: Boolean,
* For example, a string like 'The city of [New [York]]' will return ['New [York]'], * For example, a string like 'The city of [New [York]]' will return ['New [York]'],
* allowing us to have nested translations! * allowing us to have nested translations!
*/ */
@Readonly @Suppress("purity") // Local state update @Readonly
fun String.getPlaceholderParameters(): List<String> { fun String.getPlaceholderParameters(): List<String> {
if (!this.contains('[')) return emptyList() if (!this.contains('[')) return emptyList()
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

View File

@ -3,6 +3,7 @@ package com.unciv.ui.components.extensions
import com.badlogic.gdx.math.Vector2 import com.badlogic.gdx.math.Vector2
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
import com.unciv.ui.components.fonts.Fonts import com.unciv.ui.components.fonts.Fonts
import yairm210.purity.annotations.Pure
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.time.Duration import java.time.Duration
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
@ -11,13 +12,13 @@ import java.util.Locale
import java.util.SortedMap import java.util.SortedMap
/** Translate a percentage number - e.g. 25 - to the multiplication value - e.g. 1.25f */ /** Translate a percentage number - e.g. 25 - to the multiplication value - e.g. 1.25f */
fun String.toPercent() = toFloat().toPercent() @Pure fun String.toPercent() = toFloat().toPercent()
/** Translate a percentage number - e.g. 25 - to the multiplication value - e.g. 1.25f */ /** Translate a percentage number - e.g. 25 - to the multiplication value - e.g. 1.25f */
fun Int.toPercent() = toFloat().toPercent() @Pure fun Int.toPercent() = toFloat().toPercent()
/** Translate a percentage number - e.g. 25 - to the multiplication value - e.g. 1.25f */ /** Translate a percentage number - e.g. 25 - to the multiplication value - e.g. 1.25f */
fun Float.toPercent() = 1 + this/100 @Pure fun Float.toPercent() = 1 + this/100
/** Convert a [resource name][this] into "Consumes [amount] $resource" string (untranslated) */ /** Convert a [resource name][this] into "Consumes [amount] $resource" string (untranslated) */
fun String.getConsumesAmountString(amount: Int, isStockpiled: Boolean): String { fun String.getConsumesAmountString(amount: Int, isStockpiled: Boolean): String {