chore(purity): Converted internal state classes to use annotations

This commit is contained in:
yairm210 2025-08-08 13:03:31 +03:00
parent 064ba90990
commit 714fc6cfa6
8 changed files with 34 additions and 15 deletions

View File

@ -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.49" apply(false)
id("io.github.yairm210.purity-plugin") version "0.0.51" apply(false)
}
allprojects {
@ -59,12 +59,6 @@ allprojects {
)
wellKnownInternalStateClasses = setOf(
"com.badlogic.gdx.math.Vector2",
"com.unciv.models.stats.Stats",
"com.unciv.models.Counter",
"com.unciv.models.ruleset.tile.ResourceSupplyList",
"com.unciv.models.ruleset.validation.RulesetErrorList",
"com.unciv.logic.map.BFS",
"com.unciv.logic.trade.TradeOffersList",
)
warnOnPossibleAnnotations = true
}
@ -176,7 +170,7 @@ project(":core") {
"implementation"("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
"implementation"("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion")
"implementation"("io.github.yairm210:purity-annotations:0.0.40")
"implementation"("io.github.yairm210:purity-annotations:0.0.51")
"implementation"("io.ktor:ktor-client-core:$ktorVersion")
"implementation"("io.ktor:ktor-client-cio:$ktorVersion")

View File

@ -1,6 +1,7 @@
package com.unciv.logic.map
import com.unciv.logic.map.tile.Tile
import yairm210.purity.annotations.InternalState
import yairm210.purity.annotations.Readonly
import kotlin.collections.ArrayDeque
@ -10,6 +11,7 @@ import kotlin.collections.ArrayDeque
* @param startingPoint Starting [Tile] from which to start the search
* @param predicate A condition for subsequent neighboring tiles to be considered in search
*/
@InternalState
class BFS(
val startingPoint: Tile,
private val predicate : (Tile) -> Boolean

View File

@ -1,7 +1,9 @@
package com.unciv.logic.trade
import com.unciv.logic.IsPartOfGameInfoSerialization
import yairm210.purity.annotations.InternalState
@InternalState
class TradeOffersList: ArrayList<TradeOffer>(), IsPartOfGameInfoSerialization {
override fun add(element: TradeOffer): Boolean {
val equivalentOffer = firstOrNull { it.name == element.name && it.type == element.type }

View File

@ -3,6 +3,7 @@ package com.unciv.models
import com.badlogic.gdx.utils.Json
import com.badlogic.gdx.utils.JsonValue
import com.unciv.logic.IsPartOfGameInfoSerialization
import yairm210.purity.annotations.InternalState
import yairm210.purity.annotations.LocalState
import yairm210.purity.annotations.Readonly
@ -14,6 +15,7 @@ import yairm210.purity.annotations.Readonly
* - Therefore, Deserialization works properly ***only*** with [K] === String.
* (ignoring this will return a deserialized map, but the keys will violate the compile-time type and BE strings)
*/
@InternalState
open class Counter<K>(
fromMap: Map<K, Int>? = null
) : LinkedHashMap<K, Int>(fromMap?.size ?: 10), IsPartOfGameInfoSerialization, Json.Serializable {

View File

@ -3,12 +3,14 @@ package com.unciv.models.ruleset.tile
import com.unciv.Constants
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.IConstruction // Kdoc only
import yairm210.purity.annotations.InternalState
import yairm210.purity.annotations.Readonly
/** Container helps aggregating supply and demand of [resources][ResourceSupply.resource], categorized by [origin][ResourceSupply.origin].
*
* @param keepZeroAmounts If `false`, entries with [amount][ResourceSupply.amount] 0 are eliminated
*/
@InternalState
class ResourceSupplyList(
private val keepZeroAmounts: Boolean = false
) : ArrayList<ResourceSupplyList.ResourceSupply>(24) {

View File

@ -7,6 +7,7 @@ import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.ruleset.validation.ModCompatibility.meetsAllRequirements
import com.unciv.models.ruleset.validation.ModCompatibility.meetsBaseRequirements
import yairm210.purity.annotations.Pure
import yairm210.purity.annotations.Readonly
/**
* Helper collection dealing with declarative Mod compatibility
@ -28,8 +29,9 @@ object ModCompatibility {
*
* Note: The guessing part may potentially be deprecated and removed if we get our Modders to complete declarative coverage.
*/
fun isAudioVisualMod(mod: Ruleset) = isAudioVisualDeclared(mod) ?: isAudioVisualGuessed(mod)
@Readonly fun isAudioVisualMod(mod: Ruleset) = isAudioVisualDeclared(mod) ?: isAudioVisualGuessed(mod)
@Readonly
private fun isAudioVisualDeclared(mod: Ruleset): Boolean? {
if (mod.modOptions.hasUnique(UniqueType.ModIsAudioVisualOnly)) return true
if (mod.modOptions.hasUnique(UniqueType.ModIsAudioVisual)) return true
@ -38,6 +40,7 @@ object ModCompatibility {
}
// If there's media (audio folders or any atlas), show the PAV choice...
@Readonly @Suppress("purity") // requies marking file functions
private fun isAudioVisualGuessed(mod: Ruleset): Boolean {
val folder = mod.folderLocation ?: return false // Also catches isBuiltin
fun isSubFolderNotEmpty(modFolder: FileHandle, name: String): Boolean {
@ -52,11 +55,14 @@ object ModCompatibility {
return folder.list("atlas").isNotEmpty()
}
@Readonly
fun isExtensionMod(mod: Ruleset) =
!mod.modOptions.isBaseRuleset
&& mod.name.isNotBlank()
&& !mod.modOptions.hasUnique(UniqueType.ModIsAudioVisualOnly)
@Readonly
@Suppress("purity") // requies marking file functions
fun isConstantsOnly(mod: Ruleset): Boolean {
val folder = mod.folderLocation ?: return false
if (folder.list("atlas").isNotEmpty()) return false
@ -73,10 +79,12 @@ object ModCompatibility {
return partialName in modName.lowercase()
}
@Readonly
private fun isIncompatibleWith(mod: Ruleset, otherMod: Ruleset) =
mod.modOptions.getMatchingUniques(UniqueType.ModIncompatibleWith)
.any { modNameFilter(otherMod.name, it.params[0]) }
@Readonly
private fun isIncompatible(mod: Ruleset, otherMod: Ruleset) =
isIncompatibleWith(mod, otherMod) || isIncompatibleWith(otherMod, mod)
@ -88,6 +96,7 @@ object ModCompatibility {
* - For each ModRequires: Not ([baseRuleset] meets filter OR any other cached _extension_ mod meets filter) -> Nope
* - All ModRequires tested -> OK
*/
@Readonly
fun meetsBaseRequirements(mod: Ruleset, baseRuleset: Ruleset): Boolean {
if (isIncompatible(mod, baseRuleset)) return false
@ -115,6 +124,7 @@ object ModCompatibility {
* - For each ModRequires: Not([baseRuleset] meets filter OR any other **selected** extension mod meets filter) -> Nope
* - All ModRequires tested -> OK
*/
@Readonly
fun meetsAllRequirements(mod: Ruleset, baseRuleset: Ruleset, selectedExtensionMods: Iterable<Ruleset>): Boolean {
val otherSelectedExtensionMods = selectedExtensionMods.filterNot { it == mod }.toList()
if (otherSelectedExtensionMods.any { isIncompatible(mod, it) }) return false

View File

@ -6,7 +6,8 @@ import com.unciv.models.ruleset.unique.IHasUniques
import com.unciv.models.ruleset.unique.GameContext
import com.unciv.models.ruleset.unique.Unique
import com.unciv.models.ruleset.unique.UniqueType
import yairm210.purity.annotations.LocalState
import yairm210.purity.annotations.InternalState
import yairm210.purity.annotations.Pure
import yairm210.purity.annotations.Readonly
class RulesetError(val text: String, val errorSeverityToReport: RulesetErrorSeverity)
@ -31,6 +32,7 @@ enum class RulesetErrorSeverity(val color: Color, val iconName: String) {
*
* @param ruleset The ruleset being validated (needed to check modOptions for suppression uniques). Leave `null` only for validation results that need no suppression checks.
*/
@InternalState
class RulesetErrorList(
ruleset: Ruleset? = null
) : ArrayList<RulesetError>() {
@ -87,20 +89,23 @@ class RulesetErrorList(
return true
}
@Readonly
fun getFinalSeverity(): RulesetErrorSeverity {
if (isEmpty()) return RulesetErrorSeverity.OK
return this.maxOf { it.errorSeverityToReport }
}
/** @return `true` means severe errors make the mod unplayable */
fun isError() = getFinalSeverity() == RulesetErrorSeverity.Error
@Readonly fun isError() = getFinalSeverity() == RulesetErrorSeverity.Error
/** @return `true` means problems exist, Options screen mod checker or unit tests for vanilla ruleset should complain */
fun isNotOK() = getFinalSeverity() != RulesetErrorSeverity.OK
@Readonly fun isNotOK() = getFinalSeverity() != RulesetErrorSeverity.OK
/** @return `true` means at least errors impacting gameplay exist, new game screen should warn or block */
fun isWarnUser() = getFinalSeverity() >= RulesetErrorSeverity.Warning
@Readonly fun isWarnUser() = getFinalSeverity() >= RulesetErrorSeverity.Warning
fun getErrorText(unfiltered: Boolean = false) =
@Readonly fun getErrorText(unfiltered: Boolean = false) =
getErrorText { unfiltered || it.errorSeverityToReport > RulesetErrorSeverity.WarningOptionsOnly }
@Readonly
fun getErrorText(filter: (RulesetError)->Boolean) =
filter(filter)
.sortedByDescending { it.errorSeverityToReport }
@ -114,7 +119,7 @@ class RulesetErrorList(
companion object {
/** Helper factory for a single entry list (which can result in an empty list due to suppression)
* Note: Valid source for [addAll] since suppression is already taken care of. */
@Readonly
@Pure
fun of(
text: String,
severity: RulesetErrorSeverity = RulesetErrorSeverity.Error,

View File

@ -1,6 +1,7 @@
package com.unciv.models.stats
import com.unciv.models.translations.tr
import yairm210.purity.annotations.InternalState
import yairm210.purity.annotations.Pure
import yairm210.purity.annotations.Readonly
@ -12,6 +13,7 @@ import yairm210.purity.annotations.Readonly
*
* Also possible: `<Stats>`.[values].sum() and similar aggregates over a Sequence<Float>.
*/
@InternalState
open class Stats(
var production: Float = 0f,
var food: Float = 0f,