From 5c09f1b743f628c01143edcd7b05147a611c3df2 Mon Sep 17 00:00:00 2001 From: yairm210 Date: Wed, 6 Aug 2025 17:46:47 +0300 Subject: [PATCH] chore(purity): Espionage --- build.gradle.kts | 1 + .../city/managers/CityEspionageManager.kt | 11 ++++---- .../city/managers/CityPopulationManager.kt | 1 + .../unciv/logic/civilization/Civilization.kt | 2 +- .../civilization/managers/EspionageManager.kt | 20 +++++++-------- core/src/com/unciv/models/Spy.kt | 25 ++++++++++++------- 6 files changed, 34 insertions(+), 26 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 4c2c44ee6f..fbb7fb5716 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -61,6 +61,7 @@ allprojects { "java.util.BitSet.get", // moved "kotlin.collections.getValue", // moved + "kotlin.collections.randomOrNull", ) wellKnownPureClasses = setOf( ) diff --git a/core/src/com/unciv/logic/city/managers/CityEspionageManager.kt b/core/src/com/unciv/logic/city/managers/CityEspionageManager.kt index 0bb012d9a1..2a77543123 100644 --- a/core/src/com/unciv/logic/city/managers/CityEspionageManager.kt +++ b/core/src/com/unciv/logic/city/managers/CityEspionageManager.kt @@ -4,6 +4,7 @@ import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.logic.city.City import com.unciv.logic.civilization.Civilization import com.unciv.models.Spy +import yairm210.purity.annotations.Readonly enum class SpyFleeReason { CityDestroyed, @@ -25,13 +26,11 @@ class CityEspionageManager : IsPartOfGameInfoSerialization { this.city = city } - fun hasSpyOf(civInfo: Civilization): Boolean { - return civInfo.espionageManager.spyList.any { it.getCityOrNull() == city } - } + + @Readonly fun hasSpyOf(civInfo: Civilization): Boolean = civInfo.espionageManager.spyList.any { it.getCityOrNull() == city } - fun getAllStationedSpies(): List { - return city.civ.gameInfo.civilizations.flatMap { it.espionageManager.getSpiesInCity(city) } - } + @Readonly fun getAllStationedSpies(): List = + city.civ.gameInfo.civilizations.flatMap { it.espionageManager.getSpiesInCity(city) } fun removeAllPresentSpies(reason: SpyFleeReason) { for (spy in getAllStationedSpies()) { diff --git a/core/src/com/unciv/logic/city/managers/CityPopulationManager.kt b/core/src/com/unciv/logic/city/managers/CityPopulationManager.kt index 65f1aacb3d..946394e451 100644 --- a/core/src/com/unciv/logic/city/managers/CityPopulationManager.kt +++ b/core/src/com/unciv/logic/city/managers/CityPopulationManager.kt @@ -71,6 +71,7 @@ class CityPopulationManager : IsPartOfGameInfoSerialization { /** Take null to mean infinity. */ + @Readonly fun getNumTurnsToNewPopulation(): Int? { if (!city.isGrowing()) return null val roundedFoodPerTurn = city.foodForNextTurn().toFloat() diff --git a/core/src/com/unciv/logic/civilization/Civilization.kt b/core/src/com/unciv/logic/civilization/Civilization.kt index 9d684deba5..0f1866cd63 100644 --- a/core/src/com/unciv/logic/civilization/Civilization.kt +++ b/core/src/com/unciv/logic/civilization/Civilization.kt @@ -1039,7 +1039,7 @@ class Civilization : IsPartOfGameInfoSerialization { moveCapitalTo(newCapital, oldCapital) } - fun getAllyCiv(): Civilization? = if (allyCivName == null) null + @Readonly fun getAllyCiv(): Civilization? = if (allyCivName == null) null else gameInfo.getCivilization(allyCivName!!) @Readonly fun getAllyCivName() = allyCivName fun setAllyCiv(newAllyName: String?) { allyCivName = newAllyName } diff --git a/core/src/com/unciv/logic/civilization/managers/EspionageManager.kt b/core/src/com/unciv/logic/civilization/managers/EspionageManager.kt index 52f619230f..b91934a7b4 100644 --- a/core/src/com/unciv/logic/civilization/managers/EspionageManager.kt +++ b/core/src/com/unciv/logic/civilization/managers/EspionageManager.kt @@ -6,6 +6,7 @@ import com.unciv.logic.civilization.Civilization import com.unciv.logic.map.tile.Tile import com.unciv.models.Spy import com.unciv.models.ruleset.unique.UniqueType +import yairm210.purity.annotations.Readonly class EspionageManager : IsPartOfGameInfoSerialization { @@ -42,6 +43,7 @@ class EspionageManager : IsPartOfGameInfoSerialization { spy.endTurn() } + @Readonly fun getSpyName(): String { val usedSpyNames = spyList.map { it.name }.toHashSet() val validSpyNames = civInfo.nation.spyNames.filter { it !in usedSpyNames } @@ -57,6 +59,7 @@ class EspionageManager : IsPartOfGameInfoSerialization { return newSpy } + @Readonly fun getTilesVisibleViaSpies(): Sequence { return spyList.asSequence() .filter { it.isSetUp() } @@ -64,6 +67,7 @@ class EspionageManager : IsPartOfGameInfoSerialization { .flatMap { it.getCenterTile().getTilesInDistance(1) } } + @Readonly fun getTechsToSteal(otherCiv: Civilization): Set { val techsToSteal = mutableSetOf() for (tech in otherCiv.tech.techsResearched) { @@ -74,30 +78,26 @@ class EspionageManager : IsPartOfGameInfoSerialization { return techsToSteal } - fun getSpiesInCity(city: City): List { - return spyList.filterTo(mutableListOf()) { it.getCityOrNull() == city } - } + @Readonly fun getSpiesInCity(city: City): List = spyList.filter { it.getCityOrNull() == city } - fun getStartingSpyRank(): Int = 1 + civInfo.getMatchingUniques(UniqueType.SpyStartingLevel).sumOf { it.params[0].toInt() } + @Readonly fun getStartingSpyRank(): Int = 1 + civInfo.getMatchingUniques(UniqueType.SpyStartingLevel).sumOf { it.params[0].toInt() } /** * Returns a list of all cities with our spies in them. * The list needs to be stable across calls on the same turn. */ - fun getCitiesWithOurSpies(): List = spyList.filter { it.isSetUp() }.mapNotNull { it.getCityOrNull() } + @Readonly fun getCitiesWithOurSpies(): List = spyList.filter { it.isSetUp() }.mapNotNull { it.getCityOrNull() } - fun getSpyAssignedToCity(city: City): Spy? = spyList.firstOrNull { it.getCityOrNull() == city } + @Readonly fun getSpyAssignedToCity(city: City): Spy? = spyList.firstOrNull { it.getCityOrNull() == city } /** * Determines whether the NextTurnAction MoveSpies should be shown or not * @return true if there are spies waiting to be moved */ - fun shouldShowMoveSpies(): Boolean = !dismissedShouldMoveSpies && spyList.any { it.isIdle() } + @Readonly fun shouldShowMoveSpies(): Boolean = !dismissedShouldMoveSpies && spyList.any { it.isIdle() } && civInfo.gameInfo.getCities().any { civInfo.hasExplored(it.getCenterTile()) && getSpyAssignedToCity(it) == null } - fun getIdleSpies(): List { - return spyList.filterTo(mutableListOf()) { it.isIdle() } - } + @Readonly fun getIdleSpies(): List = spyList.filter { it.isIdle() } /** * Takes all spies away from their cities. diff --git a/core/src/com/unciv/models/Spy.kt b/core/src/com/unciv/models/Spy.kt index 69a51922e2..3242078d9d 100644 --- a/core/src/com/unciv/models/Spy.kt +++ b/core/src/com/unciv/models/Spy.kt @@ -14,6 +14,7 @@ import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers import com.unciv.logic.civilization.managers.EspionageManager import com.unciv.models.ruleset.unique.Unique import com.unciv.models.ruleset.unique.UniqueType +import yairm210.purity.annotations.Readonly import kotlin.math.ceil import kotlin.random.Random @@ -166,6 +167,7 @@ class Spy private constructor() : IsPartOfGameInfoSerialization { * A -1 means we have no techonologies to steal. * A -2 means we the city produces no science */ + @Readonly @Suppress("purity") // something here is wrong, it's actually mutating?! private fun getTurnsRemainingToStealTech(): Int { val stealableTechs = espionageManager.getTechsToSteal(getCity().civ) if (stealableTechs.isEmpty()) return -1 @@ -314,6 +316,7 @@ class Spy private constructor() : IsPartOfGameInfoSerialization { /** * Calculates the success chance of a coup in this city state. */ + @Readonly fun getCoupChanceOfSuccess(includeunknownFactors: Boolean): Float { val cityState = getCity().civ var successPercentage = 50f @@ -348,7 +351,8 @@ class Spy private constructor() : IsPartOfGameInfoSerialization { this.city = city setAction(SpyAction.Moving, 1) } - + + @Readonly private fun canDismissAgreementToNotSendSpies(city: City): Boolean { val otherCivDiplomacyManager = city.civ.getDiplomacyManager(civInfo) ?: return false val otherCiv = otherCivDiplomacyManager.civInfo @@ -370,6 +374,7 @@ class Spy private constructor() : IsPartOfGameInfoSerialization { return false } + @Readonly fun canMoveTo(city: City): Boolean { if (canDismissAgreementToNotSendSpies(city)) return false if (getCityOrNull() == city) return true @@ -377,13 +382,14 @@ class Spy private constructor() : IsPartOfGameInfoSerialization { return espionageManager.getSpyAssignedToCity(city) == null } - fun isSetUp() = action.isSetUp + @Readonly fun isSetUp() = action.isSetUp - fun isIdle() = action == SpyAction.None + @Readonly fun isIdle() = action == SpyAction.None - fun isDoingWork() = action.isDoingWork(this) + @Readonly fun isDoingWork() = action.isDoingWork(this) /** Returns the City this Spy is in, or `null` if it is in the hideout. */ + @Readonly @Suppress("purity") // this also appears to be NOT readonly. Something is fishy. fun getCityOrNull(): City? { if (location == null) return null if (city == null) city = civInfo.gameInfo.tileMap[location!!].getCity() @@ -392,9 +398,9 @@ class Spy private constructor() : IsPartOfGameInfoSerialization { /** Non-null version of [getCityOrNull] for the frequent case it is known the spy cannot be in the hideout. * @throws NullPointerException if the spy is in the hideout */ - fun getCity(): City = getCityOrNull()!! + @Readonly fun getCity(): City = getCityOrNull()!! - fun getLocationName() = getCityOrNull()?.name ?: Constants.spyHideout + @Readonly fun getLocationName() = getCityOrNull()?.name ?: Constants.spyHideout fun levelUpSpy(amount: Int = 1) { if (rank >= civInfo.gameInfo.ruleset.modOptions.constants.maxSpyRank) return @@ -406,12 +412,13 @@ class Spy private constructor() : IsPartOfGameInfoSerialization { } /** Modifier of the skill bonus of the spy by percent */ - fun getSkillModifierPercent() = rank * civInfo.gameInfo.ruleset.modOptions.constants.spyRankSkillPercentBonus + @Readonly fun getSkillModifierPercent() = rank * civInfo.gameInfo.ruleset.modOptions.constants.spyRankSkillPercentBonus /** * Gets a friendly and enemy efficiency uniques for the spy at the location * @return a value centered around 1.0 for the work efficiency of the spy, won't be negative */ + @Readonly fun getEfficiencyModifier(): Double { val friendlyUniques: Sequence val enemyUniques: Sequence @@ -446,7 +453,7 @@ class Spy private constructor() : IsPartOfGameInfoSerialization { rank = 1 } - fun isAlive(): Boolean = action != SpyAction.Dead + @Readonly fun isAlive(): Boolean = action != SpyAction.Dead /** Shorthand for [Civilization.addNotification] specialized for espionage - action, category and icon are always the same */ fun addNotification(text: String) = @@ -454,5 +461,5 @@ class Spy private constructor() : IsPartOfGameInfoSerialization { /** Anti-save-scum: Deterministic random from city and turn * @throws NullPointerException for spies in the hideout */ - private fun randomSeed() = (getCity().run { location.x * location.y } + 123f * civInfo.gameInfo.turns).toInt() + name.hashCode() + @Readonly private fun randomSeed() = (getCity().run { location.x * location.y } + 123f * civInfo.gameInfo.turns).toInt() + name.hashCode() }