diff --git a/build.gradle.kts b/build.gradle.kts index dc4f61252a..44a1d0e9f8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -37,7 +37,7 @@ plugins { // This is *with* gradle 8.2 downloaded according the project specs, no idea what that's about kotlin("multiplatform") 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 { @@ -51,7 +51,8 @@ allprojects { "com.unciv.logic.civilization.diplomacy.RelationshipLevel.compareTo", "kotlin.math.max", "kotlin.math.min", - "kotlin.math.abs" + "kotlin.math.abs", + "kotlin.internal.ir.noWhenBranchMatchedException", ) wellKnownReadonlyFunctions = setOf( // Looks like the Collection.contains is not considered overridden :thunk: diff --git a/core/src/com/unciv/logic/GameInfo.kt b/core/src/com/unciv/logic/GameInfo.kt index 6c08e1ad63..427acf6dad 100644 --- a/core/src/com/unciv/logic/GameInfo.kt +++ b/core/src/com/unciv/logic/GameInfo.kt @@ -239,18 +239,17 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion fun getCivilizationsAsPreviews() = civilizations.map { it.asPreview() }.toMutableList() /** Get barbarian civ * @throws NoSuchElementException in no-barbarians games! */ - fun getBarbarianCivilization() = getCivilization(Constants.barbarians) - @Readonly @Suppress("purity") // This should be autorecognized!! + @Readonly fun getBarbarianCivilization() = getCivilization(Constants.barbarians) fun getDifficulty() = difficultyObject /** Access a cached `GlobalUniques` that combines the [ruleset]'s [globalUniques][Ruleset.globalUniques] * 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 /** @return Sequence of all cities in game, both major civilizations and city states */ - fun getCities() = civilizations.asSequence().flatMap { it.cities } - fun getAliveCityStates() = civilizations.filter { it.isAlive() && it.isCityState } - fun getAliveMajorCivs() = civilizations.filter { it.isAlive() && it.isMajorCiv() } + @Readonly fun getCities() = civilizations.asSequence().flatMap { it.cities } + @Readonly fun getAliveCityStates() = civilizations.filter { it.isAlive() && it.isCityState } + @Readonly fun getAliveMajorCivs() = civilizations.filter { it.isAlive() && it.isMajorCiv() } /** Gets civilizations in their commonly used order - City-states last, * otherwise alphabetically by culture and translation. [civToSortFirst] can be used to force diff --git a/core/src/com/unciv/logic/battle/CityCombatant.kt b/core/src/com/unciv/logic/battle/CityCombatant.kt index 19ca74c8d5..778820d0ef 100644 --- a/core/src/com/unciv/logic/battle/CityCombatant.kt +++ b/core/src/com/unciv/logic/battle/CityCombatant.kt @@ -10,6 +10,8 @@ import com.unciv.models.ruleset.unique.GameContext import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unit.UnitType import com.unciv.ui.components.extensions.toPercent +import yairm210.purity.annotations.Pure +import yairm210.purity.annotations.Readonly import kotlin.math.pow import kotlin.math.roundToInt @@ -19,10 +21,10 @@ class CityCombatant(val city: City) : ICombatant { } 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 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 canAttack(): Boolean = city.canBombard() 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 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 return getCityStrength() } @Suppress("MemberVisibilityCanBePrivate") + @Readonly 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 var strength = modConstants.cityStrengthBase diff --git a/core/src/com/unciv/logic/city/City.kt b/core/src/com/unciv/logic/city/City.kt index c58590a9bc..4695d60fcd 100644 --- a/core/src/com/unciv/logic/city/City.kt +++ b/core/src/com/unciv/logic/city/City.kt @@ -165,22 +165,21 @@ class City : IsPartOfGameInfoSerialization, INamed { return toReturn } - fun canBombard() = !attackedThisTurn && !isInResistance() + @Readonly fun canBombard() = !attackedThisTurn && !isInResistance() + @Readonly @Suppress("purity") // should be autorecognized fun getCenterTile(): Tile = centerTile - fun getCenterTileOrNull(): Tile? = if (::centerTile.isInitialized) centerTile else null - fun getTiles(): Sequence = tiles.asSequence().map { tileMap[it] } - fun getWorkableTiles() = tilesInRange.asSequence().filter { it.getOwner() == civ } - @Readonly - fun isWorked(tile: Tile) = workedTiles.contains(tile.position) + @Readonly fun getCenterTileOrNull(): Tile? = if (::centerTile.isInitialized) centerTile else null + @Readonly fun getTiles(): Sequence = tiles.asSequence().map { tileMap[it] } + @Readonly fun getWorkableTiles() = tilesInRange.asSequence().filter { it.getOwner() == civ } + @Readonly 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 - fun getWorkRange(): Int = civ.gameInfo.ruleset.modOptions.constants.cityWorkRange - fun getExpandRange(): Int = civ.gameInfo.ruleset.modOptions.constants.cityExpandRange + @Readonly fun isCapital(): Boolean = cityConstructions.builtBuildingUniqueMap.hasUnique(UniqueType.IndicatesCapital, state) + @Readonly fun isCoastal(): Boolean = centerTile.isCoastalTile() + + @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) -> Boolean = { true }): Boolean { val mediumTypes = civ.cache.citiesConnectedToCapitalToMediums[this] ?: return false @@ -193,11 +192,11 @@ class City : IsPartOfGameInfoSerialization, INamed { it.civ == this.civ && it.canGarrison() } - fun hasFlag(flag: CityFlags) = flagsCountdown.containsKey(flag.name) - fun getFlag(flag: CityFlags) = flagsCountdown[flag.name]!! + @Readonly fun hasFlag(flag: CityFlags) = flagsCountdown.containsKey(flag.name) + @Readonly fun getFlag(flag: CityFlags) = flagsCountdown[flag.name]!! fun isWeLoveTheKingDayActive() = hasFlag(CityFlags.WeLoveTheKing) - fun isInResistance() = hasFlag(CityFlags.Resistance) + @Readonly fun isInResistance() = hasFlag(CityFlags.Resistance) fun isBlockaded(): Boolean { // Coastal cities are blocked if every adjacent water tile is blocked if (!isCoastal()) return false @@ -281,7 +280,7 @@ class City : IsPartOfGameInfoSerialization, INamed { internal fun getMaxHealth() = 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 @Transient diff --git a/core/src/com/unciv/logic/city/CityConstructions.kt b/core/src/com/unciv/logic/city/CityConstructions.kt index 046d95bf75..b1a880da1f 100644 --- a/core/src/com/unciv/logic/city/CityConstructions.kt +++ b/core/src/com/unciv/logic/city/CityConstructions.kt @@ -37,6 +37,7 @@ import com.unciv.ui.screens.civilopediascreen.FormattedLine import com.unciv.ui.screens.pickerscreens.PromotionTree import com.unciv.utils.withItem import com.unciv.utils.withoutItem +import yairm210.purity.annotations.Readonly import kotlin.math.ceil import kotlin.math.min import kotlin.math.roundToInt @@ -253,7 +254,7 @@ class CityConstructions : IsPartOfGameInfoSerialization { throw NotBuildingOrUnitException("$constructionName is not a building or a unit!") } - fun getBuiltBuildings(): Sequence = builtBuildingObjects.asSequence() + @Readonly fun getBuiltBuildings(): Sequence = builtBuildingObjects.asSequence() fun containsBuildingOrEquivalent(buildingNameOrUnique: String): Boolean = isBuilt(buildingNameOrUnique) || getBuiltBuildings().any { it.replaces == buildingNameOrUnique || it.hasUnique(buildingNameOrUnique, city.state) } diff --git a/core/src/com/unciv/logic/civilization/Civilization.kt b/core/src/com/unciv/logic/civilization/Civilization.kt index 1d9449d688..61a91c7f30 100644 --- a/core/src/com/unciv/logic/civilization/Civilization.kt +++ b/core/src/com/unciv/logic/civilization/Civilization.kt @@ -425,6 +425,7 @@ class Civilization : IsPartOfGameInfoSerialization { stats.statsForNextTurn = newStats } + @Readonly fun getHappiness() = stats.happiness /** 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 { return if (isDefeated()) 0 else when (category) { @@ -711,12 +712,14 @@ class Civilization : IsPartOfGameInfoSerialization { } } + @Readonly @Suppress("purity") // caches private fun getMilitaryMight(): Int { if (cachedMilitaryMight < 0) cachedMilitaryMight = calculateMilitaryMight() return cachedMilitaryMight } + @Readonly private fun calculateMilitaryMight(): Int { var sum = 1 // minimum value, so we never end up with 0 for (unit in units.getCivUnits()) { @@ -740,6 +743,7 @@ class Civilization : IsPartOfGameInfoSerialization { } fun isLongCountDisplay() = hasLongCountDisplayUnique && isLongCountActive() + @Readonly fun calculateScoreBreakdown(): HashMap { val scoreBreakdown = hashMapOf() // 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 } + @Readonly fun calculateTotalScore() = calculateScoreBreakdown().values.sum() //endregion @@ -824,7 +829,7 @@ class Civilization : IsPartOfGameInfoSerialization { fun getTurnsBetweenDiplomaticVotes() = (15 * gameInfo.speed.modifier).toInt() // Dunno the exact calculation, hidden in Lua files 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 mayVoteForDiplomaticVictory() = @@ -1046,7 +1051,7 @@ class Civilization : IsPartOfGameInfoSerialization { fun getAllyCiv(): Civilization? = if (allyCivName == null) null else gameInfo.getCivilization(allyCivName!!) - @Readonly @Suppress("purity") // should be autorecognized! + @Readonly @Suppress("purity") // should be autorecognized fun getAllyCivName() = allyCivName fun setAllyCiv(newAllyName: String?) { allyCivName = newAllyName } diff --git a/core/src/com/unciv/logic/civilization/diplomacy/CityStateFunctions.kt b/core/src/com/unciv/logic/civilization/diplomacy/CityStateFunctions.kt index c005d9af26..6b674336d0 100644 --- a/core/src/com/unciv/logic/civilization/diplomacy/CityStateFunctions.kt +++ b/core/src/com/unciv/logic/civilization/diplomacy/CityStateFunctions.kt @@ -27,6 +27,7 @@ import com.unciv.models.ruleset.unit.BaseUnit import com.unciv.models.stats.Stat import com.unciv.ui.screens.victoryscreen.RankingType import com.unciv.utils.randomWeighted +import yairm210.purity.annotations.LocalState import yairm210.purity.annotations.Readonly import kotlin.math.min import kotlin.math.pow @@ -214,6 +215,7 @@ class CityStateFunctions(val civInfo: Civilization) { civInfo.questManager.receivedGoldGift(donorCiv) } + @Readonly fun getProtectorCivs() : List { if(civInfo.isMajorCiv()) return emptyList() return civInfo.diplomacy.values @@ -414,8 +416,9 @@ class CityStateFunctions(val civInfo: Civilization) { return getTributeModifiers(demandingCiv, demandingWorker).values.sum() } - @Readonly @Suppress("purity") // Local state update + @Readonly fun getTributeModifiers(demandingCiv: Civilization, demandingWorker: Boolean = false, requireWholeList: Boolean = false): HashMap { + @LocalState val modifiers = LinkedHashMap() // Linked to preserve order when presenting the modifiers table // Can't bully major civs or unsettled CS's if (!civInfo.isCityState) { diff --git a/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt b/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt index 10c49446f7..3d0f5977b1 100644 --- a/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt +++ b/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt @@ -198,10 +198,9 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization { } //region pure functions - @Readonly - fun otherCiv() = civInfo.gameInfo.getCivilization(otherCivName) - @Readonly - fun otherCivDiplomacy() = otherCiv().getDiplomacyManager(civInfo)!! + + @Readonly fun otherCiv() = civInfo.gameInfo.getCivilization(otherCivName) + @Readonly fun otherCivDiplomacy() = otherCiv().getDiplomacyManager(civInfo)!! fun turnsToPeaceTreaty(): Int { for (trade in trades) diff --git a/core/src/com/unciv/logic/civilization/managers/TechManager.kt b/core/src/com/unciv/logic/civilization/managers/TechManager.kt index 72e2c6dc7f..a59993ec3d 100644 --- a/core/src/com/unciv/logic/civilization/managers/TechManager.kt +++ b/core/src/com/unciv/logic/civilization/managers/TechManager.kt @@ -96,7 +96,7 @@ class TechManager : IsPartOfGameInfoSerialization { return toReturn } - fun getNumberOfTechsResearched(): Int = techsResearched.size + @Readonly fun getNumberOfTechsResearched(): Int = techsResearched.size fun getOverflowScience(): Int = overflowScience diff --git a/core/src/com/unciv/logic/civilization/managers/UnitManager.kt b/core/src/com/unciv/logic/civilization/managers/UnitManager.kt index dfa9d77bbd..5c9db6f2ab 100644 --- a/core/src/com/unciv/logic/civilization/managers/UnitManager.kt +++ b/core/src/com/unciv/logic/civilization/managers/UnitManager.kt @@ -127,10 +127,10 @@ class UnitManager(val civInfo: Civilization) { } return unit } - @Readonly - fun getCivUnitsSize(): Int = unitList.size - fun getCivUnits(): Sequence = unitList.asSequence() - fun getCivGreatPeople(): Sequence = getCivUnits().filter { mapUnit -> mapUnit.isGreatPerson() } + + @Readonly fun getCivUnitsSize(): Int = unitList.size + @Readonly fun getCivUnits(): Sequence = unitList.asSequence() + @Readonly fun getCivGreatPeople(): Sequence = getCivUnits().filter { mapUnit -> mapUnit.isGreatPerson() } // Similar to getCivUnits(), but the returned list is rotated so that the // 'nextPotentiallyDueAt' unit is first here. diff --git a/core/src/com/unciv/logic/map/MapParameters.kt b/core/src/com/unciv/logic/map/MapParameters.kt index 289de81548..682b6be8b3 100644 --- a/core/src/com/unciv/logic/map/MapParameters.kt +++ b/core/src/com/unciv/logic/map/MapParameters.kt @@ -4,6 +4,7 @@ import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.logic.map.HexMath.getNumberOfTilesInHexagon import com.unciv.logic.map.mapgenerator.MapResourceSetting import com.unciv.models.metadata.BaseRuleset +import yairm210.purity.annotations.Readonly object MapShape { @@ -172,6 +173,7 @@ class MapParameters : IsPartOfGameInfoSerialization { yield(", {Water level}=" + waterThreshold.niceToString(2)) }.joinToString("") + @Readonly fun numberOfTiles() = if (shape == MapShape.hexagonal || shape == MapShape.flatEarth) { 1 + 3 * mapSize.radius * (mapSize.radius - 1) diff --git a/core/src/com/unciv/logic/map/mapunit/MapUnit.kt b/core/src/com/unciv/logic/map/mapunit/MapUnit.kt index f94397e038..59d95d166b 100644 --- a/core/src/com/unciv/logic/map/mapunit/MapUnit.kt +++ b/core/src/com/unciv/logic/map/mapunit/MapUnit.kt @@ -497,7 +497,7 @@ class MapUnit : IsPartOfGameInfoSerialization { // Only military land units can truly "garrison" fun canGarrison() = isMilitary() && baseUnit.isLandUnit - fun isGreatPerson() = baseUnit.isGreatPerson + @Readonly fun isGreatPerson() = baseUnit.isGreatPerson fun isGreatPersonOfType(type: String) = baseUnit.isGreatPersonOfType(type) fun canIntercept(attackedTile: Tile): Boolean { @@ -621,6 +621,7 @@ class MapUnit : IsPartOfGameInfoSerialization { return civ.gameInfo.religions[religion]!!.getReligionDisplayName() } + @Readonly fun getForceEvaluation(): Int { val promotionBonus = (promotions.numberOfPromotions + 1).toFloat().pow(0.3f) var power = (baseUnit.getForceEvaluation() * promotionBonus).toInt() diff --git a/core/src/com/unciv/logic/map/tile/Tile.kt b/core/src/com/unciv/logic/map/tile/Tile.kt index 0603f22ea1..7f72c11fb4 100644 --- a/core/src/com/unciv/logic/map/tile/Tile.kt +++ b/core/src/com/unciv/logic/map/tile/Tile.kt @@ -261,7 +261,6 @@ class Tile : IsPartOfGameInfoSerialization, Json.Serializable { return null } - @Readonly @Suppress("purity") // should be autorecognized as readonly fun getCity(): City? = owningCity @Readonly internal fun getNaturalWonder(): Terrain = @@ -282,7 +281,6 @@ class Tile : IsPartOfGameInfoSerialization, Json.Serializable { return exploredBy.contains(player.civName) } - @Readonly @Suppress("purity") // should be autorecognized as readonly fun isCityCenter(): Boolean = isCityCenterInternal @Readonly fun isNaturalWonder(): Boolean = naturalWonder != null @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 @Readonly @@ -743,7 +741,6 @@ class Tile : IsPartOfGameInfoSerialization, Json.Serializable { return out } - @Readonly @Suppress("purity") // should be auto-recognized as readonly fun getContinent() = continent /** Checks if this tile is marked as target tile for a building with a [UniqueType.CreatesOneImprovement] unique */ diff --git a/core/src/com/unciv/models/ruleset/Policy.kt b/core/src/com/unciv/models/ruleset/Policy.kt index dfab85abb3..17f5a994e0 100644 --- a/core/src/com/unciv/models/ruleset/Policy.kt +++ b/core/src/com/unciv/models/ruleset/Policy.kt @@ -8,6 +8,7 @@ import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.translations.tr import com.unciv.ui.objectdescriptions.uniquesToCivilopediaTextLines import com.unciv.ui.screens.civilopediascreen.FormattedLine +import yairm210.purity.annotations.Pure open class Policy : RulesetObject() { 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. * 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) } diff --git a/core/src/com/unciv/models/ruleset/unique/Unique.kt b/core/src/com/unciv/models/ruleset/unique/Unique.kt index 1d26f94cda..8b16f8d827 100644 --- a/core/src/com/unciv/models/ruleset/unique/Unique.kt +++ b/core/src/com/unciv/models/ruleset/unique/Unique.kt @@ -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 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 - 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 { //According to: https://www.reddit.com/r/civ/comments/gvx44v/comment/fsrifc2/ 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 return modifier } + @Readonly fun hasTriggerConditional(): Boolean { if (modifiers.none()) return false return modifiers.any { conditional -> diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueTarget.kt b/core/src/com/unciv/models/ruleset/unique/UniqueTarget.kt index 207b6ee3a4..3a771ab23e 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueTarget.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueTarget.kt @@ -1,5 +1,7 @@ package com.unciv.models.ruleset.unique +import yairm210.purity.annotations.Readonly + /** * 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 */ // Building.canAcceptUniqueTarget(Global) == true // Global.canAcceptUniqueTarget(Building) == false + @Readonly fun canAcceptUniqueTarget(uniqueTarget: UniqueTarget): Boolean { if (this == uniqueTarget) return true if (inheritsFrom != null) return inheritsFrom.canAcceptUniqueTarget(uniqueTarget) diff --git a/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt b/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt index 0052635b94..c0744e8cfb 100644 --- a/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt +++ b/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt @@ -136,30 +136,35 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction { return unit } + @Readonly override fun hasUnique(uniqueType: UniqueType, state: GameContext?): Boolean { val gameContext = state ?: GameContext.EmptyState return if (::ruleset.isInitialized) rulesetUniqueMap.hasUnique(uniqueType, gameContext) else super.hasUnique(uniqueType, gameContext) } + @Readonly override fun hasUnique(uniqueTag: String, state: GameContext?): Boolean { val gameContext = state ?: GameContext.EmptyState return if (::ruleset.isInitialized) rulesetUniqueMap.hasUnique(uniqueTag, gameContext) else super.hasUnique(uniqueTag, gameContext) } + @Readonly override fun hasTagUnique(tagUnique: String): Boolean { return if (::ruleset.isInitialized) rulesetUniqueMap.hasTagUnique(tagUnique) else super.hasTagUnique(tagUnique) } /** Allows unique functions (getMatchingUniques, hasUnique) to "see" uniques from the UnitType */ + @Readonly override fun getMatchingUniques(uniqueType: UniqueType, state: GameContext): Sequence { return if (::ruleset.isInitialized) rulesetUniqueMap.getMatchingUniques(uniqueType, state) else super.getMatchingUniques(uniqueType, state) } /** Allows unique functions (getMatchingUniques, hasUnique) to "see" uniques from the UnitType */ + @Readonly override fun getMatchingUniques(uniqueTag: String, state: GameContext): Sequence { return if (::ruleset.isInitialized) rulesetUniqueMap.getMatchingUniques(uniqueTag, state) else super.getMatchingUniques(uniqueTag, state) @@ -459,7 +464,7 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction { 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 */ - 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 } @@ -486,11 +491,13 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction { && getMatchingUniques(UniqueType.Strength, GameContext.IgnoreConditionals) .any { it.params[0].toInt() > 0 && it.hasModifier(UniqueType.ConditionalVsCity) } + @Readonly fun getForceEvaluation(): Int { if (cachedForceEvaluation < 0) evaluateForce() return cachedForceEvaluation } + @Readonly @Suppress("purity") // reads from cache private fun evaluateForce() { if (strength == 0 && rangedStrength == 0) { cachedForceEvaluation = 0 diff --git a/core/src/com/unciv/models/translations/Translations.kt b/core/src/com/unciv/models/translations/Translations.kt index e8c622686c..b49cc27e91 100644 --- a/core/src/com/unciv/models/translations/Translations.kt +++ b/core/src/com/unciv/models/translations/Translations.kt @@ -14,6 +14,7 @@ import com.unciv.utils.Log import com.unciv.utils.debug import java.util.Locale import org.jetbrains.annotations.VisibleForTesting +import yairm210.purity.annotations.LocalState import yairm210.purity.annotations.Pure 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]'], * allowing us to have nested translations! */ -@Readonly @Suppress("purity") // Local state update +@Readonly fun String.getPlaceholderParameters(): List { if (!this.contains('[')) return emptyList() val stringToParse = this.removeConditionals() + @LocalState val parameters = ArrayList() var depthOfBraces = 0 var startOfCurrentParameter = -1 diff --git a/core/src/com/unciv/ui/components/extensions/FormattingExtensions.kt b/core/src/com/unciv/ui/components/extensions/FormattingExtensions.kt index 805db40b72..a9d17e991a 100644 --- a/core/src/com/unciv/ui/components/extensions/FormattingExtensions.kt +++ b/core/src/com/unciv/ui/components/extensions/FormattingExtensions.kt @@ -3,6 +3,7 @@ package com.unciv.ui.components.extensions import com.badlogic.gdx.math.Vector2 import com.unciv.models.translations.tr import com.unciv.ui.components.fonts.Fonts +import yairm210.purity.annotations.Pure import java.text.SimpleDateFormat import java.time.Duration import java.time.temporal.ChronoUnit @@ -11,13 +12,13 @@ import java.util.Locale import java.util.SortedMap /** 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 */ -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 */ -fun Float.toPercent() = 1 + this/100 +@Pure fun Float.toPercent() = 1 + this/100 /** Convert a [resource name][this] into "Consumes [amount] $resource" string (untranslated) */ fun String.getConsumesAmountString(amount: Int, isStockpiled: Boolean): String {