diff --git a/build.gradle.kts b/build.gradle.kts index 1fd8ce6cb5..fe48664f6f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -38,7 +38,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.42" apply(false) + id("io.github.yairm210.purity-plugin") version "0.0.43" apply(false) } allprojects { @@ -62,19 +62,14 @@ allprojects { "java.lang.Class.getField", // Looks like the Collection.contains is not considered overridden :thunk: - "kotlin.collections.Iterable.iterator", // moved - "kotlin.collections.Collection.containsAll", // moved - "kotlin.collections.filterKeys", // moved - "kotlin.collections.reversed", // moved - "kotlin.collections.minus", // moved "kotlin.Array.get", "kotlin.collections.mutableSetOf", "kotlin.collections.withIndex", // applicable to sequence as well "kotlin.collections.intersect", ) wellKnownPureClasses = setOf( - "java.util.Locale", // moved ) + warnOnPossibleAnnotations = true } apply(plugin = "eclipse") diff --git a/core/src/com/unciv/GUI.kt b/core/src/com/unciv/GUI.kt index 40dcb7f30f..459c1e208e 100644 --- a/core/src/com/unciv/GUI.kt +++ b/core/src/com/unciv/GUI.kt @@ -25,47 +25,29 @@ object GUI { UncivGame.Current.resetToWorldScreen() } + @Readonly fun getSettings(): GameSettings = UncivGame.Current.settings + + @Readonly fun isWorldLoaded(): Boolean = UncivGame.Current.worldScreen != null + @Readonly - fun getSettings(): GameSettings { - return UncivGame.Current.settings - } - - fun isWorldLoaded(): Boolean { - return UncivGame.Current.worldScreen != null - } - fun isMyTurn(): Boolean { if (!UncivGame.isCurrentInitialized() || !isWorldLoaded()) return false return UncivGame.Current.worldScreen!!.isPlayersTurn } - fun isAllowedChangeState(): Boolean { - return UncivGame.Current.worldScreen!!.canChangeState - } + @Readonly fun isAllowedChangeState(): Boolean = UncivGame.Current.worldScreen!!.canChangeState - fun getWorldScreen(): WorldScreen { - return UncivGame.Current.worldScreen!! - } + @Readonly fun getWorldScreen(): WorldScreen = UncivGame.Current.worldScreen!! - fun getWorldScreenIfActive(): WorldScreen? { - return UncivGame.Current.getWorldScreenIfActive() - } + @Readonly fun getWorldScreenIfActive(): WorldScreen? = UncivGame.Current.getWorldScreenIfActive() - fun getMap(): WorldMapHolder { - return UncivGame.Current.worldScreen!!.mapHolder - } + @Readonly fun getMap(): WorldMapHolder = UncivGame.Current.worldScreen!!.mapHolder - fun getUnitTable(): UnitTable { - return UncivGame.Current.worldScreen!!.bottomUnitTable - } + @Readonly fun getUnitTable(): UnitTable = UncivGame.Current.worldScreen!!.bottomUnitTable - fun getViewingPlayer(): Civilization { - return UncivGame.Current.worldScreen!!.viewingCiv - } + @Readonly fun getViewingPlayer(): Civilization = UncivGame.Current.worldScreen!!.viewingCiv - fun getSelectedPlayer(): Civilization { - return UncivGame.Current.worldScreen!!.selectedCiv - } + @Readonly fun getSelectedPlayer(): Civilization = UncivGame.Current.worldScreen!!.selectedCiv /** Disable Undo (as in: forget the way back, but allow future undo checkpoints) */ fun clearUndoCheckpoints() { diff --git a/core/src/com/unciv/UncivGame.kt b/core/src/com/unciv/UncivGame.kt index 8be643ade7..882fff5c29 100644 --- a/core/src/com/unciv/UncivGame.kt +++ b/core/src/com/unciv/UncivGame.kt @@ -47,6 +47,8 @@ import com.unciv.utils.launchOnGLThread import com.unciv.utils.withGLContext import com.unciv.utils.withThreadPoolContext import kotlinx.coroutines.CancellationException +import yairm210.purity.annotations.Pure +import yairm210.purity.annotations.Readonly import java.io.PrintWriter import java.util.EnumSet import java.util.UUID @@ -346,7 +348,9 @@ open class UncivGame(val isConsoleMode: Boolean = false) : Game(), PlatformSpeci /** Get all currently existing screens of type [clazz] * - Not a generic to allow screenStack to be private */ - fun getScreensOfType(clazz: KClass): Sequence = screenStack.asSequence().filter { it::class == clazz } + @Readonly + fun getScreensOfType(clazz: KClass): Sequence = + screenStack.asSequence().filter { it::class == clazz } /** Dispose and remove all currently existing screens of type [clazz] * - Not a generic to allow screenStack to be private @@ -471,6 +475,7 @@ open class UncivGame(val isConsoleMode: Boolean = false) : Game(), PlatformSpeci } /** Returns the [worldScreen] if it is the currently active screen of the game */ + @Readonly fun getWorldScreenIfActive(): WorldScreen? { return if (screen == worldScreen) worldScreen else null } @@ -494,11 +499,11 @@ open class UncivGame(val isConsoleMode: Boolean = false) : Game(), PlatformSpeci // Set by Gdx Game.create callback, or the special cases ConsoleLauncher and unit tests make do with out Gdx and set this themselves. lateinit var Current: UncivGame /** @return `true` if [Current] has been set yet and can be accessed */ - fun isCurrentInitialized() = this::Current.isInitialized + @Readonly fun isCurrentInitialized() = this::Current.isInitialized /** Get the game currently in progress safely - null either if [Current] has not yet been set or if its gameInfo field has no game */ - fun getGameInfoOrNull() = if (isCurrentInitialized()) Current.gameInfo else null - fun isCurrentGame(gameId: String): Boolean = isCurrentInitialized() && Current.gameInfo != null && Current.gameInfo!!.gameId == gameId - fun isDeepLinkedGameLoading() = isCurrentInitialized() && Current.deepLinkedMultiplayerGame != null + @Readonly fun getGameInfoOrNull() = if (isCurrentInitialized()) Current.gameInfo else null + @Readonly fun isCurrentGame(gameId: String): Boolean = isCurrentInitialized() && Current.gameInfo != null && Current.gameInfo!!.gameId == gameId + @Readonly fun isDeepLinkedGameLoading() = isCurrentInitialized() && Current.deepLinkedMultiplayerGame != null } data class Version( @@ -507,7 +512,7 @@ open class UncivGame(val isConsoleMode: Boolean = false) : Game(), PlatformSpeci ) : IsPartOfGameInfoSerialization { @Suppress("unused") // used by json serialization constructor() : this("", -1) - fun toNiceString() = "$text (Build ${number.tr()})" + @Pure fun toNiceString() = "$text (Build ${number.tr()})" } } diff --git a/core/src/com/unciv/json/LastSeenImprovement.kt b/core/src/com/unciv/json/LastSeenImprovement.kt index 44bc95a8e2..43c3489ced 100644 --- a/core/src/com/unciv/json/LastSeenImprovement.kt +++ b/core/src/com/unciv/json/LastSeenImprovement.kt @@ -4,6 +4,8 @@ import com.badlogic.gdx.math.Vector2 import com.badlogic.gdx.utils.Json import com.badlogic.gdx.utils.JsonValue import com.unciv.ui.components.extensions.toPrettyString +import yairm210.purity.annotations.Pure +import yairm210.purity.annotations.Readonly /** * Dedicated HashMap with [Vector2] keys for [Civilization.lastSeenImprovement]. @@ -37,6 +39,7 @@ open class LastSeenImprovement( } } + @Pure private fun String.toVector2(): Vector2 { val (x, y) = removeSurrounding("(", ")").split(',') return Vector2(x.toFloat(), y.toFloat()) diff --git a/core/src/com/unciv/logic/BackwardCompatibility.kt b/core/src/com/unciv/logic/BackwardCompatibility.kt index 1e7e737aae..a7d788badb 100644 --- a/core/src/com/unciv/logic/BackwardCompatibility.kt +++ b/core/src/com/unciv/logic/BackwardCompatibility.kt @@ -8,6 +8,7 @@ import com.unciv.logic.civilization.managers.TechManager import com.unciv.models.ruleset.ModOptions import com.unciv.models.ruleset.PerpetualConstruction import com.unciv.models.ruleset.Ruleset +import yairm210.purity.annotations.Readonly /** * Container for all temporarily used code managing transitions from deprecated elements to their replacements. @@ -98,6 +99,7 @@ object BackwardCompatibility { city.cityConstructions.builtBuildings.remove(building) } + @Readonly fun isInvalidConstruction(construction: String) = !ruleset.buildings.containsKey(construction) && !ruleset.units.containsKey(construction) diff --git a/core/src/com/unciv/logic/GameInfo.kt b/core/src/com/unciv/logic/GameInfo.kt index bb2669d56e..ace10192dd 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.Pure import yairm210.purity.annotations.Readonly import java.security.MessageDigest import java.util.UUID @@ -83,8 +84,7 @@ data class CompatibilityVersion( @Suppress("unused") // used by json serialization constructor() : this(-1, Version()) - operator fun compareTo(other: CompatibilityVersion) = number.compareTo(other.number) - + @Pure operator fun compareTo(other: CompatibilityVersion) = number.compareTo(other.number) } data class VictoryData(val winningCiv: String, val victoryType: String, val victoryTurn: Int) { @@ -371,6 +371,7 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion // Skip the player if we are playing hotseat // If all hotseat players are defeated then skip all but the first one + @Readonly fun shouldAutoProcessHotseatPlayer(): Boolean = !isOnline && player.isDefeated() && (civilizations.any { it.isHuman() && it.isAlive() } @@ -378,6 +379,7 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion // Skip all spectators and defeated players // If all players are defeated then let the first player control next turn + @Readonly fun shouldAutoProcessOnlinePlayer(): Boolean = isOnline && (player.isSpectator() || player.isDefeated() && (civilizations.any { it.isHuman() && it.isAlive() } @@ -474,7 +476,7 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion ) } - fun getEnabledVictories() = ruleset.victories.filter { !it.value.hiddenInVictoryScreen && gameParameters.victoryTypes.contains(it.key) } + @Readonly fun getEnabledVictories() = ruleset.victories.filter { !it.value.hiddenInVictoryScreen && gameParameters.victoryTypes.contains(it.key) } fun processDiplomaticVictory() { if (diplomaticVictoryVotesProcessed) return @@ -778,7 +780,7 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion //endregion - fun asPreview() = GameInfoPreview(this) + @Readonly fun asPreview() = GameInfoPreview(this) } @@ -809,9 +811,9 @@ class GameInfoPreview() { currentTurnStartTime = gameInfo.currentTurnStartTime } - fun getCivilization(civName: String) = civilizations.first { it.civName == civName } - fun getCurrentPlayerCiv() = getCivilization(currentPlayer) - fun getPlayerCiv(playerId: String) = civilizations.firstOrNull { it.playerId == playerId } + @Readonly fun getCivilization(civName: String) = civilizations.first { it.civName == civName } + @Readonly fun getCurrentPlayerCiv() = getCivilization(currentPlayer) + @Readonly fun getPlayerCiv(playerId: String) = civilizations.firstOrNull { it.playerId == playerId } } /** Class to use when parsing jsons if you only want the serialization [version]. */ diff --git a/core/src/com/unciv/logic/GameStarter.kt b/core/src/com/unciv/logic/GameStarter.kt index 029a8dcee2..980e77dfe4 100644 --- a/core/src/com/unciv/logic/GameStarter.kt +++ b/core/src/com/unciv/logic/GameStarter.kt @@ -24,6 +24,7 @@ import com.unciv.models.stats.Stats import com.unciv.models.translations.equalsPlaceholderText import com.unciv.models.translations.getPlaceholderParameters import com.unciv.utils.debug +import yairm210.purity.annotations.Readonly object GameStarter { // temporary instrumentation while tuning/debugging @@ -577,6 +578,7 @@ object GameStarter { }.sortedByDescending { it.isHuman() } // More important for humans to get their start biases! } + @Readonly private fun getFreeTiles(tileMap: TileMap, landTilesInBigEnoughGroup: Map, minimumDistanceBetweenStartingLocations: Int): MutableList { return landTilesInBigEnoughGroup.asSequence() .filter { diff --git a/core/src/com/unciv/logic/HolidayDates.kt b/core/src/com/unciv/logic/HolidayDates.kt index 2c6d9dd492..0a1724e363 100644 --- a/core/src/com/unciv/logic/HolidayDates.kt +++ b/core/src/com/unciv/logic/HolidayDates.kt @@ -1,6 +1,8 @@ package com.unciv.logic import com.unciv.ui.screens.mainmenuscreen.EasterEggFloatingArt +import yairm210.purity.annotations.Pure +import yairm210.purity.annotations.Readonly import java.time.DayOfWeek import java.time.Instant import java.time.LocalDate @@ -123,7 +125,7 @@ object HolidayDates { abstract fun getByYear(year: Int): DateRange companion object { - fun safeValueOf(name: String) = entries.firstOrNull { it.name == name } + @Pure fun safeValueOf(name: String) = entries.firstOrNull { it.name == name } } } diff --git a/core/src/com/unciv/logic/IdChecker.kt b/core/src/com/unciv/logic/IdChecker.kt index 2b9d85e041..8364e5a5ba 100644 --- a/core/src/com/unciv/logic/IdChecker.kt +++ b/core/src/com/unciv/logic/IdChecker.kt @@ -1,5 +1,6 @@ package com.unciv.logic +import yairm210.purity.annotations.Readonly import java.util.Locale import kotlin.math.abs diff --git a/core/src/com/unciv/logic/city/CityResources.kt b/core/src/com/unciv/logic/city/CityResources.kt index 9af07ac448..7831739f6f 100644 --- a/core/src/com/unciv/logic/city/CityResources.kt +++ b/core/src/com/unciv/logic/city/CityResources.kt @@ -21,7 +21,7 @@ object CityResources { /** Only for *city-wide* resources - civ-wide resources should use civ-level resources */ @Readonly fun getCityResourcesAvailableToCity(city: City): ResourceSupplyList { - @LocalState val resourceModifers = HashMap() + val resourceModifers = HashMap() for (resource in city.civ.gameInfo.ruleset.tileResources.values) resourceModifers[resource.name] = city.civ.getResourceModifier(resource) diff --git a/core/src/com/unciv/logic/civilization/Civilization.kt b/core/src/com/unciv/logic/civilization/Civilization.kt index 577b0eb4bf..2100ad5f8d 100644 --- a/core/src/com/unciv/logic/civilization/Civilization.kt +++ b/core/src/com/unciv/logic/civilization/Civilization.kt @@ -473,7 +473,6 @@ class Civilization : IsPartOfGameInfoSerialization { */ @Readonly fun getCivResourcesByName(): HashMap { - @LocalState val hashMap = HashMap(gameInfo.ruleset.tileResources.size) for (resource in gameInfo.ruleset.tileResources.keys) hashMap[resource] = 0 for (entry in getCivResourceSupply())