mirror of
https://github.com/yairm210/Unciv.git
synced 2025-01-05 21:11:35 +07:00
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:
parent
7b778f7535
commit
4fac033704
@ -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
|
||||
|
||||
|
46
core/src/com/unciv/models/ruleset/ModOptions.kt
Normal file
46
core/src/com/unciv/models/ruleset/ModOptions.kt
Normal 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()
|
||||
}
|
@ -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>()
|
||||
}
|
||||
|
182
core/src/com/unciv/models/ruleset/RulesetCache.kt
Normal file
182
core/src/com/unciv/models/ruleset/RulesetCache.kt
Normal 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) }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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>()
|
||||
}
|
||||
|
11
core/src/com/unciv/models/ruleset/Specialist.kt
Normal file
11
core/src/com/unciv/models/ruleset/Specialist.kt
Normal 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>()
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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('>','〉')
|
||||
}
|
||||
}
|
@ -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('>','〉')
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package com.unciv.models.ruleset
|
||||
package com.unciv.models.ruleset.validation
|
||||
|
||||
/**
|
||||
* Algorithm:
|
@ -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
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ open class NamedStats : Stats(), INamed {
|
||||
override fun toString(): String {
|
||||
return name
|
||||
}
|
||||
|
||||
|
||||
fun cloneStats(): Stats {
|
||||
return clone()
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user