From 4fac0337044fee3c6a33b3aedacf673c9dfe8fc7 Mon Sep 17 00:00:00 2001 From: SomeTroglodyte <63000004+SomeTroglodyte@users.noreply.github.com> Date: Tue, 8 Aug 2023 22:06:33 +0200 Subject: [PATCH] Ruleset object reorganization (#9898) * IHasUniques without INamed removed - treat ModOptions same as GlobalUniques in that respect * Unify uniqueObjects and uniqueMap initialization * Move and split RulesetValidator * Split Ruleset file to make it pure single-class * Minor linting --- .../com/unciv/models/ruleset/GlobalUniques.kt | 2 + .../com/unciv/models/ruleset/ModOptions.kt | 46 ++++ core/src/com/unciv/models/ruleset/Ruleset.kt | 237 ++---------------- .../com/unciv/models/ruleset/RulesetCache.kt | 182 ++++++++++++++ .../com/unciv/models/ruleset/RulesetObject.kt | 27 +- .../com/unciv/models/ruleset/Specialist.kt | 11 + .../models/ruleset/unique/IHasUniques.kt | 21 +- .../com/unciv/models/ruleset/unique/Unique.kt | 2 +- .../unciv/models/ruleset/unique/UniqueType.kt | 4 +- .../ruleset/validation/RulesetErrorList.kt | 63 +++++ .../{ => validation}/RulesetValidator.kt | 151 +++++------ .../{ => validation}/TextSimilarity.kt | 2 +- core/src/com/unciv/models/stats/INamed.kt | 3 + core/src/com/unciv/models/stats/NamedStats.kt | 2 +- .../unciv/ui/popups/options/ModCheckTab.kt | 6 +- .../newgamescreen/NewGameModCheckHelpers.kt | 2 +- 16 files changed, 419 insertions(+), 342 deletions(-) create mode 100644 core/src/com/unciv/models/ruleset/ModOptions.kt create mode 100644 core/src/com/unciv/models/ruleset/RulesetCache.kt create mode 100644 core/src/com/unciv/models/ruleset/Specialist.kt create mode 100644 core/src/com/unciv/models/ruleset/validation/RulesetErrorList.kt rename core/src/com/unciv/models/ruleset/{ => validation}/RulesetValidator.kt (88%) rename core/src/com/unciv/models/ruleset/{ => validation}/TextSimilarity.kt (98%) diff --git a/core/src/com/unciv/models/ruleset/GlobalUniques.kt b/core/src/com/unciv/models/ruleset/GlobalUniques.kt index af9fc24806..b5e04458b0 100644 --- a/core/src/com/unciv/models/ruleset/GlobalUniques.kt +++ b/core/src/com/unciv/models/ruleset/GlobalUniques.kt @@ -5,6 +5,8 @@ import com.unciv.models.ruleset.unique.UniqueTarget import com.unciv.models.ruleset.unique.UniqueType class GlobalUniques: RulesetObject() { + override var name = "GlobalUniques" + override fun getUniqueTarget() = UniqueTarget.Global override fun makeLink() = "" // No own category on Civilopedia screen diff --git a/core/src/com/unciv/models/ruleset/ModOptions.kt b/core/src/com/unciv/models/ruleset/ModOptions.kt new file mode 100644 index 0000000000..7ed994f182 --- /dev/null +++ b/core/src/com/unciv/models/ruleset/ModOptions.kt @@ -0,0 +1,46 @@ +package com.unciv.models.ruleset + +import com.unciv.models.ModConstants +import com.unciv.models.ruleset.unique.IHasUniques +import com.unciv.models.ruleset.unique.Unique +import com.unciv.models.ruleset.unique.UniqueMap +import com.unciv.models.ruleset.unique.UniqueTarget + +object ModOptionsConstants { + const val diplomaticRelationshipsCannotChange = "Diplomatic relationships cannot change" + const val convertGoldToScience = "Can convert gold to science with sliders" + const val allowCityStatesSpawnUnits = "Allow City States to spawn with additional units" + const val tradeCivIntroductions = "Can trade civilization introductions for [] Gold" + const val disableReligion = "Disable religion" + const val allowRazeCapital = "Allow raze capital" + const val allowRazeHolyCity = "Allow raze holy city" +} + +class ModOptions : IHasUniques { + override var name = "ModOptions" + + var isBaseRuleset = false + var techsToRemove = HashSet() + var buildingsToRemove = HashSet() + var unitsToRemove = HashSet() + var nationsToRemove = HashSet() + + + var lastUpdated = "" + var modUrl = "" + var defaultBranch = "master" + var author = "" + var modSize = 0 + var topics = mutableListOf() + + override var uniques = ArrayList() + + @delegate:Transient + override val uniqueObjects: List by lazy (::uniqueObjectsProvider) + @delegate:Transient + override val uniqueMap: UniqueMap by lazy(::uniqueMapProvider) + + override fun getUniqueTarget() = UniqueTarget.ModOptions + + val constants = ModConstants() +} diff --git a/core/src/com/unciv/models/ruleset/Ruleset.kt b/core/src/com/unciv/models/ruleset/Ruleset.kt index f79e0c43ed..5a0cecad18 100644 --- a/core/src/com/unciv/models/ruleset/Ruleset.kt +++ b/core/src/com/unciv/models/ruleset/Ruleset.kt @@ -1,16 +1,10 @@ package com.unciv.models.ruleset -import com.badlogic.gdx.Gdx import com.badlogic.gdx.files.FileHandle import com.unciv.json.fromJsonFile import com.unciv.json.json import com.unciv.logic.BackwardCompatibility.updateDeprecations -import com.unciv.logic.UncivShowableException -import com.unciv.logic.map.MapParameters -import com.unciv.models.Counter -import com.unciv.models.ModConstants import com.unciv.models.metadata.BaseRuleset -import com.unciv.models.metadata.GameParameters import com.unciv.models.ruleset.nation.CityStateType import com.unciv.models.ruleset.nation.Difficulty import com.unciv.models.ruleset.nation.Nation @@ -22,61 +16,34 @@ import com.unciv.models.ruleset.tile.TileImprovement import com.unciv.models.ruleset.tile.TileResource import com.unciv.models.ruleset.unique.IHasUniques import com.unciv.models.ruleset.unique.Unique -import com.unciv.models.ruleset.unique.UniqueTarget import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unit.BaseUnit import com.unciv.models.ruleset.unit.Promotion import com.unciv.models.ruleset.unit.UnitType +import com.unciv.models.ruleset.validation.RulesetValidator import com.unciv.models.stats.INamed -import com.unciv.models.stats.NamedStats import com.unciv.models.translations.tr -import com.unciv.ui.components.extensions.colorFromRGB import com.unciv.utils.Log -import com.unciv.utils.debug import kotlin.collections.set -object ModOptionsConstants { - const val diplomaticRelationshipsCannotChange = "Diplomatic relationships cannot change" - const val convertGoldToScience = "Can convert gold to science with sliders" - const val allowCityStatesSpawnUnits = "Allow City States to spawn with additional units" - const val tradeCivIntroductions = "Can trade civilization introductions for [] Gold" - const val disableReligion = "Disable religion" - const val allowRazeCapital = "Allow raze capital" - const val allowRazeHolyCity = "Allow raze holy city" -} - -class ModOptions : IHasUniques { - var isBaseRuleset = false - var techsToRemove = HashSet() - var buildingsToRemove = HashSet() - var unitsToRemove = HashSet() - var nationsToRemove = HashSet() - - - var lastUpdated = "" - var modUrl = "" - var defaultBranch = "master" - var author = "" - var modSize = 0 - var topics = mutableListOf() - - override var uniques = ArrayList() - - // If these two are delegated with "by lazy", the mod download process crashes and burns - // Instead, Ruleset.load sets them, which is preferable in this case anyway - override var uniqueObjects: List = listOf() - override var uniqueMap: Map> = mapOf() - - override fun getUniqueTarget() = UniqueTarget.ModOptions - - val constants = ModConstants() -} - class Ruleset { - var folderLocation:FileHandle?=null + /** If (and only if) this Ruleset is a mod, this will be the source folder. + * In other words, this is `null` for built-in and combined rulesets. + */ + var folderLocation: FileHandle? = null + /** A Ruleset instance can represent a built-in ruleset, a mod or a combined ruleset. + * + * `name` will be the built-in's fullName, the mod's name as displayed (same as folder name), + * or in the case of combined rulesets it will be empty. + * + * @see toString + * @see BaseRuleset.fullName + * @see RulesetCache.getComplexRuleset + */ var name = "" + val beliefs = LinkedHashMap() val buildings = LinkedHashMap() val difficulties = LinkedHashMap() @@ -240,8 +207,6 @@ class Ruleset { } catch (ex: Exception) { Log.error("Failed to get modOptions from json file", ex) } - modOptions.uniqueObjects = modOptions.uniques.map { Unique(it, UniqueTarget.ModOptions) } - modOptions.uniqueMap = modOptions.uniqueObjects.groupBy { it.placeholderText } } val techFile = folderHandle.child("Techs.json") @@ -471,175 +436,3 @@ class Ruleset { fun checkModLinks(tryFixUnknownUniques: Boolean = false) = RulesetValidator(this).getErrorList(tryFixUnknownUniques) } - -/** Loading mods is expensive, so let's only do it once and - * save all of the loaded rulesets somewhere for later use - * */ -object RulesetCache : HashMap() { - /** Whether mod checking allows untyped uniques - set to `false` once all vanilla uniques are converted! */ - var modCheckerAllowUntypedUniques = true - - /** Similarity below which an untyped unique can be considered a potential misspelling. - * Roughly corresponds to the fraction of the Unique placeholder text that can be different/misspelled, but with some extra room for [getRelativeTextDistance] idiosyncrasies. */ - var uniqueMisspellingThreshold = 0.15 // Tweak as needed. Simple misspellings seem to be around 0.025, so would mostly be caught by 0.05. IMO 0.1 would be good, but raising to 0.15 also seemed to catch what may be an outdated Unique. - - - /** Returns error lines from loading the rulesets, so we can display the errors to users */ - fun loadRulesets(consoleMode: Boolean = false, noMods: Boolean = false) :List { - val newRulesets = HashMap() - - for (ruleset in BaseRuleset.values()) { - val fileName = "jsons/${ruleset.fullName}" - val fileHandle = - if (consoleMode) FileHandle(fileName) - else Gdx.files.internal(fileName) - newRulesets[ruleset.fullName] = Ruleset().apply { - name = ruleset.fullName - load(fileHandle) - } - } - this.putAll(newRulesets) - - val errorLines = ArrayList() - if (!noMods){ - val modsHandles = if (consoleMode) FileHandle("mods").list() - else Gdx.files.local("mods").list() - - for (modFolder in modsHandles) { - if (modFolder.name().startsWith('.')) continue - if (!modFolder.isDirectory) continue - try { - val modRuleset = Ruleset() - modRuleset.name = modFolder.name() - modRuleset.load(modFolder.child("jsons")) - modRuleset.folderLocation = modFolder - newRulesets[modRuleset.name] = modRuleset - debug("Mod loaded successfully: %s", modRuleset.name) - if (Log.shouldLog()) { - val modLinksErrors = modRuleset.checkModLinks() - // For extension mods which use references to base ruleset objects, the parameter type - // errors are irrelevant - the checker ran without a base ruleset - val logFilter: (RulesetError) -> Boolean = - if (modRuleset.modOptions.isBaseRuleset) { { it.errorSeverityToReport > RulesetErrorSeverity.WarningOptionsOnly } } - else { { it.errorSeverityToReport > RulesetErrorSeverity.WarningOptionsOnly && !it.text.contains("does not fit parameter type") } } - if (modLinksErrors.any(logFilter)) { - debug("checkModLinks errors: %s", modLinksErrors.getErrorText(logFilter)) - } - } - } catch (ex: Exception) { - errorLines += "Exception loading mod '${modFolder.name()}':" - errorLines += " ${ex.localizedMessage}" - errorLines += " ${ex.cause?.localizedMessage}" - } - } - if (Log.shouldLog()) for (line in errorLines) debug(line) - } - - // We save the 'old' cache values until we're ready to replace everything, so that the cache isn't empty while we try to load ruleset files - // - this previously lead to "can't find Vanilla ruleset" if the user had a lot of mods and downloaded a new one - this.clear() - this.putAll(newRulesets) - - return errorLines - } - - - fun getVanillaRuleset() = this[BaseRuleset.Civ_V_Vanilla.fullName]!!.clone() // safeguard, so no-one edits the base ruleset by mistake - - fun getSortedBaseRulesets(): List { - val baseRulesets = values - .filter { it.modOptions.isBaseRuleset } - .map { it.name } - .distinct() - if (baseRulesets.size < 2) return baseRulesets - - // We sort the base rulesets such that the ones unciv provides are on the top, - // and the rest is alphabetically ordered. - return baseRulesets.sortedWith( - compareBy( - { ruleset -> - BaseRuleset.values() - .firstOrNull { br -> br.fullName == ruleset }?.ordinal - ?: BaseRuleset.values().size - }, - { it } - ) - ) - } - - /** Creates a combined [Ruleset] from a list of mods contained in [parameters]. */ - fun getComplexRuleset(parameters: MapParameters) = - getComplexRuleset(parameters.mods, parameters.baseRuleset) - - /** Creates a combined [Ruleset] from a list of mods contained in [parameters]. */ - fun getComplexRuleset(parameters: GameParameters) = - getComplexRuleset(parameters.mods, parameters.baseRuleset) - - /** - * Creates a combined [Ruleset] from a list of mods. - * If no baseRuleset is passed in [optionalBaseRuleset] (or a non-existing one), then the vanilla Ruleset is included automatically. - * Any mods in the [mods] parameter marked as base ruleset (or not loaded in [RulesetCache]) are ignored. - */ - fun getComplexRuleset(mods: LinkedHashSet, optionalBaseRuleset: String? = null): Ruleset { - val baseRuleset = - if (containsKey(optionalBaseRuleset) && this[optionalBaseRuleset]!!.modOptions.isBaseRuleset) - this[optionalBaseRuleset]!! - else getVanillaRuleset() - - val loadedMods = mods.asSequence() - .filter { containsKey(it) } - .map { this[it]!! } - .filter { !it.modOptions.isBaseRuleset } - - return getComplexRuleset(baseRuleset, loadedMods.asIterable()) - } - - /** - * Creates a combined [Ruleset] from [baseRuleset] and [extensionRulesets] which must only contain non-base rulesets. - */ - fun getComplexRuleset(baseRuleset: Ruleset, extensionRulesets: Iterable): Ruleset { - val newRuleset = Ruleset() - - val loadedMods = extensionRulesets.asSequence() + baseRuleset - - for (mod in loadedMods.sortedByDescending { it.modOptions.isBaseRuleset }) { - if (mod.modOptions.isBaseRuleset) { - // This is so we don't keep using the base ruleset's uniques *by reference* and add to in ad infinitum - newRuleset.modOptions.uniques = ArrayList() - newRuleset.modOptions.isBaseRuleset = true - } - newRuleset.add(mod) - newRuleset.mods += mod.name - } - newRuleset.updateBuildingCosts() // only after we've added all the mods can we calculate the building costs - - return newRuleset - } - - /** - * Runs [Ruleset.checkModLinks] on a temporary [combined Ruleset][getComplexRuleset] for a list of [mods] - */ - fun checkCombinedModLinks( - mods: LinkedHashSet, - baseRuleset: String? = null, - tryFixUnknownUniques: Boolean = false - ): RulesetErrorList { - return try { - val newRuleset = getComplexRuleset(mods, baseRuleset) - newRuleset.modOptions.isBaseRuleset = true // This is so the checkModLinks finds all connections - newRuleset.checkModLinks(tryFixUnknownUniques) - } catch (ex: UncivShowableException) { - // This happens if a building is dependent on a tech not in the base ruleset - // because newRuleset.updateBuildingCosts() in getComplexRuleset() throws an error - RulesetErrorList() - .apply { add(ex.message, RulesetErrorSeverity.Error) } - } - } - -} - -class Specialist: NamedStats() { - var color = ArrayList() - val colorObject by lazy { colorFromRGB(color) } - var greatPersonPoints = Counter() -} diff --git a/core/src/com/unciv/models/ruleset/RulesetCache.kt b/core/src/com/unciv/models/ruleset/RulesetCache.kt new file mode 100644 index 0000000000..72d43b33e5 --- /dev/null +++ b/core/src/com/unciv/models/ruleset/RulesetCache.kt @@ -0,0 +1,182 @@ +package com.unciv.models.ruleset + +import com.badlogic.gdx.Gdx +import com.badlogic.gdx.files.FileHandle +import com.unciv.logic.UncivShowableException +import com.unciv.logic.map.MapParameters +import com.unciv.models.metadata.BaseRuleset +import com.unciv.models.metadata.GameParameters +import com.unciv.models.ruleset.validation.RulesetError +import com.unciv.models.ruleset.validation.RulesetErrorList +import com.unciv.models.ruleset.validation.RulesetErrorSeverity +import com.unciv.utils.Log +import com.unciv.utils.debug + +/** Loading mods is expensive, so let's only do it once and + * save all of the loaded rulesets somewhere for later use + * */ +object RulesetCache : HashMap() { + /** Whether mod checking allows untyped uniques - set to `false` once all vanilla uniques are converted! */ + var modCheckerAllowUntypedUniques = true + + /** Similarity below which an untyped unique can be considered a potential misspelling. + * Roughly corresponds to the fraction of the Unique placeholder text that can be different/misspelled, but with some extra room for [getRelativeTextDistance] idiosyncrasies. */ + var uniqueMisspellingThreshold = 0.15 // Tweak as needed. Simple misspellings seem to be around 0.025, so would mostly be caught by 0.05. IMO 0.1 would be good, but raising to 0.15 also seemed to catch what may be an outdated Unique. + + + /** Returns error lines from loading the rulesets, so we can display the errors to users */ + fun loadRulesets(consoleMode: Boolean = false, noMods: Boolean = false) :List { + val newRulesets = HashMap() + + for (ruleset in BaseRuleset.values()) { + val fileName = "jsons/${ruleset.fullName}" + val fileHandle = + if (consoleMode) FileHandle(fileName) + else Gdx.files.internal(fileName) + newRulesets[ruleset.fullName] = Ruleset().apply { + name = ruleset.fullName + load(fileHandle) + } + } + this.putAll(newRulesets) + + val errorLines = ArrayList() + if (!noMods) { + val modsHandles = if (consoleMode) FileHandle("mods").list() + else Gdx.files.local("mods").list() + + for (modFolder in modsHandles) { + if (modFolder.name().startsWith('.')) continue + if (!modFolder.isDirectory) continue + try { + val modRuleset = Ruleset() + modRuleset.name = modFolder.name() + modRuleset.load(modFolder.child("jsons")) + modRuleset.folderLocation = modFolder + newRulesets[modRuleset.name] = modRuleset + debug("Mod loaded successfully: %s", modRuleset.name) + if (Log.shouldLog()) { + val modLinksErrors = modRuleset.checkModLinks() + // For extension mods which use references to base ruleset objects, the parameter type + // errors are irrelevant - the checker ran without a base ruleset + val logFilter: (RulesetError) -> Boolean = + if (modRuleset.modOptions.isBaseRuleset) { { it.errorSeverityToReport > RulesetErrorSeverity.WarningOptionsOnly } } + else { { it.errorSeverityToReport > RulesetErrorSeverity.WarningOptionsOnly && !it.text.contains("does not fit parameter type") } } + if (modLinksErrors.any(logFilter)) { + debug( + "checkModLinks errors: %s", + modLinksErrors.getErrorText(logFilter) + ) + } + } + } catch (ex: Exception) { + errorLines += "Exception loading mod '${modFolder.name()}':" + errorLines += " ${ex.localizedMessage}" + errorLines += " ${ex.cause?.localizedMessage}" + } + } + if (Log.shouldLog()) for (line in errorLines) debug(line) + } + + // We save the 'old' cache values until we're ready to replace everything, so that the cache isn't empty while we try to load ruleset files + // - this previously lead to "can't find Vanilla ruleset" if the user had a lot of mods and downloaded a new one + this.clear() + this.putAll(newRulesets) + + return errorLines + } + + + fun getVanillaRuleset() = this[BaseRuleset.Civ_V_Vanilla.fullName]!!.clone() // safeguard, so no-one edits the base ruleset by mistake + + fun getSortedBaseRulesets(): List { + val baseRulesets = values + .filter { it.modOptions.isBaseRuleset } + .map { it.name } + .distinct() + if (baseRulesets.size < 2) return baseRulesets + + // We sort the base rulesets such that the ones unciv provides are on the top, + // and the rest is alphabetically ordered. + return baseRulesets.sortedWith( + compareBy( + { ruleset -> + BaseRuleset.values() + .firstOrNull { br -> br.fullName == ruleset }?.ordinal + ?: BaseRuleset.values().size + }, + { it } + ) + ) + } + + /** Creates a combined [Ruleset] from a list of mods contained in [parameters]. */ + fun getComplexRuleset(parameters: MapParameters) = + getComplexRuleset(parameters.mods, parameters.baseRuleset) + + /** Creates a combined [Ruleset] from a list of mods contained in [parameters]. */ + fun getComplexRuleset(parameters: GameParameters) = + getComplexRuleset(parameters.mods, parameters.baseRuleset) + + /** + * Creates a combined [Ruleset] from a list of mods. + * If no baseRuleset is passed in [optionalBaseRuleset] (or a non-existing one), then the vanilla Ruleset is included automatically. + * Any mods in the [mods] parameter marked as base ruleset (or not loaded in [RulesetCache]) are ignored. + */ + fun getComplexRuleset(mods: LinkedHashSet, optionalBaseRuleset: String? = null): Ruleset { + val baseRuleset = + if (containsKey(optionalBaseRuleset) && this[optionalBaseRuleset]!!.modOptions.isBaseRuleset) + this[optionalBaseRuleset]!! + else getVanillaRuleset() + + val loadedMods = mods.asSequence() + .filter { containsKey(it) } + .map { this[it]!! } + .filter { !it.modOptions.isBaseRuleset } + + return getComplexRuleset(baseRuleset, loadedMods.asIterable()) + } + + /** + * Creates a combined [Ruleset] from [baseRuleset] and [extensionRulesets] which must only contain non-base rulesets. + */ + fun getComplexRuleset(baseRuleset: Ruleset, extensionRulesets: Iterable): Ruleset { + val newRuleset = Ruleset() + + val loadedMods = extensionRulesets.asSequence() + baseRuleset + + for (mod in loadedMods.sortedByDescending { it.modOptions.isBaseRuleset }) { + if (mod.modOptions.isBaseRuleset) { + // This is so we don't keep using the base ruleset's uniques *by reference* and add to in ad infinitum + newRuleset.modOptions.uniques = ArrayList() + newRuleset.modOptions.isBaseRuleset = true + } + newRuleset.add(mod) + newRuleset.mods += mod.name + } + newRuleset.updateBuildingCosts() // only after we've added all the mods can we calculate the building costs + + return newRuleset + } + + /** + * Runs [Ruleset.checkModLinks] on a temporary [combined Ruleset][getComplexRuleset] for a list of [mods] + */ + fun checkCombinedModLinks( + mods: LinkedHashSet, + baseRuleset: String? = null, + tryFixUnknownUniques: Boolean = false + ): RulesetErrorList { + return try { + val newRuleset = getComplexRuleset(mods, baseRuleset) + newRuleset.modOptions.isBaseRuleset = true // This is so the checkModLinks finds all connections + newRuleset.checkModLinks(tryFixUnknownUniques) + } catch (ex: UncivShowableException) { + // This happens if a building is dependent on a tech not in the base ruleset + // because newRuleset.updateBuildingCosts() in getComplexRuleset() throws an error + RulesetErrorList() + .apply { add(ex.message, RulesetErrorSeverity.Error) } + } + } + +} diff --git a/core/src/com/unciv/models/ruleset/RulesetObject.kt b/core/src/com/unciv/models/ruleset/RulesetObject.kt index ff559df120..d4274acae4 100644 --- a/core/src/com/unciv/models/ruleset/RulesetObject.kt +++ b/core/src/com/unciv/models/ruleset/RulesetObject.kt @@ -3,13 +3,12 @@ package com.unciv.models.ruleset import com.unciv.models.ruleset.unique.IHasUniques import com.unciv.models.ruleset.unique.Unique import com.unciv.models.ruleset.unique.UniqueMap -import com.unciv.models.stats.INamed import com.unciv.models.stats.NamedStats import com.unciv.ui.screens.civilopediascreen.FormattedLine import com.unciv.ui.screens.civilopediascreen.ICivilopediaText -interface IRulesetObject: INamed, IHasUniques, ICivilopediaText{ - var originRuleset:String +interface IRulesetObject: IHasUniques, ICivilopediaText { + var originRuleset: String } abstract class RulesetObject: IRulesetObject { @@ -17,17 +16,9 @@ abstract class RulesetObject: IRulesetObject { override var originRuleset = "" override var uniques = ArrayList() // Can not be a hashset as that would remove doubles @delegate:Transient - override val uniqueObjects: List by lazy { - if (uniques.isEmpty()) emptyList() - else uniques.map { Unique(it, getUniqueTarget(), name) } - } + override val uniqueObjects: List by lazy (::uniqueObjectsProvider) @delegate:Transient - override val uniqueMap: UniqueMap by lazy { - if (uniques.isEmpty()) UniqueMap() - val newUniqueMap = UniqueMap() - newUniqueMap.addUniques(uniqueObjects) - newUniqueMap - } + override val uniqueMap: UniqueMap by lazy(::uniqueMapProvider) override var civilopediaText = listOf() override fun toString() = name @@ -38,15 +29,9 @@ abstract class RulesetStatsObject: NamedStats(), IRulesetObject { override var originRuleset = "" override var uniques = ArrayList() // Can not be a hashset as that would remove doubles @delegate:Transient - override val uniqueObjects: List by lazy { - if (uniques.isEmpty()) emptyList() - else uniques.map { Unique(it, getUniqueTarget(), name) } - } + override val uniqueObjects: List by lazy (::uniqueObjectsProvider) @delegate:Transient - override val uniqueMap: Map> by lazy { - if (uniques.isEmpty()) emptyMap() - else uniqueObjects.groupBy { it.placeholderText } - } + override val uniqueMap: UniqueMap by lazy(::uniqueMapProvider) override var civilopediaText = listOf() } diff --git a/core/src/com/unciv/models/ruleset/Specialist.kt b/core/src/com/unciv/models/ruleset/Specialist.kt new file mode 100644 index 0000000000..0a7920e0cc --- /dev/null +++ b/core/src/com/unciv/models/ruleset/Specialist.kt @@ -0,0 +1,11 @@ +package com.unciv.models.ruleset + +import com.unciv.models.Counter +import com.unciv.models.stats.NamedStats +import com.unciv.ui.components.extensions.colorFromRGB + +class Specialist: NamedStats() { + var color = ArrayList() + val colorObject by lazy { colorFromRGB(color) } + var greatPersonPoints = Counter() +} diff --git a/core/src/com/unciv/models/ruleset/unique/IHasUniques.kt b/core/src/com/unciv/models/ruleset/unique/IHasUniques.kt index ea53588a22..e1733eba5e 100644 --- a/core/src/com/unciv/models/ruleset/unique/IHasUniques.kt +++ b/core/src/com/unciv/models/ruleset/unique/IHasUniques.kt @@ -1,15 +1,29 @@ package com.unciv.models.ruleset.unique +import com.unciv.models.stats.INamed + /** * Common interface for all 'ruleset objects' that have Uniques, like BaseUnit, Nation, etc. */ -interface IHasUniques { +interface IHasUniques : INamed { var uniques: ArrayList // Can not be a hashset as that would remove doubles - // I bet there's a way of initializing these without having to override it everywhere... - val uniqueObjects: List + // Every implementation should override these with the same `by lazy (::thingsProvider)` + // AND every implementation should annotate these with `@delegate:Transient` + val uniqueObjects: List val uniqueMap: Map> + fun uniqueObjectsProvider(): List { + if (uniques.isEmpty()) return emptyList() + return uniques.map { Unique(it, getUniqueTarget(), name) } + } + fun uniqueMapProvider(): UniqueMap { + val newUniqueMap = UniqueMap() + if (uniques.isNotEmpty()) + newUniqueMap.addUniques(uniqueObjects) + return newUniqueMap + } + /** Technically not currently needed, since the unique target can be retrieved from every unique in the uniqueObjects, * But making this a function is relevant for future "unify Unciv object" plans ;) * */ @@ -29,4 +43,3 @@ interface IHasUniques { fun hasUnique(uniqueType: UniqueType, stateForConditionals: StateForConditionals? = null) = getMatchingUniques(uniqueType.placeholderText, stateForConditionals).any() } - diff --git a/core/src/com/unciv/models/ruleset/unique/Unique.kt b/core/src/com/unciv/models/ruleset/unique/Unique.kt index 91f28b7e65..d84b234039 100644 --- a/core/src/com/unciv/models/ruleset/unique/Unique.kt +++ b/core/src/com/unciv/models/ruleset/unique/Unique.kt @@ -8,7 +8,7 @@ import com.unciv.logic.city.City import com.unciv.logic.civilization.Civilization import com.unciv.logic.civilization.managers.ReligionState import com.unciv.models.ruleset.Ruleset -import com.unciv.models.ruleset.RulesetValidator +import com.unciv.models.ruleset.validation.RulesetValidator import com.unciv.models.stats.Stats import com.unciv.models.translations.getConditionals import com.unciv.models.translations.getPlaceholderParameters diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt index 139baba17a..58056b3531 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt @@ -2,8 +2,8 @@ package com.unciv.models.ruleset.unique import com.unciv.Constants import com.unciv.models.ruleset.Ruleset -import com.unciv.models.ruleset.RulesetErrorSeverity -import com.unciv.models.ruleset.RulesetValidator // Kdoc only +import com.unciv.models.ruleset.validation.RulesetErrorSeverity +import com.unciv.models.ruleset.validation.RulesetValidator // Kdoc only import com.unciv.models.translations.getPlaceholderParameters import com.unciv.models.translations.getPlaceholderText diff --git a/core/src/com/unciv/models/ruleset/validation/RulesetErrorList.kt b/core/src/com/unciv/models/ruleset/validation/RulesetErrorList.kt new file mode 100644 index 0000000000..ffe97c9df7 --- /dev/null +++ b/core/src/com/unciv/models/ruleset/validation/RulesetErrorList.kt @@ -0,0 +1,63 @@ +package com.unciv.models.ruleset.validation + +import com.badlogic.gdx.graphics.Color + +class RulesetError(val text: String, val errorSeverityToReport: RulesetErrorSeverity) + +enum class RulesetErrorSeverity(val color: Color) { + OK(Color.GREEN), + WarningOptionsOnly(Color.YELLOW), + Warning(Color.YELLOW), + Error(Color.RED), +} + +class RulesetErrorList : ArrayList() { + operator fun plusAssign(text: String) { + add(text, RulesetErrorSeverity.Error) + } + + fun add(text: String, errorSeverityToReport: RulesetErrorSeverity) { + add(RulesetError(text, errorSeverityToReport)) + } + + override fun add(element: RulesetError): Boolean { + // Suppress duplicates due to the double run of some checks for invariant/specific, + // Without changing collection type or making RulesetError obey the equality contract + val existing = firstOrNull { it.text == element.text } + ?: return super.add(element) + if (existing.errorSeverityToReport >= element.errorSeverityToReport) return false + remove(existing) + return super.add(element) + } + + override fun addAll(elements: Collection): Boolean { + var result = false + for (element in elements) + if (add(element)) result = true + return result + } + + 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 + /** @return `true` means problems exist, Options screen mod checker or unit tests for vanilla ruleset should complain */ + 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 + + fun getErrorText(unfiltered: Boolean = false) = + getErrorText { unfiltered || it.errorSeverityToReport != RulesetErrorSeverity.WarningOptionsOnly } + fun getErrorText(filter: (RulesetError)->Boolean) = + filter(filter) + .sortedByDescending { it.errorSeverityToReport } + .joinToString("\n") { + it.errorSeverityToReport.name + ": " + + // This will go through tr(), unavoidably, which will move the conditionals + // out of place. Prevent via kludge: + it.text.replace('<','〈').replace('>','〉') + } +} diff --git a/core/src/com/unciv/models/ruleset/RulesetValidator.kt b/core/src/com/unciv/models/ruleset/validation/RulesetValidator.kt similarity index 88% rename from core/src/com/unciv/models/ruleset/RulesetValidator.kt rename to core/src/com/unciv/models/ruleset/validation/RulesetValidator.kt index 6fd609788a..b8c7eb16b9 100644 --- a/core/src/com/unciv/models/ruleset/RulesetValidator.kt +++ b/core/src/com/unciv/models/ruleset/validation/RulesetValidator.kt @@ -1,8 +1,11 @@ -package com.unciv.models.ruleset +package com.unciv.models.ruleset.validation import com.badlogic.gdx.graphics.Color import com.unciv.Constants import com.unciv.logic.map.tile.RoadStatus +import com.unciv.models.ruleset.IRulesetObject +import com.unciv.models.ruleset.Ruleset +import com.unciv.models.ruleset.RulesetCache import com.unciv.models.ruleset.nation.getContrastRatio import com.unciv.models.ruleset.nation.getRelativeLuminance import com.unciv.models.ruleset.tile.TerrainType @@ -66,16 +69,21 @@ class RulesetValidator(val ruleset: Ruleset) { if (techColumn.columnNumber < 0) lines+= "Tech Column number ${techColumn.columnNumber} is negative" if (techColumn.buildingCost == -1) - lines.add("Tech Column number ${techColumn.columnNumber} has no explicit building cost", RulesetErrorSeverity.Warning) + lines.add("Tech Column number ${techColumn.columnNumber} has no explicit building cost", + RulesetErrorSeverity.Warning + ) if (techColumn.wonderCost == -1) - lines.add("Tech Column number ${techColumn.columnNumber} has no explicit wonder cost", RulesetErrorSeverity.Warning) + lines.add("Tech Column number ${techColumn.columnNumber} has no explicit wonder cost", + RulesetErrorSeverity.Warning + ) } for (building in ruleset.buildings.values) { if (building.requiredTech == null && building.cost == -1 && !building.hasUnique( UniqueType.Unbuildable)) lines.add("${building.name} is buildable and therefore should either have an explicit cost or reference an existing tech!", - RulesetErrorSeverity.Warning) + RulesetErrorSeverity.Warning + ) checkUniques(building, lines, rulesetInvariant, tryFixUnknownUniques) @@ -191,7 +199,8 @@ class RulesetValidator(val ruleset: Ruleset) { unit.isCivilian() && !unit.isGreatPersonOfType("War")) { lines.add("${unit.name} can place improvement $improvementName which has no stats, preventing unit automation!", - RulesetErrorSeverity.Warning) + RulesetErrorSeverity.Warning + ) } } @@ -286,7 +295,8 @@ class RulesetValidator(val ruleset: Ruleset) { if (tech.prerequisites.asSequence().filterNot { it == prereq } .any { getPrereqTree(it).contains(prereq) }){ lines.add("No need to add $prereq as a prerequisite of ${tech.name} - it is already implicit from the other prerequisites!", - RulesetErrorSeverity.Warning) + RulesetErrorSeverity.Warning + ) } if (getPrereqTree(prereq).contains(tech.name)) @@ -332,9 +342,13 @@ class RulesetValidator(val ruleset: Ruleset) { lines += "Population in cities from settlers must be strictly positive! Found value ${era.settlerPopulation} for era ${era.name}" if (era.allyBonus.isNotEmpty()) - lines.add("Era ${era.name} contains city-state bonuses. City-state bonuses are now defined in CityStateType.json", RulesetErrorSeverity.WarningOptionsOnly) + lines.add("Era ${era.name} contains city-state bonuses. City-state bonuses are now defined in CityStateType.json", + RulesetErrorSeverity.WarningOptionsOnly + ) if (era.friendBonus.isNotEmpty()) - lines.add("Era ${era.name} contains city-state bonuses. City-state bonuses are now defined in CityStateType.json", RulesetErrorSeverity.WarningOptionsOnly) + lines.add("Era ${era.name} contains city-state bonuses. City-state bonuses are now defined in CityStateType.json", + RulesetErrorSeverity.WarningOptionsOnly + ) checkUniques(era, lines, rulesetSpecific, tryFixUnknownUniques) @@ -388,10 +402,14 @@ class RulesetValidator(val ruleset: Ruleset) { // These are warning as of 3.17.5 to not break existing mods and give them time to correct, should be upgraded to error in the future for (prereq in promotion.prerequisites) if (!ruleset.unitPromotions.containsKey(prereq)) - lines.add("${promotion.name} requires promotion $prereq which does not exist!", RulesetErrorSeverity.Warning) + lines.add("${promotion.name} requires promotion $prereq which does not exist!", + RulesetErrorSeverity.Warning + ) for (unitType in promotion.unitTypes) if (!ruleset.unitTypes.containsKey(unitType) && (ruleset.unitTypes.isNotEmpty() || !vanillaRuleset.unitTypes.containsKey(unitType))) - lines.add("${promotion.name} references unit type $unitType, which does not exist!", RulesetErrorSeverity.Warning) + lines.add("${promotion.name} references unit type $unitType, which does not exist!", + RulesetErrorSeverity.Warning + ) checkUniques(promotion, lines, rulesetSpecific, tryFixUnknownUniques) checkPromotionCircularReferences(lines) } @@ -403,13 +421,19 @@ class RulesetValidator(val ruleset: Ruleset) { for (victoryType in ruleset.victories.values) { for (requiredUnit in victoryType.requiredSpaceshipParts) if (!ruleset.units.contains(requiredUnit)) - lines.add("Victory type ${victoryType.name} requires adding the non-existant unit $requiredUnit to the capital to win!", RulesetErrorSeverity.Warning) + lines.add("Victory type ${victoryType.name} requires adding the non-existant unit $requiredUnit to the capital to win!", + RulesetErrorSeverity.Warning + ) for (milestone in victoryType.milestoneObjects) if (milestone.type == null) - lines.add("Victory type ${victoryType.name} has milestone ${milestone.uniqueDescription} that is of an unknown type!", RulesetErrorSeverity.Error) + lines.add("Victory type ${victoryType.name} has milestone ${milestone.uniqueDescription} that is of an unknown type!", + RulesetErrorSeverity.Error + ) for (victory in ruleset.victories.values) if (victory.name != victoryType.name && victory.milestones == victoryType.milestones) - lines.add("Victory types ${victoryType.name} and ${victory.name} have the same requirements!", RulesetErrorSeverity.Warning) + lines.add("Victory types ${victoryType.name} and ${victory.name} have the same requirements!", + RulesetErrorSeverity.Warning + ) } for (difficulty in ruleset.difficulties.values) { @@ -436,7 +460,9 @@ class RulesetValidator(val ruleset: Ruleset) { private fun checkPromotionCircularReferences(lines: RulesetErrorList) { fun recursiveCheck(history: LinkedHashSet, promotion: Promotion, level: Int) { if (promotion in history) { - lines.add("Circular Reference in Promotions: ${history.joinToString("→") { it.name }}→${promotion.name}", RulesetErrorSeverity.Warning) + lines.add("Circular Reference in Promotions: ${history.joinToString("→") { it.name }}→${promotion.name}", + RulesetErrorSeverity.Warning + ) return } if (level > 99) return @@ -487,11 +513,13 @@ class RulesetValidator(val ruleset: Ruleset) { val typeComplianceErrors = unique.type.getComplianceErrors(unique, ruleset) for (complianceError in typeComplianceErrors) { if (complianceError.errorSeverity <= severityToReport) - rulesetErrors.add(RulesetError("$prefix unique \"${unique.text}\" contains parameter ${complianceError.parameterName}," + + rulesetErrors.add( + RulesetError("$prefix unique \"${unique.text}\" contains parameter ${complianceError.parameterName}," + " which does not fit parameter type" + " ${complianceError.acceptableParameterTypes.joinToString(" or ") { it.parameterName }} !", complianceError.errorSeverity.getRulesetErrorSeverity(severityToReport) - )) + ) + ) } for (conditional in unique.conditionals) { @@ -505,17 +533,20 @@ class RulesetValidator(val ruleset: Ruleset) { if (conditional.type.targetTypes.none { it.modifierType != UniqueTarget.ModifierType.None }) rulesetErrors.add("$prefix unique \"${unique.text}\" contains the conditional \"${conditional.text}\"," + " which is a Unique type not allowed as conditional or trigger.", - RulesetErrorSeverity.Warning) + RulesetErrorSeverity.Warning + ) val conditionalComplianceErrors = conditional.type.getComplianceErrors(conditional, ruleset) for (complianceError in conditionalComplianceErrors) { if (complianceError.errorSeverity == severityToReport) - rulesetErrors.add(RulesetError( "$prefix unique \"${unique.text}\" contains the conditional \"${conditional.text}\"." + + rulesetErrors.add( + RulesetError( "$prefix unique \"${unique.text}\" contains the conditional \"${conditional.text}\"." + " This contains the parameter ${complianceError.parameterName} which does not fit parameter type" + " ${complianceError.acceptableParameterTypes.joinToString(" or ") { it.parameterName }} !", complianceError.errorSeverity.getRulesetErrorSeverity(severityToReport) - )) + ) + ) } } } @@ -546,9 +577,12 @@ class RulesetValidator(val ruleset: Ruleset) { private fun checkUntypedUnique(unique: Unique, tryFixUnknownUniques: Boolean, prefix: String ): List { // Malformed conditional is always bad if (unique.text.count { it == '<' } != unique.text.count { it == '>' }) - return listOf(RulesetError( + return listOf( + RulesetError( "$prefix unique \"${unique.text}\" contains mismatched conditional braces!", - RulesetErrorSeverity.Warning)) + RulesetErrorSeverity.Warning + ) + ) // Support purely filtering Uniques without actual implementation if (isFilteringUniqueAllowed(unique)) return emptyList() @@ -559,9 +593,12 @@ class RulesetValidator(val ruleset: Ruleset) { if (RulesetCache.modCheckerAllowUntypedUniques) return emptyList() - return listOf(RulesetError( + return listOf( + RulesetError( "$prefix unique \"${unique.text}\" not found in Unciv's unique types.", - RulesetErrorSeverity.WarningOptionsOnly)) + RulesetErrorSeverity.WarningOptionsOnly + ) + ) } private fun isFilteringUniqueAllowed(unique: Unique): Boolean { @@ -582,9 +619,12 @@ class RulesetValidator(val ruleset: Ruleset) { similarUniques.filter { it.placeholderText == unique.placeholderText } return when { // This should only ever happen if a bug is or has been introduced that prevents Unique.type from being set for a valid UniqueType, I think.\ - equalUniques.isNotEmpty() -> listOf(RulesetError( + equalUniques.isNotEmpty() -> listOf( + RulesetError( "$prefix unique \"${unique.text}\" looks like it should be fine, but for some reason isn't recognized.", - RulesetErrorSeverity.OK)) + RulesetErrorSeverity.OK + ) + ) similarUniques.isNotEmpty() -> { val text = @@ -603,64 +643,3 @@ class RulesetValidator(val ruleset: Ruleset) { } } } - - -class RulesetError(val text: String, val errorSeverityToReport: RulesetErrorSeverity) - -enum class RulesetErrorSeverity(val color: Color) { - OK(Color.GREEN), - WarningOptionsOnly(Color.YELLOW), - Warning(Color.YELLOW), - Error(Color.RED), -} - -class RulesetErrorList : ArrayList() { - operator fun plusAssign(text: String) { - add(text, RulesetErrorSeverity.Error) - } - - fun add(text: String, errorSeverityToReport: RulesetErrorSeverity) { - add(RulesetError(text, errorSeverityToReport)) - } - - override fun add(element: RulesetError): Boolean { - // Suppress duplicates due to the double run of some checks for invariant/specific, - // Without changing collection type or making RulesetError obey the equality contract - val existing = firstOrNull { it.text == element.text } - ?: return super.add(element) - if (existing.errorSeverityToReport >= element.errorSeverityToReport) return false - remove(existing) - return super.add(element) - } - - override fun addAll(elements: Collection): Boolean { - var result = false - for (element in elements) - if (add(element)) result = true - return result - } - - 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 - /** @return `true` means problems exist, Options screen mod checker or unit tests for vanilla ruleset should complain */ - 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 - - fun getErrorText(unfiltered: Boolean = false) = - getErrorText { unfiltered || it.errorSeverityToReport != RulesetErrorSeverity.WarningOptionsOnly } - fun getErrorText(filter: (RulesetError)->Boolean) = - filter(filter) - .sortedByDescending { it.errorSeverityToReport } - .joinToString("\n") { - it.errorSeverityToReport.name + ": " + - // This will go through tr(), unavoidably, which will move the conditionals - // out of place. Prevent via kludge: - it.text.replace('<','〈').replace('>','〉') - } -} diff --git a/core/src/com/unciv/models/ruleset/TextSimilarity.kt b/core/src/com/unciv/models/ruleset/validation/TextSimilarity.kt similarity index 98% rename from core/src/com/unciv/models/ruleset/TextSimilarity.kt rename to core/src/com/unciv/models/ruleset/validation/TextSimilarity.kt index 6bcbd90bbe..4311a3d5c0 100644 --- a/core/src/com/unciv/models/ruleset/TextSimilarity.kt +++ b/core/src/com/unciv/models/ruleset/validation/TextSimilarity.kt @@ -1,4 +1,4 @@ -package com.unciv.models.ruleset +package com.unciv.models.ruleset.validation /** * Algorithm: diff --git a/core/src/com/unciv/models/stats/INamed.kt b/core/src/com/unciv/models/stats/INamed.kt index c15a0fb017..cd6c6d8894 100644 --- a/core/src/com/unciv/models/stats/INamed.kt +++ b/core/src/com/unciv/models/stats/INamed.kt @@ -1,5 +1,8 @@ package com.unciv.models.stats interface INamed { + // This is a var because unit tests set it (see `createRulesetObject` in TestGame.kt) + // As of 2023-08-08 no core code modifies a name! + // The main source of names are RuleSet json files, and Json deserialization can set a val just fine var name: String } diff --git a/core/src/com/unciv/models/stats/NamedStats.kt b/core/src/com/unciv/models/stats/NamedStats.kt index af9111cbde..82e3bf8fef 100644 --- a/core/src/com/unciv/models/stats/NamedStats.kt +++ b/core/src/com/unciv/models/stats/NamedStats.kt @@ -7,7 +7,7 @@ open class NamedStats : Stats(), INamed { override fun toString(): String { return name } - + fun cloneStats(): Stats { return clone() } diff --git a/core/src/com/unciv/ui/popups/options/ModCheckTab.kt b/core/src/com/unciv/ui/popups/options/ModCheckTab.kt index 92ad598823..dad575d9d6 100644 --- a/core/src/com/unciv/ui/popups/options/ModCheckTab.kt +++ b/core/src/com/unciv/ui/popups/options/ModCheckTab.kt @@ -7,9 +7,9 @@ import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.utils.Align import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.RulesetCache -import com.unciv.models.ruleset.RulesetError -import com.unciv.models.ruleset.RulesetErrorSeverity -import com.unciv.models.ruleset.RulesetValidator +import com.unciv.models.ruleset.validation.RulesetError +import com.unciv.models.ruleset.validation.RulesetErrorSeverity +import com.unciv.models.ruleset.validation.RulesetValidator import com.unciv.models.ruleset.unique.Unique import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.translations.tr diff --git a/core/src/com/unciv/ui/screens/newgamescreen/NewGameModCheckHelpers.kt b/core/src/com/unciv/ui/screens/newgamescreen/NewGameModCheckHelpers.kt index d2c7423a0e..713dc6b071 100644 --- a/core/src/com/unciv/ui/screens/newgamescreen/NewGameModCheckHelpers.kt +++ b/core/src/com/unciv/ui/screens/newgamescreen/NewGameModCheckHelpers.kt @@ -1,6 +1,6 @@ package com.unciv.ui.screens.newgamescreen -import com.unciv.models.ruleset.RulesetErrorList +import com.unciv.models.ruleset.validation.RulesetErrorList import com.unciv.models.translations.tr import com.unciv.ui.popups.ToastPopup import com.unciv.ui.popups.popups