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
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:

View File

@ -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

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.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

View File

@ -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<Tile> = 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<Tile> = 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<String>) -> 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

View File

@ -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<Building> = builtBuildingObjects.asSequence()
@Readonly fun getBuiltBuildings(): Sequence<Building> = builtBuildingObjects.asSequence()
fun containsBuildingOrEquivalent(buildingNameOrUnique: String): Boolean =
isBuilt(buildingNameOrUnique) || getBuiltBuildings().any { it.replaces == buildingNameOrUnique || it.hasUnique(buildingNameOrUnique, city.state) }

View File

@ -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<String,Double> {
val scoreBreakdown = hashMapOf<String,Double>()
// 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 }

View File

@ -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<Civilization> {
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<String, Int> {
@LocalState
val modifiers = LinkedHashMap<String, Int>() // Linked to preserve order when presenting the modifiers table
// Can't bully major civs or unsettled CS's
if (!civInfo.isCityState) {

View File

@ -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)

View File

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

View File

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

View File

@ -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()

View File

@ -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 */

View File

@ -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)
}

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 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 ->

View File

@ -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)

View File

@ -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<RulesetObject>.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<RulesetObject>.hasUnique(uniqueTag, gameContext)
}
@Readonly
override fun hasTagUnique(tagUnique: String): Boolean {
return if (::ruleset.isInitialized) rulesetUniqueMap.hasTagUnique(tagUnique)
else super<RulesetObject>.hasTagUnique(tagUnique)
}
/** Allows unique functions (getMatchingUniques, hasUnique) to "see" uniques from the UnitType */
@Readonly
override fun getMatchingUniques(uniqueType: UniqueType, state: GameContext): Sequence<Unique> {
return if (::ruleset.isInitialized) rulesetUniqueMap.getMatchingUniques(uniqueType, state)
else super<RulesetObject>.getMatchingUniques(uniqueType, state)
}
/** Allows unique functions (getMatchingUniques, hasUnique) to "see" uniques from the UnitType */
@Readonly
override fun getMatchingUniques(uniqueTag: String, state: GameContext): Sequence<Unique> {
return if (::ruleset.isInitialized) rulesetUniqueMap.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 }
/** 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

View File

@ -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<String> {
if (!this.contains('[')) return emptyList()
val stringToParse = this.removeConditionals()
@LocalState
val parameters = ArrayList<String>()
var depthOfBraces = 0
var startOfCurrentParameter = -1

View File

@ -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 {