mirror of
https://github.com/yairm210/Unciv.git
synced 2025-09-27 13:55:54 -04:00
Ruleset validator: Tilesets (#9881)
* Defensive Tileset loading * Deleting a mod should do the same cleanup as downloading a new one * Extensive RulesetValidator checks on Tileset mod integrity
This commit is contained in:
parent
903fb2c097
commit
8576f16c6e
@ -90,6 +90,8 @@ object Constants {
|
|||||||
const val uncivXyzServer = "https://uncivserver.xyz"
|
const val uncivXyzServer = "https://uncivserver.xyz"
|
||||||
|
|
||||||
const val defaultTileset = "HexaRealm"
|
const val defaultTileset = "HexaRealm"
|
||||||
|
/** Default for TileSetConfig.fallbackTileSet - Don't change unless you've also moved the crosshatch, borders, and arrows as well */
|
||||||
|
const val defaultFallbackTileset = "FantasyHex"
|
||||||
const val defaultUnitset = "AbsoluteUnits"
|
const val defaultUnitset = "AbsoluteUnits"
|
||||||
const val defaultSkin = "Minimal"
|
const val defaultSkin = "Minimal"
|
||||||
|
|
||||||
|
@ -1,8 +1,13 @@
|
|||||||
package com.unciv.models.ruleset.validation
|
package com.unciv.models.ruleset.validation
|
||||||
|
|
||||||
|
import com.badlogic.gdx.Gdx
|
||||||
import com.badlogic.gdx.graphics.Color
|
import com.badlogic.gdx.graphics.Color
|
||||||
|
import com.badlogic.gdx.graphics.g2d.TextureAtlas.TextureAtlasData
|
||||||
import com.unciv.Constants
|
import com.unciv.Constants
|
||||||
|
import com.unciv.json.fromJsonFile
|
||||||
|
import com.unciv.json.json
|
||||||
import com.unciv.logic.map.tile.RoadStatus
|
import com.unciv.logic.map.tile.RoadStatus
|
||||||
|
import com.unciv.models.metadata.BaseRuleset
|
||||||
import com.unciv.models.ruleset.IRulesetObject
|
import com.unciv.models.ruleset.IRulesetObject
|
||||||
import com.unciv.models.ruleset.Ruleset
|
import com.unciv.models.ruleset.Ruleset
|
||||||
import com.unciv.models.ruleset.RulesetCache
|
import com.unciv.models.ruleset.RulesetCache
|
||||||
@ -17,6 +22,8 @@ import com.unciv.models.ruleset.unique.UniqueType
|
|||||||
import com.unciv.models.ruleset.unit.Promotion
|
import com.unciv.models.ruleset.unit.Promotion
|
||||||
import com.unciv.models.stats.INamed
|
import com.unciv.models.stats.INamed
|
||||||
import com.unciv.models.stats.Stats
|
import com.unciv.models.stats.Stats
|
||||||
|
import com.unciv.models.tilesets.TileSetCache
|
||||||
|
import com.unciv.models.tilesets.TileSetConfig
|
||||||
|
|
||||||
class RulesetValidator(val ruleset: Ruleset) {
|
class RulesetValidator(val ruleset: Ruleset) {
|
||||||
|
|
||||||
@ -70,20 +77,17 @@ class RulesetValidator(val ruleset: Ruleset) {
|
|||||||
lines += "Tech Column number ${techColumn.columnNumber} is negative"
|
lines += "Tech Column number ${techColumn.columnNumber} is negative"
|
||||||
if (techColumn.buildingCost == -1)
|
if (techColumn.buildingCost == -1)
|
||||||
lines.add("Tech Column number ${techColumn.columnNumber} has no explicit building cost",
|
lines.add("Tech Column number ${techColumn.columnNumber} has no explicit building cost",
|
||||||
RulesetErrorSeverity.Warning
|
RulesetErrorSeverity.Warning)
|
||||||
)
|
|
||||||
if (techColumn.wonderCost == -1)
|
if (techColumn.wonderCost == -1)
|
||||||
lines.add("Tech Column number ${techColumn.columnNumber} has no explicit wonder cost",
|
lines.add("Tech Column number ${techColumn.columnNumber} has no explicit wonder cost",
|
||||||
RulesetErrorSeverity.Warning
|
RulesetErrorSeverity.Warning)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (building in ruleset.buildings.values) {
|
for (building in ruleset.buildings.values) {
|
||||||
if (building.requiredTech == null && building.cost == -1 && !building.hasUnique(
|
if (building.requiredTech == null && building.cost == -1 && !building.hasUnique(
|
||||||
UniqueType.Unbuildable))
|
UniqueType.Unbuildable))
|
||||||
lines.add("${building.name} is buildable and therefore should either have an explicit cost or reference an existing tech!",
|
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)
|
checkUniques(building, lines, rulesetInvariant, tryFixUnknownUniques)
|
||||||
|
|
||||||
@ -148,6 +152,13 @@ class RulesetValidator(val ruleset: Ruleset) {
|
|||||||
checkUniques(resource, lines, rulesetInvariant, tryFixUnknownUniques)
|
checkUniques(resource, lines, rulesetInvariant, tryFixUnknownUniques)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/********************** Tileset tests **********************/
|
||||||
|
// e.g. json configs complete and parseable
|
||||||
|
// Check for mod or Civ_V_GnK to avoid running the same test twice (~200ms for the builtin assets)
|
||||||
|
if (ruleset.folderLocation != null || ruleset.name == BaseRuleset.Civ_V_GnK.fullName) {
|
||||||
|
checkTilesetSanity(lines)
|
||||||
|
}
|
||||||
|
|
||||||
// Quit here when no base ruleset is loaded - references cannot be checked
|
// Quit here when no base ruleset is loaded - references cannot be checked
|
||||||
if (!ruleset.modOptions.isBaseRuleset) return lines
|
if (!ruleset.modOptions.isBaseRuleset) return lines
|
||||||
|
|
||||||
@ -199,8 +210,7 @@ class RulesetValidator(val ruleset: Ruleset) {
|
|||||||
unit.isCivilian() &&
|
unit.isCivilian() &&
|
||||||
!unit.isGreatPersonOfType("War")) {
|
!unit.isGreatPersonOfType("War")) {
|
||||||
lines.add("${unit.name} can place improvement $improvementName which has no stats, preventing unit automation!",
|
lines.add("${unit.name} can place improvement $improvementName which has no stats, preventing unit automation!",
|
||||||
RulesetErrorSeverity.Warning
|
RulesetErrorSeverity.Warning)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -295,8 +305,7 @@ class RulesetValidator(val ruleset: Ruleset) {
|
|||||||
if (tech.prerequisites.asSequence().filterNot { it == prereq }
|
if (tech.prerequisites.asSequence().filterNot { it == prereq }
|
||||||
.any { getPrereqTree(it).contains(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!",
|
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))
|
if (getPrereqTree(prereq).contains(tech.name))
|
||||||
@ -343,13 +352,10 @@ class RulesetValidator(val ruleset: Ruleset) {
|
|||||||
|
|
||||||
if (era.allyBonus.isNotEmpty())
|
if (era.allyBonus.isNotEmpty())
|
||||||
lines.add("Era ${era.name} contains city-state bonuses. City-state bonuses are now defined in CityStateType.json",
|
lines.add("Era ${era.name} contains city-state bonuses. City-state bonuses are now defined in CityStateType.json",
|
||||||
RulesetErrorSeverity.WarningOptionsOnly
|
RulesetErrorSeverity.WarningOptionsOnly)
|
||||||
)
|
|
||||||
if (era.friendBonus.isNotEmpty())
|
if (era.friendBonus.isNotEmpty())
|
||||||
lines.add("Era ${era.name} contains city-state bonuses. City-state bonuses are now defined in CityStateType.json",
|
lines.add("Era ${era.name} contains city-state bonuses. City-state bonuses are now defined in CityStateType.json",
|
||||||
RulesetErrorSeverity.WarningOptionsOnly
|
RulesetErrorSeverity.WarningOptionsOnly)
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
checkUniques(era, lines, rulesetSpecific, tryFixUnknownUniques)
|
checkUniques(era, lines, rulesetSpecific, tryFixUnknownUniques)
|
||||||
}
|
}
|
||||||
@ -403,13 +409,11 @@ class RulesetValidator(val ruleset: Ruleset) {
|
|||||||
for (prereq in promotion.prerequisites)
|
for (prereq in promotion.prerequisites)
|
||||||
if (!ruleset.unitPromotions.containsKey(prereq))
|
if (!ruleset.unitPromotions.containsKey(prereq))
|
||||||
lines.add("${promotion.name} requires promotion $prereq which does not exist!",
|
lines.add("${promotion.name} requires promotion $prereq which does not exist!",
|
||||||
RulesetErrorSeverity.Warning
|
RulesetErrorSeverity.Warning)
|
||||||
)
|
|
||||||
for (unitType in promotion.unitTypes)
|
for (unitType in promotion.unitTypes)
|
||||||
if (!ruleset.unitTypes.containsKey(unitType) && (ruleset.unitTypes.isNotEmpty() || !vanillaRuleset.unitTypes.containsKey(unitType)))
|
if (!ruleset.unitTypes.containsKey(unitType) && (ruleset.unitTypes.isNotEmpty() || !vanillaRuleset.unitTypes.containsKey(unitType)))
|
||||||
lines.add("${promotion.name} references unit type $unitType, which does not exist!",
|
lines.add("${promotion.name} references unit type $unitType, which does not exist!",
|
||||||
RulesetErrorSeverity.Warning
|
RulesetErrorSeverity.Warning)
|
||||||
)
|
|
||||||
checkUniques(promotion, lines, rulesetSpecific, tryFixUnknownUniques)
|
checkUniques(promotion, lines, rulesetSpecific, tryFixUnknownUniques)
|
||||||
checkPromotionCircularReferences(lines)
|
checkPromotionCircularReferences(lines)
|
||||||
}
|
}
|
||||||
@ -422,18 +426,15 @@ class RulesetValidator(val ruleset: Ruleset) {
|
|||||||
for (requiredUnit in victoryType.requiredSpaceshipParts)
|
for (requiredUnit in victoryType.requiredSpaceshipParts)
|
||||||
if (!ruleset.units.contains(requiredUnit))
|
if (!ruleset.units.contains(requiredUnit))
|
||||||
lines.add("Victory type ${victoryType.name} requires adding the non-existant unit $requiredUnit to the capital to win!",
|
lines.add("Victory type ${victoryType.name} requires adding the non-existant unit $requiredUnit to the capital to win!",
|
||||||
RulesetErrorSeverity.Warning
|
RulesetErrorSeverity.Warning)
|
||||||
)
|
|
||||||
for (milestone in victoryType.milestoneObjects)
|
for (milestone in victoryType.milestoneObjects)
|
||||||
if (milestone.type == null)
|
if (milestone.type == null)
|
||||||
lines.add("Victory type ${victoryType.name} has milestone ${milestone.uniqueDescription} that is of an unknown type!",
|
lines.add("Victory type ${victoryType.name} has milestone ${milestone.uniqueDescription} that is of an unknown type!",
|
||||||
RulesetErrorSeverity.Error
|
RulesetErrorSeverity.Error)
|
||||||
)
|
|
||||||
for (victory in ruleset.victories.values)
|
for (victory in ruleset.victories.values)
|
||||||
if (victory.name != victoryType.name && victory.milestones == victoryType.milestones)
|
if (victory.name != victoryType.name && victory.milestones == victoryType.milestones)
|
||||||
lines.add("Victory types ${victoryType.name} and ${victory.name} have the same requirements!",
|
lines.add("Victory types ${victoryType.name} and ${victory.name} have the same requirements!",
|
||||||
RulesetErrorSeverity.Warning
|
RulesetErrorSeverity.Warning)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (difficulty in ruleset.difficulties.values) {
|
for (difficulty in ruleset.difficulties.values) {
|
||||||
@ -457,12 +458,73 @@ class RulesetValidator(val ruleset: Ruleset) {
|
|||||||
return lines
|
return lines
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun checkTilesetSanity(lines: RulesetErrorList) {
|
||||||
|
val tilesetConfigFolder = (ruleset.folderLocation ?: Gdx.files.internal("")).child("jsons\\TileSets")
|
||||||
|
if (!tilesetConfigFolder.exists()) return
|
||||||
|
|
||||||
|
val configTilesets = mutableSetOf<String>()
|
||||||
|
val allFallbacks = mutableSetOf<String>()
|
||||||
|
val folderContent = tilesetConfigFolder.list()
|
||||||
|
var folderContentBad = false
|
||||||
|
|
||||||
|
for (file in folderContent) {
|
||||||
|
if (file.isDirectory || file.extension() != "json") { folderContentBad = true; continue }
|
||||||
|
// All json files should be parseable
|
||||||
|
try {
|
||||||
|
val config = json().fromJsonFile(TileSetConfig::class.java, file)
|
||||||
|
configTilesets += file.nameWithoutExtension().removeSuffix("Config")
|
||||||
|
if (config.fallbackTileSet?.isNotEmpty() == true)
|
||||||
|
allFallbacks.add(config.fallbackTileSet!!)
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
// Our fromJsonFile wrapper already intercepts Exceptions and gives them a generalized message, so go a level deeper for useful details (like "unmatched brace")
|
||||||
|
lines.add("Tileset config '${file.name()}' cannot be loaded (${ex.cause?.message})", RulesetErrorSeverity.Warning)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Folder should not contain subdirectories, non-json files, or be empty
|
||||||
|
if (folderContentBad)
|
||||||
|
lines.add("The Mod tileset config folder contains non-json files or subdirectories", RulesetErrorSeverity.Warning)
|
||||||
|
if (configTilesets.isEmpty())
|
||||||
|
lines.add("The Mod tileset config folder contains no json files", RulesetErrorSeverity.Warning)
|
||||||
|
|
||||||
|
// There should be atlas images corresponding to each json name
|
||||||
|
val atlasTilesets = getTilesetNamesFromAtlases()
|
||||||
|
val configOnlyTilesets = configTilesets - atlasTilesets
|
||||||
|
if (configOnlyTilesets.isNotEmpty())
|
||||||
|
lines.add("Mod has no graphics for configured tilesets: ${configOnlyTilesets.joinToString()}", RulesetErrorSeverity.Warning)
|
||||||
|
|
||||||
|
// For all atlas images matching "TileSets/*" there should be a json
|
||||||
|
val atlasOnlyTilesets = atlasTilesets - configTilesets
|
||||||
|
if (atlasOnlyTilesets.isNotEmpty())
|
||||||
|
lines.add("Mod has no configuration for tileset graphics: ${atlasOnlyTilesets.joinToString()}", RulesetErrorSeverity.Warning)
|
||||||
|
|
||||||
|
// All fallbacks should exist (default added because TileSetCache is not loaded when running as unit test)
|
||||||
|
val unknownFallbacks = allFallbacks - TileSetCache.keys - Constants.defaultFallbackTileset
|
||||||
|
if (unknownFallbacks.isNotEmpty())
|
||||||
|
lines.add("Fallback tileset invalid: ${unknownFallbacks.joinToString()}", RulesetErrorSeverity.Warning)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getTilesetNamesFromAtlases(): Set<String> {
|
||||||
|
// This partially duplicates code in ImageGetter.getAvailableTilesets, but we don't want to reload that singleton cache.
|
||||||
|
|
||||||
|
// Our builtin rulesets have no folderLocation, in that case cheat and apply knowledge about
|
||||||
|
// where the builtin Tileset textures are (correct would be to parse Atlases.json):
|
||||||
|
val files = ruleset.folderLocation?.list("atlas")?.asSequence()
|
||||||
|
?: sequenceOf(Gdx.files.internal("Tilesets.atlas"))
|
||||||
|
// Next, we need to cope with this running without GL context (unit test) - no TextureAtlas(file)
|
||||||
|
return files
|
||||||
|
.flatMap { file ->
|
||||||
|
TextureAtlasData(file, file.parent(), false).regions.asSequence()
|
||||||
|
.filter { it.name.startsWith("TileSets/") && !it.name.contains("/Units/") }
|
||||||
|
}.map { it.name.split("/")[1] }
|
||||||
|
.toSet()
|
||||||
|
}
|
||||||
|
|
||||||
private fun checkPromotionCircularReferences(lines: RulesetErrorList) {
|
private fun checkPromotionCircularReferences(lines: RulesetErrorList) {
|
||||||
fun recursiveCheck(history: LinkedHashSet<Promotion>, promotion: Promotion, level: Int) {
|
fun recursiveCheck(history: LinkedHashSet<Promotion>, promotion: Promotion, level: Int) {
|
||||||
if (promotion in history) {
|
if (promotion in history) {
|
||||||
lines.add("Circular Reference in Promotions: ${history.joinToString("→") { it.name }}→${promotion.name}",
|
lines.add("Circular Reference in Promotions: ${history.joinToString("→") { it.name }}→${promotion.name}",
|
||||||
RulesetErrorSeverity.Warning
|
RulesetErrorSeverity.Warning)
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (level > 99) return
|
if (level > 99) return
|
||||||
@ -513,13 +575,11 @@ class RulesetValidator(val ruleset: Ruleset) {
|
|||||||
val typeComplianceErrors = unique.type.getComplianceErrors(unique, ruleset)
|
val typeComplianceErrors = unique.type.getComplianceErrors(unique, ruleset)
|
||||||
for (complianceError in typeComplianceErrors) {
|
for (complianceError in typeComplianceErrors) {
|
||||||
if (complianceError.errorSeverity <= severityToReport)
|
if (complianceError.errorSeverity <= severityToReport)
|
||||||
rulesetErrors.add(
|
rulesetErrors.add(RulesetError("$prefix unique \"${unique.text}\" contains parameter ${complianceError.parameterName}," +
|
||||||
RulesetError("$prefix unique \"${unique.text}\" contains parameter ${complianceError.parameterName}," +
|
|
||||||
" which does not fit parameter type" +
|
" which does not fit parameter type" +
|
||||||
" ${complianceError.acceptableParameterTypes.joinToString(" or ") { it.parameterName }} !",
|
" ${complianceError.acceptableParameterTypes.joinToString(" or ") { it.parameterName }} !",
|
||||||
complianceError.errorSeverity.getRulesetErrorSeverity(severityToReport)
|
complianceError.errorSeverity.getRulesetErrorSeverity(severityToReport)
|
||||||
)
|
))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (conditional in unique.conditionals) {
|
for (conditional in unique.conditionals) {
|
||||||
@ -533,20 +593,17 @@ class RulesetValidator(val ruleset: Ruleset) {
|
|||||||
if (conditional.type.targetTypes.none { it.modifierType != UniqueTarget.ModifierType.None })
|
if (conditional.type.targetTypes.none { it.modifierType != UniqueTarget.ModifierType.None })
|
||||||
rulesetErrors.add("$prefix unique \"${unique.text}\" contains the conditional \"${conditional.text}\"," +
|
rulesetErrors.add("$prefix unique \"${unique.text}\" contains the conditional \"${conditional.text}\"," +
|
||||||
" which is a Unique type not allowed as conditional or trigger.",
|
" which is a Unique type not allowed as conditional or trigger.",
|
||||||
RulesetErrorSeverity.Warning
|
RulesetErrorSeverity.Warning)
|
||||||
)
|
|
||||||
|
|
||||||
val conditionalComplianceErrors =
|
val conditionalComplianceErrors =
|
||||||
conditional.type.getComplianceErrors(conditional, ruleset)
|
conditional.type.getComplianceErrors(conditional, ruleset)
|
||||||
for (complianceError in conditionalComplianceErrors) {
|
for (complianceError in conditionalComplianceErrors) {
|
||||||
if (complianceError.errorSeverity == severityToReport)
|
if (complianceError.errorSeverity == severityToReport)
|
||||||
rulesetErrors.add(
|
rulesetErrors.add(RulesetError( "$prefix unique \"${unique.text}\" contains the conditional \"${conditional.text}\"." +
|
||||||
RulesetError( "$prefix unique \"${unique.text}\" contains the conditional \"${conditional.text}\"." +
|
|
||||||
" This contains the parameter ${complianceError.parameterName} which does not fit parameter type" +
|
" This contains the parameter ${complianceError.parameterName} which does not fit parameter type" +
|
||||||
" ${complianceError.acceptableParameterTypes.joinToString(" or ") { it.parameterName }} !",
|
" ${complianceError.acceptableParameterTypes.joinToString(" or ") { it.parameterName }} !",
|
||||||
complianceError.errorSeverity.getRulesetErrorSeverity(severityToReport)
|
complianceError.errorSeverity.getRulesetErrorSeverity(severityToReport)
|
||||||
)
|
))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -577,12 +634,9 @@ class RulesetValidator(val ruleset: Ruleset) {
|
|||||||
private fun checkUntypedUnique(unique: Unique, tryFixUnknownUniques: Boolean, prefix: String ): List<RulesetError> {
|
private fun checkUntypedUnique(unique: Unique, tryFixUnknownUniques: Boolean, prefix: String ): List<RulesetError> {
|
||||||
// Malformed conditional is always bad
|
// Malformed conditional is always bad
|
||||||
if (unique.text.count { it == '<' } != unique.text.count { it == '>' })
|
if (unique.text.count { it == '<' } != unique.text.count { it == '>' })
|
||||||
return listOf(
|
return listOf(RulesetError(
|
||||||
RulesetError(
|
|
||||||
"$prefix unique \"${unique.text}\" contains mismatched conditional braces!",
|
"$prefix unique \"${unique.text}\" contains mismatched conditional braces!",
|
||||||
RulesetErrorSeverity.Warning
|
RulesetErrorSeverity.Warning))
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Support purely filtering Uniques without actual implementation
|
// Support purely filtering Uniques without actual implementation
|
||||||
if (isFilteringUniqueAllowed(unique)) return emptyList()
|
if (isFilteringUniqueAllowed(unique)) return emptyList()
|
||||||
@ -593,12 +647,9 @@ class RulesetValidator(val ruleset: Ruleset) {
|
|||||||
|
|
||||||
if (RulesetCache.modCheckerAllowUntypedUniques) return emptyList()
|
if (RulesetCache.modCheckerAllowUntypedUniques) return emptyList()
|
||||||
|
|
||||||
return listOf(
|
return listOf(RulesetError(
|
||||||
RulesetError(
|
|
||||||
"$prefix unique \"${unique.text}\" not found in Unciv's unique types.",
|
"$prefix unique \"${unique.text}\" not found in Unciv's unique types.",
|
||||||
RulesetErrorSeverity.WarningOptionsOnly
|
RulesetErrorSeverity.WarningOptionsOnly))
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isFilteringUniqueAllowed(unique: Unique): Boolean {
|
private fun isFilteringUniqueAllowed(unique: Unique): Boolean {
|
||||||
@ -619,12 +670,9 @@ class RulesetValidator(val ruleset: Ruleset) {
|
|||||||
similarUniques.filter { it.placeholderText == unique.placeholderText }
|
similarUniques.filter { it.placeholderText == unique.placeholderText }
|
||||||
return when {
|
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.\
|
// 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(
|
equalUniques.isNotEmpty() -> listOf(RulesetError(
|
||||||
RulesetError(
|
|
||||||
"$prefix unique \"${unique.text}\" looks like it should be fine, but for some reason isn't recognized.",
|
"$prefix unique \"${unique.text}\" looks like it should be fine, but for some reason isn't recognized.",
|
||||||
RulesetErrorSeverity.OK
|
RulesetErrorSeverity.OK))
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
similarUniques.isNotEmpty() -> {
|
similarUniques.isNotEmpty() -> {
|
||||||
val text =
|
val text =
|
||||||
|
@ -38,6 +38,12 @@ object TileSetCache : HashMap<String, TileSet>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Load the json config files and do an initial [assembleTileSetConfigs] run without explicit mods.
|
||||||
|
*
|
||||||
|
* Runs from UncivGame.create without exception handling, therefore should not be vulnerable to broken mods.
|
||||||
|
* (The critical point is json parsing in loadConfigFiles, which is now hardened)
|
||||||
|
* Also runs from ModManagementScreen after downloading a new mod or deleting one.
|
||||||
|
*/
|
||||||
fun loadTileSetConfigs(consoleMode: Boolean = false) {
|
fun loadTileSetConfigs(consoleMode: Boolean = false) {
|
||||||
|
|
||||||
clear()
|
clear()
|
||||||
@ -73,8 +79,18 @@ object TileSetCache : HashMap<String, TileSet>() {
|
|||||||
|
|
||||||
private fun loadConfigFiles(files: Sequence<FileHandle>, configId: String) {
|
private fun loadConfigFiles(files: Sequence<FileHandle>, configId: String) {
|
||||||
for (configFile in files) {
|
for (configFile in files) {
|
||||||
|
// jsons/TileSets shouldn't have subfolders, but if a mad modder has one, don't crash (file.readString would throw):
|
||||||
|
if (configFile.isDirectory) continue
|
||||||
|
// `files` is an unfiltered directory listing - ignore chaff
|
||||||
|
if (configFile.extension() != "json") continue
|
||||||
|
|
||||||
|
val config = try {
|
||||||
|
json().fromJsonFile(TileSetConfig::class.java, configFile)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
continue // Ignore jsons that don't load
|
||||||
|
}
|
||||||
|
|
||||||
val name = configFile.nameWithoutExtension().removeSuffix("Config")
|
val name = configFile.nameWithoutExtension().removeSuffix("Config")
|
||||||
val config = json().fromJsonFile(TileSetConfig::class.java, configFile)
|
|
||||||
val tileset = get(name) ?: TileSet(name)
|
val tileset = get(name) ?: TileSet(name)
|
||||||
tileset.cacheConfigFromMod(configId, config)
|
tileset.cacheConfigFromMod(configId, config)
|
||||||
set(name, tileset)
|
set(name, tileset)
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
package com.unciv.models.tilesets
|
package com.unciv.models.tilesets
|
||||||
|
|
||||||
import com.badlogic.gdx.graphics.Color
|
import com.badlogic.gdx.graphics.Color
|
||||||
|
import com.unciv.Constants
|
||||||
|
|
||||||
class TileSetConfig {
|
class TileSetConfig {
|
||||||
var useColorAsBaseTerrain = false
|
var useColorAsBaseTerrain = false
|
||||||
var useSummaryImages = false
|
var useSummaryImages = false
|
||||||
var unexploredTileColor: Color = Color.DARK_GRAY
|
var unexploredTileColor: Color = Color.DARK_GRAY
|
||||||
var fogOfWarColor: Color = Color.BLACK
|
var fogOfWarColor: Color = Color.BLACK
|
||||||
/** Name of the tileset to use when this one is missing images. Null to disable.
|
/** Name of the tileset to use when this one is missing images. Null to disable. */
|
||||||
* Don't change unless you've also moved the crosshatch, borders, and arrows as well */
|
var fallbackTileSet: String? = Constants.defaultFallbackTileset
|
||||||
var fallbackTileSet: String? = "FantasyHex"
|
|
||||||
/** Scale factor for hex images, with hex center as origin. */
|
/** Scale factor for hex images, with hex center as origin. */
|
||||||
var tileScale: Float = 1f
|
var tileScale: Float = 1f
|
||||||
var tileScales: HashMap<String, Float> = HashMap()
|
var tileScales: HashMap<String, Float> = HashMap()
|
||||||
|
@ -576,7 +576,7 @@ class ModManagementScreen(
|
|||||||
val repoName = modFolder.name() // repo.name still has the replaced "-"'s
|
val repoName = modFolder.name() // repo.name still has the replaced "-"'s
|
||||||
ToastPopup("[$repoName] Downloaded!", this@ModManagementScreen)
|
ToastPopup("[$repoName] Downloaded!", this@ModManagementScreen)
|
||||||
RulesetCache.loadRulesets()
|
RulesetCache.loadRulesets()
|
||||||
TileSetCache.loadTileSetConfigs(false)
|
TileSetCache.loadTileSetConfigs()
|
||||||
UncivGame.Current.translations.tryReadTranslationForCurrentLanguage()
|
UncivGame.Current.translations.tryReadTranslationForCurrentLanguage()
|
||||||
RulesetCache[repoName]?.let {
|
RulesetCache[repoName]?.let {
|
||||||
installedModInfo[repoName] = ModUIData(it)
|
installedModInfo[repoName] = ModUIData(it)
|
||||||
@ -720,6 +720,7 @@ class ModManagementScreen(
|
|||||||
private fun deleteMod(mod: Ruleset) {
|
private fun deleteMod(mod: Ruleset) {
|
||||||
mod.folderLocation!!.deleteDirectory()
|
mod.folderLocation!!.deleteDirectory()
|
||||||
RulesetCache.loadRulesets()
|
RulesetCache.loadRulesets()
|
||||||
|
TileSetCache.loadTileSetConfigs()
|
||||||
installedModInfo.remove(mod.name)
|
installedModInfo.remove(mod.name)
|
||||||
refreshInstalledModTable()
|
refreshInstalledModTable()
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user