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
This commit is contained in:
SomeTroglodyte 2023-08-08 22:06:33 +02:00 committed by GitHub
parent 7b778f7535
commit 4fac033704
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 419 additions and 342 deletions

View File

@ -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

View File

@ -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<String>()
var buildingsToRemove = HashSet<String>()
var unitsToRemove = HashSet<String>()
var nationsToRemove = HashSet<String>()
var lastUpdated = ""
var modUrl = ""
var defaultBranch = "master"
var author = ""
var modSize = 0
var topics = mutableListOf<String>()
override var uniques = ArrayList<String>()
@delegate:Transient
override val uniqueObjects: List<Unique> by lazy (::uniqueObjectsProvider)
@delegate:Transient
override val uniqueMap: UniqueMap by lazy(::uniqueMapProvider)
override fun getUniqueTarget() = UniqueTarget.ModOptions
val constants = ModConstants()
}

View File

@ -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<String>()
var buildingsToRemove = HashSet<String>()
var unitsToRemove = HashSet<String>()
var nationsToRemove = HashSet<String>()
var lastUpdated = ""
var modUrl = ""
var defaultBranch = "master"
var author = ""
var modSize = 0
var topics = mutableListOf<String>()
override var uniques = ArrayList<String>()
// 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<Unique> = listOf()
override var uniqueMap: Map<String, List<Unique>> = 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<String, Belief>()
val buildings = LinkedHashMap<String, Building>()
val difficulties = LinkedHashMap<String, Difficulty>()
@ -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<String,Ruleset>() {
/** 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<String> {
val newRulesets = HashMap<String, Ruleset>()
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<String>()
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<String> {
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<String>, 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>): 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<String>,
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<Int>()
val colorObject by lazy { colorFromRGB(color) }
var greatPersonPoints = Counter<String>()
}

View File

@ -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<String, Ruleset>() {
/** 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<String> {
val newRulesets = HashMap<String, Ruleset>()
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<String>()
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<String> {
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<String>, 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>): 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<String>,
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) }
}
}
}

View File

@ -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<String>() // Can not be a hashset as that would remove doubles
@delegate:Transient
override val uniqueObjects: List<Unique> by lazy {
if (uniques.isEmpty()) emptyList()
else uniques.map { Unique(it, getUniqueTarget(), name) }
}
override val uniqueObjects: List<Unique> 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<FormattedLine>()
override fun toString() = name
@ -38,15 +29,9 @@ abstract class RulesetStatsObject: NamedStats(), IRulesetObject {
override var originRuleset = ""
override var uniques = ArrayList<String>() // Can not be a hashset as that would remove doubles
@delegate:Transient
override val uniqueObjects: List<Unique> by lazy {
if (uniques.isEmpty()) emptyList()
else uniques.map { Unique(it, getUniqueTarget(), name) }
}
override val uniqueObjects: List<Unique> by lazy (::uniqueObjectsProvider)
@delegate:Transient
override val uniqueMap: Map<String, List<Unique>> by lazy {
if (uniques.isEmpty()) emptyMap()
else uniqueObjects.groupBy { it.placeholderText }
}
override val uniqueMap: UniqueMap by lazy(::uniqueMapProvider)
override var civilopediaText = listOf<FormattedLine>()
}

View File

@ -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<Int>()
val colorObject by lazy { colorFromRGB(color) }
var greatPersonPoints = Counter<String>()
}

View File

@ -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<String> // 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<Unique>
// Every implementation should override these with the same `by lazy (::thingsProvider)`
// AND every implementation should annotate these with `@delegate:Transient`
val uniqueObjects: List<Unique>
val uniqueMap: Map<String, List<Unique>>
fun uniqueObjectsProvider(): List<Unique> {
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()
}

View File

@ -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

View File

@ -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

View File

@ -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<RulesetError>() {
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<RulesetError>): 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('>','〉')
}
}

View File

@ -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: 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<RulesetError> {
// 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<RulesetError>() {
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<RulesetError>): 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('>','〉')
}
}

View File

@ -1,4 +1,4 @@
package com.unciv.models.ruleset
package com.unciv.models.ruleset.validation
/**
* Algorithm:

View File

@ -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
}

View File

@ -7,7 +7,7 @@ open class NamedStats : Stats(), INamed {
override fun toString(): String {
return name
}
fun cloneStats(): Stats {
return clone()
}

View File

@ -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

View File

@ -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