From 8e5b36984c0d7254b407bfede9da11cf84a71836 Mon Sep 17 00:00:00 2001 From: yairm210 Date: Fri, 15 Aug 2025 13:25:05 +0300 Subject: [PATCH] chore(purity) --- build.gradle.kts | 1 + .../unciv/logic/automation/unit/AirUnitAutomation.kt | 6 +++++- .../logic/automation/unit/CivilianUnitAutomation.kt | 4 ++++ .../unit/HeadTowardsEnemyCityAutomation.kt | 3 +++ .../unciv/logic/automation/unit/UnitAutomation.kt | 12 ++++++++++-- core/src/com/unciv/logic/battle/Battle.kt | 4 +++- core/src/com/unciv/logic/battle/Nuke.kt | 2 ++ .../logic/civilization/managers/ThreatManager.kt | 1 + .../LuxuryResourcePlacementLogic.kt | 3 +++ .../unciv/logic/map/mapunit/UnitUpgradeManager.kt | 2 ++ core/src/com/unciv/models/ruleset/unit/BaseUnit.kt | 2 ++ core/src/com/unciv/utils/CollectionExtensions.kt | 1 + 12 files changed, 37 insertions(+), 4 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 94a0d9c660..c99b1a2fe6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -67,6 +67,7 @@ allprojects { "kotlin.collections.random", "kotlin.hashCode", + "kotlin.collections.shuffled", ) wellKnownPureClasses = setOf( "java.lang.StackTraceElement" // moved diff --git a/core/src/com/unciv/logic/automation/unit/AirUnitAutomation.kt b/core/src/com/unciv/logic/automation/unit/AirUnitAutomation.kt index f1e167af0b..b4f1fb0a8c 100644 --- a/core/src/com/unciv/logic/automation/unit/AirUnitAutomation.kt +++ b/core/src/com/unciv/logic/automation/unit/AirUnitAutomation.kt @@ -9,6 +9,8 @@ import com.unciv.logic.civilization.Civilization import com.unciv.logic.map.mapunit.MapUnit import com.unciv.logic.map.tile.Tile import com.unciv.models.ruleset.unique.UniqueType +import yairm210.purity.annotations.Pure +import yairm210.purity.annotations.Readonly object AirUnitAutomation { @@ -29,7 +31,7 @@ object AirUnitAutomation { if (friendlyUnusedFighterCount < enemyFighters) return if (friendlyUsedFighterCount <= enemyFighters) { - fun airSweepDamagePercentBonus(): Int { + @Readonly fun airSweepDamagePercentBonus(): Int { return unit.getMatchingUniques(UniqueType.StrengthWhenAirsweep) .sumOf { it.params[0].toInt() } } @@ -149,6 +151,7 @@ object AirUnitAutomation { * Ranks the tile to nuke based off of all tiles in it's blast radius * By default the value is -500 to prevent inefficient nuking. */ + @Readonly private fun getNukeLocationValue(nuke: MapUnit, tile: Tile): Int { val civ = nuke.civ if (!Nuke.mayUseNuke(MapUnitCombatant(nuke), tile)) return Int.MIN_VALUE @@ -166,6 +169,7 @@ object AirUnitAutomation { var explosionValue = -500 // Returns either ourValue or thierValue depending on if the input Civ matches the Nuke's Civ + @Pure fun evaluateCivValue(targetCiv: Civilization, ourValue: Int, theirValue: Int): Int { if (targetCiv == civ) // We are nuking something that we own! return ourValue diff --git a/core/src/com/unciv/logic/automation/unit/CivilianUnitAutomation.kt b/core/src/com/unciv/logic/automation/unit/CivilianUnitAutomation.kt index 3218cb0601..828457e48e 100644 --- a/core/src/com/unciv/logic/automation/unit/CivilianUnitAutomation.kt +++ b/core/src/com/unciv/logic/automation/unit/CivilianUnitAutomation.kt @@ -11,9 +11,11 @@ import com.unciv.models.ruleset.unique.UniqueType import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionModifiers import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionModifiers.canUse import com.unciv.ui.screens.worldscreen.unit.actions.UnitActions +import yairm210.purity.annotations.Readonly object CivilianUnitAutomation { + @Readonly fun shouldClearTileForAddInCapitalUnits(unit: MapUnit, tile: Tile) = tile.isCityCenter() && tile.getCity()!!.isCapital() && !unit.hasUnique(UniqueType.AddInCapital) @@ -23,6 +25,7 @@ object CivilianUnitAutomation { // To allow "found city" actions that can only trigger a limited number of times // Slightly modified getUsableUnitActionUniques() to allow for settlers with *conditional* settling uniques + @Readonly fun hasSettlerAction(uniqueType: UniqueType) = unit.getMatchingUniques(uniqueType, GameContext.IgnoreConditionals) .filter { unique -> !unique.hasModifier(UniqueType.UnitActionExtraLimitedTimes) } @@ -172,6 +175,7 @@ object CivilianUnitAutomation { return // The AI doesn't know how to handle unknown civilian units } + @Readonly private fun isLateGame(civ: Civilization): Boolean { val researchCompletePercent = (civ.tech.researchedTechnologies.size * 1.0f) / civ.gameInfo.ruleset.technologies.size diff --git a/core/src/com/unciv/logic/automation/unit/HeadTowardsEnemyCityAutomation.kt b/core/src/com/unciv/logic/automation/unit/HeadTowardsEnemyCityAutomation.kt index 7058df2070..46b813b642 100644 --- a/core/src/com/unciv/logic/automation/unit/HeadTowardsEnemyCityAutomation.kt +++ b/core/src/com/unciv/logic/automation/unit/HeadTowardsEnemyCityAutomation.kt @@ -8,6 +8,7 @@ import com.unciv.logic.city.City import com.unciv.logic.map.mapunit.MapUnit import com.unciv.logic.map.mapunit.movement.PathsToTilesWithinTurn import com.unciv.logic.map.tile.Tile +import yairm210.purity.annotations.Readonly object HeadTowardsEnemyCityAutomation { @@ -28,6 +29,7 @@ object HeadTowardsEnemyCityAutomation { ) } + @Readonly fun getEnemyCitiesByPriority(unit: MapUnit): Sequence { val enemies = unit.civ.getKnownCivs() .filter { unit.civ.isAtWarWith(it) && it.cities.isNotEmpty() } @@ -94,6 +96,7 @@ object HeadTowardsEnemyCityAutomation { } /** Cannot take within 5 turns */ + @Readonly private fun cannotTakeCitySoon( ourUnitsAroundEnemyCity: Sequence, city: City diff --git a/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt b/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt index 7e85dc1e68..47530e0b0d 100644 --- a/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt +++ b/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt @@ -24,12 +24,14 @@ import com.unciv.models.ruleset.unit.BaseUnit import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsPillage import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsUpgrade import com.unciv.utils.randomWeighted +import yairm210.purity.annotations.Readonly object UnitAutomation { private const val CLOSE_ENEMY_TILES_AWAY_LIMIT = 5 private const val CLOSE_ENEMY_TURNS_AWAY_LIMIT = 3f + @Readonly private fun isGoodTileToExplore(unit: MapUnit, tile: Tile): Boolean { return (tile.getOwner() == null || !tile.getOwner()!!.isCityState) && tile.getTilesInDistance(unit.getVisibilityRange()).any { !unit.civ.hasExplored(it) } @@ -103,6 +105,7 @@ object UnitAutomation { return false } + @Readonly private fun isGoodTileForFogBusting(unit: MapUnit, tile: Tile): Boolean { return unit.movement.canMoveTo(tile) && tile.getOwner() == null @@ -153,8 +156,10 @@ object UnitAutomation { /** Get the base unit this map unit could upgrade to, respecting researched tech and nation uniques only. * Note that if the unit can't upgrade, the current BaseUnit is returned. */ + @Readonly private fun getUnitsToUpgradeTo(unit: MapUnit): Sequence { + @Readonly fun isInvalidUpgradeDestination(baseUnit: BaseUnit): Boolean { if (!unit.civ.tech.isResearched(baseUnit)) return true @@ -423,6 +428,7 @@ object UnitAutomation { /** * @return true if the tile is safe and the unit can heal to full within [turns] */ + @Readonly private fun canUnitHealInTurnsOnCurrentTile(unit: MapUnit, turns: Int, noEnemyDistance: Int = 3): Boolean { if (unit.hasUnique(UniqueType.HealsEvenAfterAction)) return false // We can keep on moving // Check if we are not in a safe city and there is an enemy nearby this isn't a good tile to heal on @@ -433,6 +439,7 @@ object UnitAutomation { return healthRequiredPerTurn <= unit.rankTileForHealing(unit.getTile()) } + @Readonly private fun getDangerousTiles(unit: MapUnit): HashSet { val nearbyEnemyUnits = unit.currentTile.getTilesInDistance(3) .flatMap { tile -> tile.getUnits().filter { unit.civ.isAtWarWith(it.civ) } } @@ -517,6 +524,7 @@ object UnitAutomation { private fun tryPrepare(unit: MapUnit): Boolean { val civInfo = unit.civ + @Readonly fun hasPreparationFlag(targetCiv: Civilization): Boolean { val diploManager = civInfo.getDiplomacyManager(targetCiv)!! if (diploManager.hasFlag(DiplomacyFlags.Denunciation) @@ -685,7 +693,7 @@ object UnitAutomation { unit.movement.headTowards(closestReachableCityNeedsDefending.getCenterTile()) return true } - + @Readonly private fun isCityThatNeedsDefendingInWartime(city: City): Boolean { if (city.health < city.getMaxHealth()) return true // this city is under attack! for (enemyCivCity in city.civ.diplomacy.values @@ -696,7 +704,7 @@ object UnitAutomation { } private fun tryStationingMeleeNavalUnit(unit: MapUnit): Boolean { - fun isMeleeNaval(mapUnit: MapUnit) = mapUnit.baseUnit.isMelee() && mapUnit.type.isWaterUnit() + @Readonly fun isMeleeNaval(mapUnit: MapUnit) = mapUnit.baseUnit.isMelee() && mapUnit.type.isWaterUnit() if (!isMeleeNaval(unit)) return false val closeCity = unit.getTile().getTilesInDistance(3) diff --git a/core/src/com/unciv/logic/battle/Battle.kt b/core/src/com/unciv/logic/battle/Battle.kt index 0160d663d0..32b9146e01 100644 --- a/core/src/com/unciv/logic/battle/Battle.kt +++ b/core/src/com/unciv/logic/battle/Battle.kt @@ -25,6 +25,7 @@ import com.unciv.models.stats.SubStat import com.unciv.ui.components.UnitMovementMemoryType import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsPillage import com.unciv.utils.debug +import yairm210.purity.annotations.Readonly import kotlin.math.max import kotlin.math.min import kotlin.math.roundToInt @@ -678,7 +679,8 @@ object Battle { city.isBeingRazed = true } } - + + @Readonly fun getMapCombatantOfTile(tile: Tile): ICombatant? { if (tile.isCityCenter()) return CityCombatant(tile.getCity()!!) if (tile.militaryUnit != null) return MapUnitCombatant(tile.militaryUnit!!) diff --git a/core/src/com/unciv/logic/battle/Nuke.kt b/core/src/com/unciv/logic/battle/Nuke.kt index 45a97ef493..0d76f51bc4 100644 --- a/core/src/com/unciv/logic/battle/Nuke.kt +++ b/core/src/com/unciv/logic/battle/Nuke.kt @@ -15,6 +15,7 @@ import com.unciv.logic.map.tile.Tile import com.unciv.models.ruleset.unique.UniqueType import com.unciv.ui.components.extensions.toPercent import com.unciv.ui.screens.worldscreen.bottombar.BattleTable +import yairm210.purity.annotations.Readonly import kotlin.math.ulp import kotlin.random.Random @@ -28,6 +29,7 @@ object Nuke { * * Both [BattleTable.simulateNuke] and [AirUnitAutomation.automateNukes] check range, so that check is omitted here. */ + @Readonly fun mayUseNuke(nuke: MapUnitCombatant, targetTile: Tile): Boolean { val attackerCiv = nuke.getCivInfo() val launchTile = nuke.getTile() diff --git a/core/src/com/unciv/logic/civilization/managers/ThreatManager.kt b/core/src/com/unciv/logic/civilization/managers/ThreatManager.kt index 68d27ccf6e..5ad922de87 100644 --- a/core/src/com/unciv/logic/civilization/managers/ThreatManager.kt +++ b/core/src/com/unciv/logic/civilization/managers/ThreatManager.kt @@ -30,6 +30,7 @@ class ThreatManager(val civInfo: Civilization) { * The result value is cached and since it is called each turn in NextTurnAutomation.getUnitPriority * each subsequent calls are likely to be free. */ + @Readonly @Suppress("purity") //tilesWithEnemies is implicit cache fun getDistanceToClosestEnemyUnit(tile: Tile, maxDist: Int, takeLargerValues: Boolean = true): Int { val tileData = distanceToClosestEnemyTiles[tile] // Needs to be a high value, but not the max value so we can still add to it. Example: nextTurnAutomation sorting diff --git a/core/src/com/unciv/logic/map/mapgenerator/resourceplacement/LuxuryResourcePlacementLogic.kt b/core/src/com/unciv/logic/map/mapgenerator/resourceplacement/LuxuryResourcePlacementLogic.kt index 7098de0709..e4c47adacd 100644 --- a/core/src/com/unciv/logic/map/mapgenerator/resourceplacement/LuxuryResourcePlacementLogic.kt +++ b/core/src/com/unciv/logic/map/mapgenerator/resourceplacement/LuxuryResourcePlacementLogic.kt @@ -12,6 +12,7 @@ import com.unciv.models.ruleset.tile.TileResource import com.unciv.models.ruleset.unique.GameContext import com.unciv.models.ruleset.unique.UniqueType import com.unciv.utils.randomWeighted +import yairm210.purity.annotations.Readonly import kotlin.math.abs import kotlin.math.max import kotlin.math.min @@ -80,6 +81,7 @@ object LuxuryResourcePlacementLogic { return Pair(cityStateLuxuries, randomLuxuries) } + @Readonly private fun getLuxuriesForRandomPlacement( assignableLuxuries: List, amountRegionsWithLuxury: HashMap, @@ -97,6 +99,7 @@ object LuxuryResourcePlacementLogic { return remainingLuxuries.drop(targetDisabledLuxuries) } + @Readonly private fun getCandidateLuxuries( assignableLuxuries: List, amountRegionsWithLuxury: HashMap, diff --git a/core/src/com/unciv/logic/map/mapunit/UnitUpgradeManager.kt b/core/src/com/unciv/logic/map/mapunit/UnitUpgradeManager.kt index 0d0f8a9654..9eec8249a6 100644 --- a/core/src/com/unciv/logic/map/mapunit/UnitUpgradeManager.kt +++ b/core/src/com/unciv/logic/map/mapunit/UnitUpgradeManager.kt @@ -5,6 +5,7 @@ import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unit.BaseUnit import com.unciv.ui.components.extensions.toPercent import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsUpgrade +import yairm210.purity.annotations.Readonly import kotlin.math.pow class UnitUpgradeManager(val unit: MapUnit) { @@ -16,6 +17,7 @@ class UnitUpgradeManager(val unit: MapUnit) { * @param ignoreResources Ignore resource requirements (tech still counts) * Used to display disabled Upgrade button */ + @Readonly fun canUpgrade( unitToUpgradeTo: BaseUnit, ignoreRequirements: Boolean = false, diff --git a/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt b/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt index f07feee360..9acf1f6814 100644 --- a/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt +++ b/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt @@ -108,6 +108,7 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction { super.isUnavailableBySettings(gameInfo) || (!gameInfo.gameParameters.nuclearWeaponsEnabled && isNuclearWeapon()) + @Readonly fun getUpgradeUnits(gameContext: GameContext = GameContext.EmptyState): Sequence { return sequence { yieldIfNotNull(upgradesTo) @@ -116,6 +117,7 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction { } } + @Readonly fun getRulesetUpgradeUnits(gameContext: GameContext = GameContext.EmptyState): Sequence { return sequence { for (unit in getUpgradeUnits(gameContext)) diff --git a/core/src/com/unciv/utils/CollectionExtensions.kt b/core/src/com/unciv/utils/CollectionExtensions.kt index fb8eb7a419..648b38aaed 100644 --- a/core/src/com/unciv/utils/CollectionExtensions.kt +++ b/core/src/com/unciv/utils/CollectionExtensions.kt @@ -83,6 +83,7 @@ fun Sequence.toGdxArray(): Array { } /** [yield][SequenceScope.yield]s [element] if it's not null */ +@Pure suspend fun SequenceScope.yieldIfNotNull(element: T?) { if (element != null) yield(element) }