diff --git a/build.gradle.kts b/build.gradle.kts index 7b8ef4abba..655f11688e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -54,6 +54,9 @@ allprojects { "kotlin.apply", "kotlin.takeIf", "kotlin.takeUnless", + "kotlin.ranges.coerceIn", + "com.unciv.logic.civilization.diplomacy.RelationshipLevel.compareTo", +// "kotlin.Enum.compareTo", // so overrides are considered pure as well :think: ) wellKnownReadonlyFunctions = setOf( "kotlin.collections.any", @@ -62,6 +65,11 @@ allprojects { "kotlin.ranges.coerceAtLeast", // Looks like the Collection.contains is not considered overridden :thunk: "java.util.AbstractCollection.contains", + "kotlin.collections.sum", + ) + wellKnownPureClasses = setOf( + "kotlin.enums.EnumEntries", + "kotlin.Enum", ) } diff --git a/core/src/com/unciv/logic/GameInfo.kt b/core/src/com/unciv/logic/GameInfo.kt index 8406f5d748..b7dfc750a9 100644 --- a/core/src/com/unciv/logic/GameInfo.kt +++ b/core/src/com/unciv/logic/GameInfo.kt @@ -47,6 +47,7 @@ import com.unciv.ui.screens.savescreens.Gzip import com.unciv.ui.screens.worldscreen.status.NextTurnProgress import com.unciv.utils.DebugUtils import com.unciv.utils.debug +import yairm210.purity.annotations.Readonly import java.security.MessageDigest import java.util.UUID @@ -231,6 +232,7 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion val civMap by lazy { civilizations.associateBy { it.civName } } /** Get a civ by name * @throws NoSuchElementException if no civ of that name is in the game (alive or dead)! */ + @Readonly fun getCivilization(civName: String) = civMap[civName] ?: civilizations.first { it.civName == civName } // This is for spectators who are added in later, artificially fun getCurrentPlayerCivilization() = currentPlayerCiv @@ -241,6 +243,7 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion 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!! fun getGlobalUniques() = combinedGlobalUniques /** @return Sequence of all cities in game, both major civilizations and city states */ diff --git a/core/src/com/unciv/logic/city/City.kt b/core/src/com/unciv/logic/city/City.kt index 392639dd1a..1ae0036a8b 100644 --- a/core/src/com/unciv/logic/city/City.kt +++ b/core/src/com/unciv/logic/city/City.kt @@ -545,6 +545,7 @@ class City : IsPartOfGameInfoSerialization, INamed { } // Uniques coming from this city, but that should be provided globally + @Readonly fun getMatchingUniquesWithNonLocalEffects(uniqueType: UniqueType, gameContext: GameContext = state): Sequence { val uniques = cityConstructions.builtBuildingUniqueMap.getUniques(uniqueType) // Memory performance showed that this function was very memory intensive, thus we only create the filter if needed diff --git a/core/src/com/unciv/logic/civilization/Civilization.kt b/core/src/com/unciv/logic/civilization/Civilization.kt index 4943c4534a..4283907664 100644 --- a/core/src/com/unciv/logic/civilization/Civilization.kt +++ b/core/src/com/unciv/logic/civilization/Civilization.kt @@ -339,18 +339,23 @@ class Civilization : IsPartOfGameInfoSerialization { * city-states to contain the barbarians. Therefore, [getKnownCivs] will **not** list the barbarians * for major civs, but **will** do so for city-states after some gameplay. */ + @Readonly fun getKnownCivs() = diplomacy.values.asSequence().map { it.otherCiv() } .filter { !it.isDefeated() && !it.isSpectator() } fun getKnownCivsWithSpectators() = diplomacy.values.asSequence().map { it.otherCiv() } .filter { !it.isDefeated() } + @Readonly fun knows(otherCivName: String) = diplomacy.containsKey(otherCivName) + @Readonly fun knows(otherCiv: Civilization) = knows(otherCiv.civName) fun getCapital(firstCityIfNoCapital: Boolean = true) = cities.firstOrNull { it.isCapital() } ?: if (firstCityIfNoCapital) cities.firstOrNull() else null + @Readonly fun isHuman() = playerType == PlayerType.Human + @Readonly fun isAI() = playerType == PlayerType.AI fun isAIOrAutoPlaying(): Boolean { if (playerType == PlayerType.AI) return true @@ -370,7 +375,9 @@ class Civilization : IsPartOfGameInfoSerialization { @delegate:Transient val isBarbarian by lazy { nation.isBarbarian } + @Readonly fun isSpectator() = nation.isSpectator + @Readonly fun isAlive(): Boolean = !isDefeated() @delegate:Transient @@ -524,11 +531,13 @@ class Civilization : IsPartOfGameInfoSerialization { fun hasResource(resourceName: String): Boolean = getResourceAmount(resourceName) > 0 + @Readonly fun hasUnique(uniqueType: UniqueType, gameContext: GameContext = state) = getMatchingUniques(uniqueType, gameContext).any() // Does not return local uniques, only global ones. /** Destined to replace getMatchingUniques, gradually, as we fill the enum */ + @Readonly fun getMatchingUniques( uniqueType: UniqueType, gameContext: GameContext = state @@ -653,16 +662,20 @@ class Civilization : IsPartOfGameInfoSerialization { * If the civ has never controlled an original capital, it stays 'alive' as long as it has units (irrespective of non-original-capitals owned) * Otherwise, it stays 'alive' as long as it has cities (irrespective of settlers owned) */ + @Readonly fun isDefeated() = when { isBarbarian || isSpectator() -> false // Barbarians and voyeurs can't lose hasEverOwnedOriginalCapital -> cities.isEmpty() else -> units.getCivUnitsSize() == 0 } + @Readonly fun getEra(): Era = tech.era + @Readonly fun getEraNumber(): Int = getEra().eraNumber + @Readonly fun isAtWarWith(otherCiv: Civilization) = diplomacyFunctions.isAtWarWith(otherCiv) fun isAtWar() = diplomacy.values.any { it.diplomaticStatus == DiplomaticStatus.War && !it.otherCiv().isDefeated() } @@ -1039,6 +1052,7 @@ class Civilization : IsPartOfGameInfoSerialization { fun getAllyCiv(): Civilization? = if (allyCivName == null) null else gameInfo.getCivilization(allyCivName!!) + @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 108643a3d5..44a4253ecf 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.Readonly import kotlin.math.min import kotlin.math.pow import kotlin.random.Random @@ -408,10 +409,12 @@ class CityStateFunctions(val civInfo: Civilization) { civInfo.destroy(notificationLocation) } + @Readonly fun getTributeWillingness(demandingCiv: Civilization, demandingWorker: Boolean = false): Int { return getTributeModifiers(demandingCiv, demandingWorker).values.sum() } + @Readonly @Suppress("purity") fun getTributeModifiers(demandingCiv: Civilization, demandingWorker: Boolean = false, requireWholeList: Boolean = false): HashMap { val modifiers = LinkedHashMap() // Linked to preserve order when presenting the modifiers table // Can't bully major civs or unsettled CS's @@ -789,6 +792,7 @@ class CityStateFunctions(val civInfo: Civilization) { } // TODO: Optimize, update whenever status changes, otherwise retain the same list + @Readonly fun getUniquesProvidedByCityStates( uniqueType: UniqueType, gameContext: GameContext @@ -809,6 +813,7 @@ class CityStateFunctions(val civInfo: Civilization) { } + @Readonly fun getCityStateBonuses(cityStateType: CityStateType, relationshipLevel: RelationshipLevel, uniqueType: UniqueType? = null): Sequence { val cityStateUniqueMap = when (relationshipLevel) { RelationshipLevel.Ally -> cityStateType.allyBonusUniqueMap diff --git a/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyFunctions.kt b/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyFunctions.kt index b02fff0db9..358250faed 100644 --- a/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyFunctions.kt +++ b/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyFunctions.kt @@ -11,6 +11,7 @@ import com.unciv.logic.map.tile.Tile import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.stats.Stat import com.unciv.models.stats.Stats +import yairm210.purity.annotations.Readonly import kotlin.math.max class DiplomacyFunctions(val civInfo: Civilization) { @@ -74,7 +75,7 @@ class DiplomacyFunctions(val civInfo: Civilization) { } } - + @Readonly fun isAtWarWith(otherCiv: Civilization): Boolean { return when { otherCiv == civInfo -> false diff --git a/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt b/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt index 2b86538a9c..895ea5bf36 100644 --- a/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt +++ b/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt @@ -17,6 +17,7 @@ import com.unciv.models.ruleset.unique.UniqueTriggerActivation import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.translations.fillPlaceholders import com.unciv.ui.components.extensions.toPercent +import yairm210.purity.annotations.Readonly import kotlin.math.ceil import kotlin.math.max import kotlin.math.roundToInt @@ -34,6 +35,7 @@ enum class RelationshipLevel(val color: Color) { Friend(Color.ROYAL), Ally(Color.CYAN) ; + @Readonly operator fun plus(delta: Int): RelationshipLevel { val newOrdinal = (ordinal + delta).coerceIn(0, entries.size - 1) return entries[newOrdinal] @@ -196,7 +198,9 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization { } //region pure functions + @Readonly fun otherCiv() = civInfo.gameInfo.getCivilization(otherCivName) + @Readonly fun otherCivDiplomacy() = otherCiv().getDiplomacyManager(civInfo)!! fun turnsToPeaceTreaty(): Int { @@ -206,6 +210,7 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization { return 0 } + @Readonly fun opinionOfOtherCiv(): Float { var modifierSum = diplomaticModifiers.values.sum() // Angry about attacked CS and destroyed CS do not stack @@ -224,6 +229,7 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization { * @param comparesAs same as [RelationshipLevel.compareTo] * @return `true` if [relationshipLevel] ().compareTo([level]) == [comparesAs] - or: when [comparesAs] > 0 only if [relationshipLevel] > [level] and so on. */ + @Readonly private fun compareRelationshipLevel(level: RelationshipLevel, comparesAs: Int): Boolean { if (!civInfo.isCityState) return relationshipLevel().compareTo(level).sign == comparesAs @@ -259,6 +265,7 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization { if (level == RelationshipLevel.Ally) true else compareRelationshipLevel(level + 1, -1) /** @see compareRelationshipLevel */ + @Readonly fun isRelationshipLevelGE(level: RelationshipLevel) = if (level == RelationshipLevel.Unforgivable) true else compareRelationshipLevel(level + -1, 1) @@ -268,6 +275,7 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization { * @see compareRelationshipLevel * @see relationshipIgnoreAfraid */ + @Readonly fun relationshipLevel(): RelationshipLevel { val level = relationshipIgnoreAfraid() return when { @@ -278,6 +286,7 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization { } /** Same as [relationshipLevel] but omits the distinction Neutral/Afraid, which can be _much_ cheaper */ + @Readonly fun relationshipIgnoreAfraid(): RelationshipLevel { if (civInfo.isHuman() && otherCiv().isHuman()) return RelationshipLevel.Neutral // People make their own choices. @@ -365,6 +374,7 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization { civInfo.cityStateFunctions.updateAllyCivForCityState() } + @Readonly fun getInfluence() = if (civInfo.isAtWarWith(otherCiv())) MINIMUM_INFLUENCE else influence // To be run from City-State DiplomacyManager, which holds the influence. Resting point for every major civ can be different. @@ -549,12 +559,14 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization { diplomaticModifiers[modifier.name] = amount } + @Readonly internal fun getModifier(modifier: DiplomaticModifiers): Float { if (!hasModifier(modifier)) return 0f return diplomaticModifiers[modifier.name]!! } internal fun removeModifier(modifier: DiplomaticModifiers) = diplomaticModifiers.remove(modifier.name) + @Readonly fun hasModifier(modifier: DiplomaticModifiers) = diplomaticModifiers.containsKey(modifier.name) fun signDeclarationOfFriendship() { diff --git a/core/src/com/unciv/logic/civilization/managers/UnitManager.kt b/core/src/com/unciv/logic/civilization/managers/UnitManager.kt index e8896a253f..dfa9d77bbd 100644 --- a/core/src/com/unciv/logic/civilization/managers/UnitManager.kt +++ b/core/src/com/unciv/logic/civilization/managers/UnitManager.kt @@ -13,6 +13,7 @@ import com.unciv.models.ruleset.unique.UniqueTarget import com.unciv.models.ruleset.unique.UniqueTriggerActivation import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unit.BaseUnit +import yairm210.purity.annotations.Readonly class UnitManager(val civInfo: Civilization) { @@ -126,6 +127,7 @@ 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() } diff --git a/core/src/com/unciv/logic/map/tile/Tile.kt b/core/src/com/unciv/logic/map/tile/Tile.kt index 8cc558504f..49d9d5aab7 100644 --- a/core/src/com/unciv/logic/map/tile/Tile.kt +++ b/core/src/com/unciv/logic/map/tile/Tile.kt @@ -650,6 +650,7 @@ class Tile : IsPartOfGameInfoSerialization, Json.Serializable { } } + @Readonly @Suppress("purity") // sets cached value fun isAdjacentToRiver(): Boolean { if (!isAdjacentToRiverKnown) { isAdjacentToRiver = diff --git a/core/src/com/unciv/models/ruleset/unique/IHasUniques.kt b/core/src/com/unciv/models/ruleset/unique/IHasUniques.kt index de9ab563c6..27d8a1771f 100644 --- a/core/src/com/unciv/models/ruleset/unique/IHasUniques.kt +++ b/core/src/com/unciv/models/ruleset/unique/IHasUniques.kt @@ -43,9 +43,11 @@ interface IHasUniques : INamed { * */ fun getUniqueTarget(): UniqueTarget + @Readonly fun getMatchingUniques(uniqueType: UniqueType, state: GameContext = GameContext.EmptyState) = uniqueMap.getMatchingUniques(uniqueType, state) + @Readonly fun getMatchingUniques(uniqueTag: String, state: GameContext = GameContext.EmptyState) = uniqueMap.getMatchingUniques(uniqueTag, state) diff --git a/core/src/com/unciv/models/ruleset/unique/TemporaryUnique.kt b/core/src/com/unciv/models/ruleset/unique/TemporaryUnique.kt index 1a7ece4bc5..ddea5e8d53 100644 --- a/core/src/com/unciv/models/ruleset/unique/TemporaryUnique.kt +++ b/core/src/com/unciv/models/ruleset/unique/TemporaryUnique.kt @@ -1,6 +1,7 @@ package com.unciv.models.ruleset.unique import com.unciv.logic.IsPartOfGameInfoSerialization +import yairm210.purity.annotations.Readonly class TemporaryUnique() : IsPartOfGameInfoSerialization { @@ -32,6 +33,7 @@ fun ArrayList.endTurn() { removeAll { it.turnsLeft == 0 } } +@Readonly fun ArrayList.getMatchingUniques(uniqueType: UniqueType, gameContext: GameContext): Sequence { return this.asSequence() .map { it.uniqueObject }