diff --git a/build.gradle.kts b/build.gradle.kts index fbb7fb5716..d15c6050b4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -62,6 +62,8 @@ allprojects { "java.util.BitSet.get", // moved "kotlin.collections.getValue", // moved "kotlin.collections.randomOrNull", + "kotlin.collections.Collection.isEmpty", + "kotlin.collections.subtract" ) wellKnownPureClasses = setOf( ) diff --git a/core/src/com/unciv/logic/city/CityConstructions.kt b/core/src/com/unciv/logic/city/CityConstructions.kt index 8d6f633fa7..326fe07b6a 100644 --- a/core/src/com/unciv/logic/city/CityConstructions.kt +++ b/core/src/com/unciv/logic/city/CityConstructions.kt @@ -266,6 +266,7 @@ class CityConstructions : IsPartOfGameInfoSerialization { else 0 } + @Readonly fun getRemainingWork(constructionName: String, useStoredProduction: Boolean = true): Int { val constr = getConstruction(constructionName) return when { diff --git a/core/src/com/unciv/logic/civilization/Civilization.kt b/core/src/com/unciv/logic/civilization/Civilization.kt index 0f1866cd63..e06f8ba55c 100644 --- a/core/src/com/unciv/logic/civilization/Civilization.kt +++ b/core/src/com/unciv/logic/civilization/Civilization.kt @@ -380,11 +380,11 @@ class Civilization : IsPartOfGameInfoSerialization { var cityStatePersonality: CityStatePersonality = CityStatePersonality.Neutral var cityStateResource: String? = null var cityStateUniqueUnit: String? = null // Unique unit for militaristic city state. Might still be null if there are no appropriate units - - fun hasMetCivTerritory(otherCiv: Civilization): Boolean = + + @Readonly fun hasMetCivTerritory(otherCiv: Civilization): Boolean = otherCiv.getCivTerritory().any { gameInfo.tileMap[it].isExplored(this) } @Readonly fun getCompletedPolicyBranchesCount(): Int = policies.adoptedPolicies.count { Policy.isBranchCompleteByName(it) } - private fun getCivTerritory() = cities.asSequence().flatMap { it.tiles.asSequence() } + @Readonly private fun getCivTerritory() = cities.asSequence().flatMap { it.tiles.asSequence() } @Readonly fun getPreferredVictoryTypes(): List { @@ -463,7 +463,7 @@ class Civilization : IsPartOfGameInfoSerialization { return newResourceSupplyList } - fun isCapitalConnectedToCity(city: City): Boolean = cache.citiesConnectedToCapitalToMediums.keys.contains(city) + @Readonly fun isCapitalConnectedToCity(city: City): Boolean = cache.citiesConnectedToCapitalToMediums.keys.contains(city) /** diff --git a/core/src/com/unciv/logic/civilization/diplomacy/CityStateFunctions.kt b/core/src/com/unciv/logic/civilization/diplomacy/CityStateFunctions.kt index b37a1fb077..0aaae453c8 100644 --- a/core/src/com/unciv/logic/civilization/diplomacy/CityStateFunctions.kt +++ b/core/src/com/unciv/logic/civilization/diplomacy/CityStateFunctions.kt @@ -187,6 +187,7 @@ class CityStateFunctions(val civInfo: Civilization) { ) } + @Readonly fun influenceGainedByGift(donorCiv: Civilization, giftAmount: Int): Int { // https://github.com/Gedemon/Civ5-DLL/blob/aa29e80751f541ae04858b6d2a2c7dcca454201e/CvGameCoreDLL_Expansion1/CvMinorCivAI.cpp // line 8681 and below diff --git a/core/src/com/unciv/logic/civilization/managers/GreatPersonManager.kt b/core/src/com/unciv/logic/civilization/managers/GreatPersonManager.kt index 548bec538f..2dfff4cd5c 100644 --- a/core/src/com/unciv/logic/civilization/managers/GreatPersonManager.kt +++ b/core/src/com/unciv/logic/civilization/managers/GreatPersonManager.kt @@ -7,6 +7,7 @@ import com.unciv.logic.civilization.NotificationCategory import com.unciv.models.Counter import com.unciv.models.ruleset.unique.UniqueType import com.unciv.ui.components.MayaCalendar +import yairm210.purity.annotations.Readonly // todo: Great Admiral? @@ -46,11 +47,12 @@ class GreatPersonManager : IsPartOfGameInfoSerialization { return toReturn } + @Readonly private fun getPoolKey(greatPerson: String) = civInfo.getEquivalentUnit(greatPerson) .getMatchingUniques(UniqueType.GPPointPool) // An empty string is used to indicate the Unique wasn't found .firstOrNull()?.params?.get(0) ?: "" - + fun getPointsRequiredForGreatPerson(greatPerson: String): Int { val key = getPoolKey(greatPerson) if (pointsForNextGreatPersonCounter[key] == 0) { @@ -102,12 +104,14 @@ class GreatPersonManager : IsPartOfGameInfoSerialization { } /** Get Great People specific to this manager's Civilization, already filtered by `isHiddenBySettings` */ + @Readonly fun getGreatPeople() = civInfo.gameInfo.ruleset.units.values.asSequence() .filter { it.isGreatPerson } .map { civInfo.getEquivalentUnit(it.name) } .filterNot { it.isUnavailableBySettings(civInfo.gameInfo) } .toHashSet() + @Readonly fun getGreatPersonPointsForNextTurn(): Counter { val greatPersonPoints = Counter() for (city in civInfo.cities) greatPersonPoints.add(city.getGreatPersonPoints()) diff --git a/core/src/com/unciv/logic/civilization/managers/QuestManager.kt b/core/src/com/unciv/logic/civilization/managers/QuestManager.kt index 5ce7898181..62e32b162f 100644 --- a/core/src/com/unciv/logic/civilization/managers/QuestManager.kt +++ b/core/src/com/unciv/logic/civilization/managers/QuestManager.kt @@ -33,6 +33,7 @@ import com.unciv.models.translations.getPlaceholderParameters import com.unciv.models.translations.tr import com.unciv.ui.components.extensions.toPercent import com.unciv.utils.randomWeighted +import yairm210.purity.annotations.Readonly import kotlin.random.Random class QuestManager : IsPartOfGameInfoSerialization { @@ -80,21 +81,24 @@ class QuestManager : IsPartOfGameInfoSerialization { private var unitsKilledFromCiv: HashMap> = HashMap() /** Returns true if [civ] have active quests for [challenger] */ - fun haveQuestsFor(challenger: Civilization): Boolean = getAssignedQuestsFor(challenger.civName).any() + @Readonly fun haveQuestsFor(challenger: Civilization): Boolean = getAssignedQuestsFor(challenger.civName).any() /** Access all assigned Quests for [civName] */ + @Readonly fun getAssignedQuestsFor(civName: String) = assignedQuests.asSequence().filter { it.assignee == civName } /** Access all assigned Quests of "type" [questName] */ // Note if we decide to cache an index of these (such as `assignedQuests.groupBy { it.questNameInstance }`), this accessor would simplify the transition + @Readonly private fun getAssignedQuestsOfName(questName: QuestName) = assignedQuests.asSequence().filter { it.questNameInstance == questName } /** Returns true if [civ] has asked anyone to conquer [target] */ - fun wantsDead(target: String): Boolean = getAssignedQuestsOfName(QuestName.ConquerCityState).any { it.data1 == target } + @Readonly fun wantsDead(target: String): Boolean = getAssignedQuestsOfName(QuestName.ConquerCityState).any { it.data1 == target } /** Returns the influence multiplier for [donor] from a Investment quest that [civ] might have (assumes only one) */ + @Readonly fun getInvestmentMultiplier(donor: String): Float { val investmentQuest = getAssignedQuestsOfName(QuestName.Invest).firstOrNull { it.assignee == donor } ?: return 1f @@ -367,12 +371,14 @@ class QuestManager : IsPartOfGameInfoSerialization { } /** Returns true if [civ] can assign a quest to [challenger] */ + @Readonly private fun canAssignAQuestTo(challenger: Civilization): Boolean { return !challenger.isDefeated() && challenger.isMajorCiv() && civ.knows(challenger) && !civ.isAtWarWith(challenger) } /** Returns true if the [quest] can be assigned to [challenger] */ + @Readonly private fun isQuestValid(quest: Quest, challenger: Civilization): Boolean { if (!canAssignAQuestTo(challenger)) return false @@ -403,6 +409,7 @@ class QuestManager : IsPartOfGameInfoSerialization { } } + @Readonly private fun isRouteQuestValid(challenger: Civilization): Boolean { if (challenger.cities.isEmpty()) return false if (challenger.isCapitalConnectedToCity(civ.getCapital()!!)) return false @@ -414,6 +421,7 @@ class QuestManager : IsPartOfGameInfoSerialization { } } + @Readonly private fun isDenounceCivQuestValid(challenger: Civilization, mostRecentBully: String?): Boolean { return mostRecentBully != null && challenger.knows(mostRecentBully) @@ -424,6 +432,7 @@ class QuestManager : IsPartOfGameInfoSerialization { } /** Returns true if the [assignedQuest] is successfully completed */ + @Readonly private fun isComplete(assignedQuest: AssignedQuest): Boolean { val assignee = civ.gameInfo.getCivilization(assignedQuest.assignee) return when (assignedQuest.questNameInstance) { @@ -441,6 +450,7 @@ class QuestManager : IsPartOfGameInfoSerialization { } /** Returns true if the [assignedQuest] request cannot be fulfilled anymore */ + @Readonly private fun isObsolete(assignedQuest: AssignedQuest): Boolean { val assignee = civ.gameInfo.getCivilization(assignedQuest.assignee) return when (assignedQuest.questNameInstance) { @@ -489,6 +499,7 @@ class QuestManager : IsPartOfGameInfoSerialization { } /** Returns the score for the [assignedQuest] */ + @Readonly private fun getScoreForQuest(assignedQuest: AssignedQuest): Int { val assignee = civ.gameInfo.getCivilization(assignedQuest.assignee) @@ -545,6 +556,7 @@ class QuestManager : IsPartOfGameInfoSerialization { * Tied leaders are separated by ", " - translators cannot influence this, sorry. * @param inquiringAssignedQuest Determines ["type"][AssignedQuest.questNameInstance] to find all competitors in [assignedQuests] and [viewing civ][AssignedQuest.assignee]. */ + @Readonly fun getScoreStringForGlobalQuest(inquiringAssignedQuest: AssignedQuest): String { require(inquiringAssignedQuest.assigner == civ.civName) require(inquiringAssignedQuest.isGlobal()) @@ -654,7 +666,7 @@ class QuestManager : IsPartOfGameInfoSerialization { /** Gets notified when [killed]'s military unit was killed by [killer], for war with major pseudo-quest */ fun militaryUnitKilledBy(killer: Civilization, killed: Civilization) { - if (!warWithMajorActive(killed)) return + if (!isWarWithMajorActive(killed)) return // No credit if we're at war or haven't met if (!civ.knows(killer) || civ.isAtWarWith(killer)) return @@ -705,14 +717,11 @@ class QuestManager : IsPartOfGameInfoSerialization { unitsKilledFromCiv.remove(attacker.civName) } - fun warWithMajorActive(target: Civilization): Boolean { - return unitsToKillForCiv.containsKey(target.civName) - } + @Readonly fun isWarWithMajorActive(target: Civilization): Boolean = unitsToKillForCiv.containsKey(target.civName) - fun unitsToKill(target: Civilization): Int { - return unitsToKillForCiv[target.civName] ?: 0 - } + @Readonly fun unitsToKill(target: Civilization): Int = unitsToKillForCiv[target.civName] ?: 0 + @Readonly fun unitsKilledSoFar(target: Civilization, viewingCiv: Civilization): Int { val killMap = unitsKilledFromCiv[target.civName] ?: return 0 return killMap[viewingCiv.civName] ?: 0 @@ -734,6 +743,7 @@ class QuestManager : IsPartOfGameInfoSerialization { /** * Returns the weight of the [questName], depends on city state trait and personality */ + @Readonly private fun getQuestWeight(questName: String): Float { var weight = 1f val quest = ruleset.quests[questName] ?: return 0f @@ -751,6 +761,7 @@ class QuestManager : IsPartOfGameInfoSerialization { * Returns a random [Tile] containing a Barbarian encampment within 8 tiles of [civ] * to be destroyed */ + @Readonly private fun getBarbarianEncampmentForQuest(): Tile? { val encampments = civ.getCapital()!!.getCenterTile().getTilesInDistance(8) .filter { it.improvement == Constants.barbarianEncampment }.toList() @@ -764,6 +775,7 @@ class QuestManager : IsPartOfGameInfoSerialization { * by the [civ] and the [challenger], and must be viewable by the [challenger]; * if none exists, it returns null. */ + @Readonly private fun getResourceForQuest(challenger: Civilization): TileResource? { val ownedByCityStateResources = civ.detailedCivResources.map { it.resource } val ownedByMajorResources = challenger.detailedCivResources.map { it.resource } @@ -781,8 +793,9 @@ class QuestManager : IsPartOfGameInfoSerialization { return notOwnedResources.randomOrNull() } + @Readonly private fun getWonderToBuildForQuest(challenger: Civilization): Building? { - fun isMoreThanAQuarterDone(city: City, buildingName: String) = + @Readonly fun isMoreThanAQuarterDone(city: City, buildingName: String) = city.cityConstructions.getWorkDone(buildingName) * 3 > city.cityConstructions.getRemainingWork(buildingName) val wonders = ruleset.buildings.values .filter { building -> @@ -803,6 +816,7 @@ class QuestManager : IsPartOfGameInfoSerialization { /** * Returns a random Natural Wonder not yet discovered by [challenger]. */ + @Readonly private fun getNaturalWonderToFindForQuest(challenger: Civilization): String? { val naturalWondersToFind = civ.gameInfo.tileMap.naturalWonders.subtract(challenger.naturalWonders) @@ -812,6 +826,7 @@ class QuestManager : IsPartOfGameInfoSerialization { /** * Returns a Great Person [BaseUnit] that is not owned by both the [challenger] and the [civ] */ + @Readonly private fun getGreatPersonForQuest(challenger: Civilization): BaseUnit? { val ruleset = ruleset // omit if the accessor should be converted to a transient field @@ -835,6 +850,7 @@ class QuestManager : IsPartOfGameInfoSerialization { * Returns a random [Civilization] (major) that [challenger] has met, but whose territory he * cannot see; if none exists, it returns null. */ + @Readonly private fun getCivilizationToFindForQuest(challenger: Civilization): Civilization? { val civilizationsToFind = challenger.getKnownCivs() .filter { it.isAlive() && it.isMajorCiv() && !challenger.hasMetCivTerritory(it) } @@ -846,6 +862,7 @@ class QuestManager : IsPartOfGameInfoSerialization { /** * Returns a city-state [Civilization] that [civ] wants to target for hostile quests */ + @Readonly private fun getCityStateTarget(challenger: Civilization): Civilization? { val closestProximity = civ.gameInfo.getAliveCityStates() .mapNotNull { civ.proximity[it.civName] }.filter { it != Proximity.None }.minByOrNull { it.ordinal } @@ -861,6 +878,7 @@ class QuestManager : IsPartOfGameInfoSerialization { /** Returns a [Civilization] of the civ that most recently bullied [civ]. * Note: forgets after 20 turns has passed! */ + @Readonly private fun getMostRecentBully(): String? { val bullies = civ.diplomacy.values.filter { it.hasFlag(DiplomacyFlags.Bullied) } return bullies.maxByOrNull { it.getFlag(DiplomacyFlags.Bullied) }?.otherCivName @@ -892,17 +910,17 @@ class AssignedQuest( questObject = quest ?: gameInfo.ruleset.quests[questName]!! } - fun isIndividual(): Boolean = !isGlobal() - fun isGlobal(): Boolean = questObject.isGlobal() + @Readonly fun isIndividual(): Boolean = !isGlobal() + @Readonly fun isGlobal(): Boolean = questObject.isGlobal() @Suppress("MemberVisibilityCanBePrivate") - fun doesExpire(): Boolean = questObject.duration > 0 - fun isExpired(): Boolean = doesExpire() && getRemainingTurns() == 0 + @Readonly fun doesExpire(): Boolean = questObject.duration > 0 + @Readonly fun isExpired(): Boolean = doesExpire() && getRemainingTurns() == 0 @Suppress("MemberVisibilityCanBePrivate") - fun getDuration(): Int = (gameInfo.speed.modifier * questObject.duration).toInt() - fun getRemainingTurns(): Int = (assignedOnTurn + getDuration() - gameInfo.turns).coerceAtLeast(0) - fun getInfluence() = questObject.influence + @Readonly fun getDuration(): Int = (gameInfo.speed.modifier * questObject.duration).toInt() + @Readonly fun getRemainingTurns(): Int = (assignedOnTurn + getDuration() - gameInfo.turns).coerceAtLeast(0) + @Readonly fun getInfluence() = questObject.influence - fun getDescription(): String = questObject.description.fillPlaceholders(data1) + @Readonly fun getDescription(): String = questObject.description.fillPlaceholders(data1) fun onClickAction() { when (questNameInstance) { diff --git a/core/src/com/unciv/models/ruleset/Quest.kt b/core/src/com/unciv/models/ruleset/Quest.kt index ab37107a6b..50405964f5 100644 --- a/core/src/com/unciv/models/ruleset/Quest.kt +++ b/core/src/com/unciv/models/ruleset/Quest.kt @@ -2,6 +2,7 @@ package com.unciv.models.ruleset import com.unciv.logic.civilization.Civilization import com.unciv.models.stats.INamed +import yairm210.purity.annotations.Readonly enum class QuestName(val value: String) { Route("Route"), @@ -24,7 +25,7 @@ enum class QuestName(val value: String) { None("") ; companion object { - fun find(value: String) = values().firstOrNull { it.value == value } ?: None + fun find(value: String) = entries.firstOrNull { it.value == value } ?: None } } @@ -67,6 +68,6 @@ class Quest : INamed { var weightForCityStateType = HashMap() /** Checks if `this` is a Global quest */ - fun isGlobal(): Boolean = type == QuestType.Global - fun isIndividual(): Boolean = !isGlobal() + @Readonly fun isGlobal(): Boolean = type == QuestType.Global + @Readonly fun isIndividual(): Boolean = !isGlobal() } diff --git a/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt b/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt index 7a47f6bb53..585791718e 100644 --- a/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt +++ b/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt @@ -371,6 +371,7 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction { // This returns the name of the unit this tech upgrades this unit to, // or null if there is no automatic upgrade at that tech. + @Readonly fun automaticallyUpgradedInProductionToUnitByTech(techName: String): String? { for (obsoleteTech: String in techsAtWhichAutoUpgradeInProduction()) if (obsoleteTech == techName) @@ -405,6 +406,7 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction { } } + @Readonly fun getReplacedUnit(ruleset: Ruleset): BaseUnit { return if (replaces == null) this else ruleset.units[replaces!!]!! diff --git a/core/src/com/unciv/models/ruleset/validation/Suppression.kt b/core/src/com/unciv/models/ruleset/validation/Suppression.kt index 2eb27b15e7..67ca054424 100644 --- a/core/src/com/unciv/models/ruleset/validation/Suppression.kt +++ b/core/src/com/unciv/models/ruleset/validation/Suppression.kt @@ -11,6 +11,7 @@ import com.unciv.models.ruleset.unique.UniqueParameterType import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.translations.fillPlaceholders import yairm210.purity.annotations.Pure +import yairm210.purity.annotations.Readonly /** * All public methods dealing with how Mod authors can suppress RulesetValidator output. @@ -65,6 +66,7 @@ object Suppression { else -> true } + @Pure private fun matchesFilter(error: RulesetError, filter: String): Boolean { if (error.text == filter) return true if (!filter.endsWith('*') || !filter.startsWith('*')) return false @@ -72,6 +74,7 @@ object Suppression { } /** Determine if [error] matches any suppression Unique in [ModOptions] or the [sourceObject], or any suppression modifier in [sourceUnique] */ + @Readonly internal fun isErrorSuppressed( globalSuppressionFilters: Collection, sourceObject: IHasUniques?, @@ -81,6 +84,7 @@ object Suppression { if (error.errorSeverityToReport >= RulesetErrorSeverity.Error) return false if (sourceObject == null && globalSuppressionFilters.isEmpty()) return false + @Readonly fun getWildcardFilter(unique: Unique) = unique.params[0].let { if (it.startsWith('*')) it else "*$it*" } diff --git a/core/src/com/unciv/ui/screens/diplomacyscreen/CityStateDiplomacyTable.kt b/core/src/com/unciv/ui/screens/diplomacyscreen/CityStateDiplomacyTable.kt index 2b8a6196f3..ca04e9fb9c 100644 --- a/core/src/com/unciv/ui/screens/diplomacyscreen/CityStateDiplomacyTable.kt +++ b/core/src/com/unciv/ui/screens/diplomacyscreen/CityStateDiplomacyTable.kt @@ -86,7 +86,7 @@ class CityStateDiplomacyTable(private val diplomacyScreen: DiplomacyScreen) { diplomacyTable.add(getQuestTable(assignedQuest)).row() } - for (target in otherCiv.getKnownCivs().filter { otherCiv.questManager.warWithMajorActive(it) && viewingCiv != it }) { + for (target in otherCiv.getKnownCivs().filter { otherCiv.questManager.isWarWithMajorActive(it) && viewingCiv != it }) { diplomacyTable.addSeparator() diplomacyTable.add(getWarWithMajorTable(target, otherCiv)).row() }