mirror of
https://github.com/yairm210/Unciv.git
synced 2025-09-24 03:53:12 -04:00
Mod declarative compatibility - a little more (#10751)
* Mod compatibility - update declarations * Mod compatibility - logic and UI changes * Mod compatibility - flag some invalid use patterns * RulesetValidator - lint until Studio shuts up * Fix isBaseRuleset test in ModRequires validation --------- Co-authored-by: Yair Morgenstern <yairm210@hotmail.com>
This commit is contained in:
parent
746918b1d3
commit
f529d969f0
@ -179,6 +179,14 @@
|
|||||||
"name": "Checkbox-pressed",
|
"name": "Checkbox-pressed",
|
||||||
"color": "color"
|
"color": "color"
|
||||||
},
|
},
|
||||||
|
"checkbox-disabled-c": {
|
||||||
|
"name": "Checkbox",
|
||||||
|
"color": "disabled"
|
||||||
|
},
|
||||||
|
"checkbox-pressed-disabled-c": {
|
||||||
|
"name": "Checkbox-pressed",
|
||||||
|
"color": "disabled"
|
||||||
|
},
|
||||||
"list-c": {
|
"list-c": {
|
||||||
"name": "RectangleWithOutline",
|
"name": "RectangleWithOutline",
|
||||||
"color": "color"
|
"color": "color"
|
||||||
@ -258,7 +266,9 @@
|
|||||||
"com.badlogic.gdx.scenes.scene2d.ui.CheckBox$CheckBoxStyle": {
|
"com.badlogic.gdx.scenes.scene2d.ui.CheckBox$CheckBoxStyle": {
|
||||||
"default": {
|
"default": {
|
||||||
"checkboxOn": "checkbox-pressed-c",
|
"checkboxOn": "checkbox-pressed-c",
|
||||||
|
"checkboxOnDisabled": "checkbox-pressed-disabled-c",
|
||||||
"checkboxOff": "checkbox-c",
|
"checkboxOff": "checkbox-c",
|
||||||
|
"checkboxOffDisabled": "checkbox-disabled-c",
|
||||||
"font": "button",
|
"font": "button",
|
||||||
"fontColor": "color",
|
"fontColor": "color",
|
||||||
"downFontColor": "pressed",
|
"downFontColor": "pressed",
|
||||||
|
@ -572,7 +572,7 @@ enum class UniqueParameterType(
|
|||||||
override fun getTranslationWriterStringsForOutput() = knownValues
|
override fun getTranslationWriterStringsForOutput() = knownValues
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Mod declarative compatibility: Behaves like [Unknown], but makes for nicer auto-generated documentation. */
|
/** Mod declarative compatibility: Define Mod relations by their name. */
|
||||||
ModName("modFilter", "DeCiv Redux", """A Mod name, case-sensitive _or_ a simple wildcard filter beginning and ending in an Asterisk, case-insensitive""", "Mod name filter") {
|
ModName("modFilter", "DeCiv Redux", """A Mod name, case-sensitive _or_ a simple wildcard filter beginning and ending in an Asterisk, case-insensitive""", "Mod name filter") {
|
||||||
override fun getErrorSeverity(parameterText: String, ruleset: Ruleset):
|
override fun getErrorSeverity(parameterText: String, ruleset: Ruleset):
|
||||||
UniqueType.UniqueParameterErrorSeverity? =
|
UniqueType.UniqueParameterErrorSeverity? =
|
||||||
|
@ -795,8 +795,14 @@ enum class UniqueType(
|
|||||||
Comment("Comment [comment]", *UniqueTarget.Displayable,
|
Comment("Comment [comment]", *UniqueTarget.Displayable,
|
||||||
docDescription = "Allows displaying arbitrary text in a Unique listing. Only the text within the '[]' brackets will be displayed, the rest serves to allow Ruleset validation to recognize the intent."),
|
docDescription = "Allows displaying arbitrary text in a Unique listing. Only the text within the '[]' brackets will be displayed, the rest serves to allow Ruleset validation to recognize the intent."),
|
||||||
|
|
||||||
// Declarative Mod compatibility (so far rudimentary):
|
// Declarative Mod compatibility (see [ModCompatibility]):
|
||||||
ModIncompatibleWith("Mod is incompatible with [modFilter]", UniqueTarget.ModOptions),
|
// Note there is currently no display for these, but UniqueFlag.HiddenToUsers is not set.
|
||||||
|
// That means we auto-template and ask our translators for a translation that is currently unused.
|
||||||
|
//todo To think over - leave as is for future use or remove templates and translations by adding the flag?
|
||||||
|
ModIncompatibleWith("Mod is incompatible with [modFilter]", UniqueTarget.ModOptions,
|
||||||
|
docDescription = "Specifies that your Mod is incompatible with another. Always treated symmetrically, and cannot be overridden by the Mod you are declaring as incompatible."),
|
||||||
|
ModRequires("Mod requires [modFilter]", UniqueTarget.ModOptions,
|
||||||
|
docDescription = "Specifies that your Extension Mod is only available if any other Mod matching the filter is active."),
|
||||||
ModIsAudioVisualOnly("Should only be used as permanent audiovisual mod", UniqueTarget.ModOptions),
|
ModIsAudioVisualOnly("Should only be used as permanent audiovisual mod", UniqueTarget.ModOptions),
|
||||||
ModIsAudioVisual("Can be used as permanent audiovisual mod", UniqueTarget.ModOptions),
|
ModIsAudioVisual("Can be used as permanent audiovisual mod", UniqueTarget.ModOptions),
|
||||||
ModIsNotAudioVisual("Cannot be used as permanent audiovisual mod", UniqueTarget.ModOptions),
|
ModIsNotAudioVisual("Cannot be used as permanent audiovisual mod", UniqueTarget.ModOptions),
|
||||||
|
119
core/src/com/unciv/models/ruleset/validation/ModCompatibility.kt
Normal file
119
core/src/com/unciv/models/ruleset/validation/ModCompatibility.kt
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
package com.unciv.models.ruleset.validation
|
||||||
|
|
||||||
|
import com.badlogic.gdx.files.FileHandle
|
||||||
|
import com.unciv.models.ruleset.Ruleset
|
||||||
|
import com.unciv.models.ruleset.RulesetCache
|
||||||
|
import com.unciv.models.ruleset.unique.UniqueType
|
||||||
|
import com.unciv.models.ruleset.validation.ModCompatibility.meetsAllRequirements
|
||||||
|
import com.unciv.models.ruleset.validation.ModCompatibility.meetsBaseRequirements
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper collection dealing with declarative Mod compatibility
|
||||||
|
*
|
||||||
|
* Implements:
|
||||||
|
* - [UniqueType.ModRequires]
|
||||||
|
* - [UniqueType.ModIncompatibleWith]
|
||||||
|
* - [UniqueType.ModIsAudioVisual]
|
||||||
|
* - [UniqueType.ModIsNotAudioVisual]
|
||||||
|
* - [UniqueType.ModIsAudioVisualOnly]
|
||||||
|
*
|
||||||
|
* Methods:
|
||||||
|
* - [meetsBaseRequirements] - to build a checkbox list of Extension mods
|
||||||
|
* - [meetsAllRequirements] - to see if a mod is allowed in the context of a complete mod selection
|
||||||
|
*/
|
||||||
|
object ModCompatibility {
|
||||||
|
/**
|
||||||
|
* Should the "Permanent Audiovisual Mod" checkbox be shown for [mod]?
|
||||||
|
*
|
||||||
|
* Note: The guessing part may potentially be deprecated and removed if we get our Modders to complete declarative coverage.
|
||||||
|
*/
|
||||||
|
fun isAudioVisualMod(mod: Ruleset) = isAudioVisualDeclared(mod) ?: isAudioVisualGuessed(mod)
|
||||||
|
|
||||||
|
private fun isAudioVisualDeclared(mod: Ruleset): Boolean? {
|
||||||
|
if (mod.modOptions.hasUnique(UniqueType.ModIsAudioVisualOnly)) return true
|
||||||
|
if (mod.modOptions.hasUnique(UniqueType.ModIsAudioVisual)) return true
|
||||||
|
if (mod.modOptions.hasUnique(UniqueType.ModIsNotAudioVisual)) return false
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there's media (audio folders or any atlas), show the PAV choice...
|
||||||
|
private fun isAudioVisualGuessed(mod: Ruleset): Boolean {
|
||||||
|
val folder = mod.folderLocation ?: return false // Also catches isBuiltin
|
||||||
|
fun isSubFolderNotEmpty(modFolder: FileHandle, name: String): Boolean {
|
||||||
|
val file = modFolder.child(name)
|
||||||
|
if (!file.exists()) return false
|
||||||
|
if (!file.isDirectory) return false
|
||||||
|
return file.list().isNotEmpty()
|
||||||
|
}
|
||||||
|
if (isSubFolderNotEmpty(folder, "music")) return true
|
||||||
|
if (isSubFolderNotEmpty(folder, "sounds")) return true
|
||||||
|
if (isSubFolderNotEmpty(folder, "voices")) return true
|
||||||
|
return folder.list("atlas").isNotEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isExtensionMod(mod: Ruleset) =
|
||||||
|
!mod.modOptions.isBaseRuleset
|
||||||
|
&& mod.name.isNotBlank()
|
||||||
|
&& !mod.modOptions.hasUnique(UniqueType.ModIsAudioVisualOnly)
|
||||||
|
|
||||||
|
private fun modNameFilter(modName: String, filter: String): Boolean {
|
||||||
|
if (modName == filter) return true
|
||||||
|
if (filter.length < 3 || !filter.startsWith('*') || !filter.endsWith('*')) return false
|
||||||
|
val partialName = filter.substring(1, filter.length - 1).lowercase()
|
||||||
|
return partialName in modName.lowercase()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isIncompatibleWith(mod: Ruleset, otherMod: Ruleset) =
|
||||||
|
mod.modOptions.getMatchingUniques(UniqueType.ModIncompatibleWith)
|
||||||
|
.any { modNameFilter(otherMod.name, it.params[0]) }
|
||||||
|
|
||||||
|
private fun isIncompatible(mod: Ruleset, otherMod: Ruleset) =
|
||||||
|
isIncompatibleWith(mod, otherMod) || isIncompatibleWith(otherMod, mod)
|
||||||
|
|
||||||
|
/** Implement [UniqueType.ModRequires] and [UniqueType.ModIncompatibleWith]
|
||||||
|
* for selecting extension mods to show - after a [baseRuleset] was chosen.
|
||||||
|
*
|
||||||
|
* - Extension mod is incompatible with [baseRuleset] -> Nope
|
||||||
|
* - Extension mod has no ModRequires unique -> OK
|
||||||
|
* - For each ModRequires: Not ([baseRuleset] meets filter OR any other cached _extension_ mod meets filter) -> Nope
|
||||||
|
* - All ModRequires tested -> OK
|
||||||
|
*/
|
||||||
|
fun meetsBaseRequirements(mod: Ruleset, baseRuleset: Ruleset): Boolean {
|
||||||
|
if (isIncompatible(mod, baseRuleset)) return false
|
||||||
|
|
||||||
|
val allOtherExtensionModNames = RulesetCache.values.asSequence()
|
||||||
|
.filter { it != mod && !it.modOptions.isBaseRuleset && it.name.isNotEmpty() }
|
||||||
|
.map { it.name }
|
||||||
|
.toList()
|
||||||
|
|
||||||
|
for (unique in mod.modOptions.getMatchingUniques(UniqueType.ModRequires)) {
|
||||||
|
val filter = unique.params[0]
|
||||||
|
if (modNameFilter(baseRuleset.name, filter)) continue
|
||||||
|
if (allOtherExtensionModNames.none { modNameFilter(it, filter) }) return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Implement [UniqueType.ModRequires] and [UniqueType.ModIncompatibleWith]
|
||||||
|
* for _enabling_ shown extension mods depending on other extension choices
|
||||||
|
*
|
||||||
|
* @param selectedExtensionMods all "active" mods for the compatibility tests - including the testee [mod] itself in this is allowed, it will be ignored. Will be iterated only once.
|
||||||
|
*
|
||||||
|
* - No need to test: Extension mod is incompatible with [baseRuleset] - we expect [meetsBaseRequirements] did exclude it from the UI entirely
|
||||||
|
* - Extension mod is incompatible with any _other_ **selected** extension mod -> Nope
|
||||||
|
* - Extension mod has no ModRequires unique -> OK
|
||||||
|
* - For each ModRequires: Not([baseRuleset] meets filter OR any other **selected** extension mod meets filter) -> Nope
|
||||||
|
* - All ModRequires tested -> OK
|
||||||
|
*/
|
||||||
|
fun meetsAllRequirements(mod: Ruleset, baseRuleset: Ruleset, selectedExtensionMods: Iterable<Ruleset>): Boolean {
|
||||||
|
val otherSelectedExtensionMods = selectedExtensionMods.filterNot { it == mod }.toList()
|
||||||
|
if (otherSelectedExtensionMods.any { isIncompatible(mod, it) }) return false
|
||||||
|
|
||||||
|
for (unique in mod.modOptions.getMatchingUniques(UniqueType.ModRequires)) {
|
||||||
|
val filter = unique.params[0]
|
||||||
|
if (modNameFilter(baseRuleset.name, filter)) continue
|
||||||
|
if (otherSelectedExtensionMods.none { modNameFilter(it.name, filter) }) return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
@ -39,6 +39,7 @@ class RulesetValidator(val ruleset: Ruleset) {
|
|||||||
val lines = RulesetErrorList()
|
val lines = RulesetErrorList()
|
||||||
|
|
||||||
// When not checking the entire ruleset, we can only really detect ruleset-invariant errors in uniques
|
// When not checking the entire ruleset, we can only really detect ruleset-invariant errors in uniques
|
||||||
|
addModOptionsErrors(lines)
|
||||||
uniqueValidator.checkUniques(ruleset.globalUniques, lines, false, tryFixUnknownUniques)
|
uniqueValidator.checkUniques(ruleset.globalUniques, lines, false, tryFixUnknownUniques)
|
||||||
addUnitErrorsRulesetInvariant(lines, tryFixUnknownUniques)
|
addUnitErrorsRulesetInvariant(lines, tryFixUnknownUniques)
|
||||||
addTechErrorsRulesetInvariant(lines, tryFixUnknownUniques)
|
addTechErrorsRulesetInvariant(lines, tryFixUnknownUniques)
|
||||||
@ -57,11 +58,12 @@ class RulesetValidator(val ruleset: Ruleset) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun getBaseRulesetErrorList(tryFixUnknownUniques: Boolean): RulesetErrorList{
|
private fun getBaseRulesetErrorList(tryFixUnknownUniques: Boolean): RulesetErrorList {
|
||||||
|
|
||||||
uniqueValidator.populateFilteringUniqueHashsets()
|
uniqueValidator.populateFilteringUniqueHashsets()
|
||||||
|
|
||||||
val lines = RulesetErrorList()
|
val lines = RulesetErrorList()
|
||||||
|
addModOptionsErrors(lines)
|
||||||
uniqueValidator.checkUniques(ruleset.globalUniques, lines, true, tryFixUnknownUniques)
|
uniqueValidator.checkUniques(ruleset.globalUniques, lines, true, tryFixUnknownUniques)
|
||||||
|
|
||||||
addUnitErrorsBaseRuleset(lines, tryFixUnknownUniques)
|
addUnitErrorsBaseRuleset(lines, tryFixUnknownUniques)
|
||||||
@ -81,7 +83,7 @@ class RulesetValidator(val ruleset: Ruleset) {
|
|||||||
addPromotionErrors(lines, tryFixUnknownUniques)
|
addPromotionErrors(lines, tryFixUnknownUniques)
|
||||||
addUnitTypeErrors(lines, tryFixUnknownUniques)
|
addUnitTypeErrors(lines, tryFixUnknownUniques)
|
||||||
addVictoryTypeErrors(lines)
|
addVictoryTypeErrors(lines)
|
||||||
addDifficutlyErrors(lines)
|
addDifficultyErrors(lines)
|
||||||
addCityStateTypeErrors(tryFixUnknownUniques, lines)
|
addCityStateTypeErrors(tryFixUnknownUniques, lines)
|
||||||
|
|
||||||
// Check for mod or Civ_V_GnK to avoid running the same test twice (~200ms for the builtin assets)
|
// Check for mod or Civ_V_GnK to avoid running the same test twice (~200ms for the builtin assets)
|
||||||
@ -92,6 +94,22 @@ class RulesetValidator(val ruleset: Ruleset) {
|
|||||||
return lines
|
return lines
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun addModOptionsErrors(lines: RulesetErrorList) {
|
||||||
|
if (ruleset.name.isBlank()) return // These tests don't make sense for combined rulesets
|
||||||
|
|
||||||
|
val audioVisualUniqueTypes = setOf(
|
||||||
|
UniqueType.ModIsAudioVisual,
|
||||||
|
UniqueType.ModIsAudioVisualOnly,
|
||||||
|
UniqueType.ModIsNotAudioVisual
|
||||||
|
)
|
||||||
|
if (ruleset.modOptions.uniqueObjects.count { it.type in audioVisualUniqueTypes } > 1)
|
||||||
|
lines += "A mod should only specify one of the 'can/should/cannot be used as permanent audiovisual mod' options."
|
||||||
|
if (!ruleset.modOptions.isBaseRuleset) return
|
||||||
|
for (unique in ruleset.modOptions.getMatchingUniques(UniqueType.ModRequires)) {
|
||||||
|
lines += "Mod option '${unique.text}' is invalid for a base ruleset."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun addCityStateTypeErrors(
|
private fun addCityStateTypeErrors(
|
||||||
tryFixUnknownUniques: Boolean,
|
tryFixUnknownUniques: Boolean,
|
||||||
lines: RulesetErrorList
|
lines: RulesetErrorList
|
||||||
@ -109,7 +127,7 @@ class RulesetValidator(val ruleset: Ruleset) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addDifficutlyErrors(lines: RulesetErrorList) {
|
private fun addDifficultyErrors(lines: RulesetErrorList) {
|
||||||
for (difficulty in ruleset.difficulties.values) {
|
for (difficulty in ruleset.difficulties.values) {
|
||||||
for (unitName in difficulty.aiCityStateBonusStartingUnits + difficulty.aiMajorCivBonusStartingUnits + difficulty.playerBonusStartingUnits)
|
for (unitName in difficulty.aiCityStateBonusStartingUnits + difficulty.aiMajorCivBonusStartingUnits + difficulty.playerBonusStartingUnits)
|
||||||
if (unitName != Constants.eraSpecificUnit && !ruleset.units.containsKey(unitName))
|
if (unitName != Constants.eraSpecificUnit && !ruleset.units.containsKey(unitName))
|
||||||
@ -179,6 +197,7 @@ class RulesetValidator(val ruleset: Ruleset) {
|
|||||||
tryFixUnknownUniques: Boolean
|
tryFixUnknownUniques: Boolean
|
||||||
) {
|
) {
|
||||||
for (reward in ruleset.ruinRewards.values) {
|
for (reward in ruleset.ruinRewards.values) {
|
||||||
|
@Suppress("KotlinConstantConditions") // data is read from json, so any assumptions may be wrong
|
||||||
if (reward.weight < 0) lines += "${reward.name} has a negative weight, which is not allowed!"
|
if (reward.weight < 0) lines += "${reward.name} has a negative weight, which is not allowed!"
|
||||||
for (difficulty in reward.excludedDifficulties)
|
for (difficulty in reward.excludedDifficulties)
|
||||||
if (!ruleset.difficulties.containsKey(difficulty))
|
if (!ruleset.difficulties.containsKey(difficulty))
|
||||||
@ -439,7 +458,7 @@ class RulesetValidator(val ruleset: Ruleset) {
|
|||||||
|
|
||||||
for (requiredTech: String in building.requiredTechs())
|
for (requiredTech: String in building.requiredTechs())
|
||||||
if (!ruleset.technologies.containsKey(requiredTech))
|
if (!ruleset.technologies.containsKey(requiredTech))
|
||||||
lines += "${building.name} requires tech ${requiredTech} which does not exist!"
|
lines += "${building.name} requires tech $requiredTech which does not exist!"
|
||||||
for (specialistName in building.specialistSlots.keys)
|
for (specialistName in building.specialistSlots.keys)
|
||||||
if (!ruleset.specialists.containsKey(specialistName))
|
if (!ruleset.specialists.containsKey(specialistName))
|
||||||
lines += "${building.name} provides specialist $specialistName which does not exist!"
|
lines += "${building.name} provides specialist $specialistName which does not exist!"
|
||||||
@ -660,7 +679,7 @@ class RulesetValidator(val ruleset: Ruleset) {
|
|||||||
private fun checkUnitRulesetSpecific(unit: BaseUnit, lines: RulesetErrorList) {
|
private fun checkUnitRulesetSpecific(unit: BaseUnit, lines: RulesetErrorList) {
|
||||||
for (requiredTech: String in unit.requiredTechs())
|
for (requiredTech: String in unit.requiredTechs())
|
||||||
if (!ruleset.technologies.containsKey(requiredTech))
|
if (!ruleset.technologies.containsKey(requiredTech))
|
||||||
lines += "${unit.name} requires tech ${requiredTech} which does not exist!"
|
lines += "${unit.name} requires tech $requiredTech which does not exist!"
|
||||||
for (obsoleteTech: String in unit.techsAtWhichNoLongerAvailable())
|
for (obsoleteTech: String in unit.techsAtWhichNoLongerAvailable())
|
||||||
if (!ruleset.technologies.containsKey(obsoleteTech))
|
if (!ruleset.technologies.containsKey(obsoleteTech))
|
||||||
lines += "${unit.name} obsoletes at tech ${obsoleteTech} which does not exist!"
|
lines += "${unit.name} obsoletes at tech ${obsoleteTech} which does not exist!"
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
package com.unciv.ui.screens.modmanager
|
package com.unciv.ui.screens.modmanager
|
||||||
|
|
||||||
import com.badlogic.gdx.Gdx
|
import com.badlogic.gdx.Gdx
|
||||||
import com.badlogic.gdx.files.FileHandle
|
|
||||||
import com.badlogic.gdx.graphics.Texture
|
import com.badlogic.gdx.graphics.Texture
|
||||||
import com.badlogic.gdx.scenes.scene2d.ui.Image
|
import com.badlogic.gdx.scenes.scene2d.ui.Image
|
||||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||||
import com.unciv.models.metadata.BaseRuleset
|
import com.unciv.models.metadata.BaseRuleset
|
||||||
import com.unciv.models.ruleset.Ruleset
|
import com.unciv.models.ruleset.Ruleset
|
||||||
import com.unciv.models.ruleset.unique.UniqueType
|
import com.unciv.models.ruleset.validation.ModCompatibility
|
||||||
import com.unciv.models.translations.tr
|
import com.unciv.models.translations.tr
|
||||||
import com.unciv.ui.components.extensions.UncivDateFormat.formatDate
|
import com.unciv.ui.components.extensions.UncivDateFormat.formatDate
|
||||||
import com.unciv.ui.components.extensions.UncivDateFormat.parseDate
|
import com.unciv.ui.components.extensions.UncivDateFormat.parseDate
|
||||||
@ -52,7 +51,7 @@ internal class ModInfoAndActionPane : Table() {
|
|||||||
val modName = mod.name
|
val modName = mod.name
|
||||||
val modOptions = mod.modOptions // The ModOptions as enriched by us with GitHub metadata when originally downloaded
|
val modOptions = mod.modOptions // The ModOptions as enriched by us with GitHub metadata when originally downloaded
|
||||||
isBuiltin = modOptions.modUrl.isEmpty() && BaseRuleset.values().any { it.fullName == modName }
|
isBuiltin = modOptions.modUrl.isEmpty() && BaseRuleset.values().any { it.fullName == modName }
|
||||||
enableVisualCheckBox = shouldShowVisualCheckbox(mod)
|
enableVisualCheckBox = ModCompatibility.isAudioVisualMod(mod)
|
||||||
update(
|
update(
|
||||||
modName, modOptions.modUrl, modOptions.defaultBranch,
|
modName, modOptions.modUrl, modOptions.defaultBranch,
|
||||||
modOptions.lastUpdated, modOptions.author, modOptions.modSize
|
modOptions.lastUpdated, modOptions.author, modOptions.modSize
|
||||||
@ -170,26 +169,4 @@ internal class ModInfoAndActionPane : Table() {
|
|||||||
cell.size(texture.width * resizeRatio, texture.height * resizeRatio)
|
cell.size(texture.width * resizeRatio, texture.height * resizeRatio)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun shouldShowVisualCheckbox(mod: Ruleset): Boolean {
|
|
||||||
val folder = mod.folderLocation ?: return false // Also catches isBuiltin
|
|
||||||
|
|
||||||
// Check declared Mod Compatibility
|
|
||||||
if (mod.modOptions.hasUnique(UniqueType.ModIsAudioVisualOnly)) return true
|
|
||||||
if (mod.modOptions.hasUnique(UniqueType.ModIsAudioVisual)) return true
|
|
||||||
if (mod.modOptions.hasUnique(UniqueType.ModIsNotAudioVisual)) return false
|
|
||||||
|
|
||||||
// The following is the "guessing" part: If there's media, show the PAV choice...
|
|
||||||
// Might be deprecated if declarative Mod compatibility succeeds
|
|
||||||
fun isSubFolderNotEmpty(modFolder: FileHandle, name: String): Boolean {
|
|
||||||
val file = modFolder.child(name)
|
|
||||||
if (!file.exists()) return false
|
|
||||||
if (!file.isDirectory) return false
|
|
||||||
return file.list().isNotEmpty()
|
|
||||||
}
|
|
||||||
if (isSubFolderNotEmpty(folder, "music")) return true
|
|
||||||
if (isSubFolderNotEmpty(folder, "sounds")) return true
|
|
||||||
if (isSubFolderNotEmpty(folder, "voices")) return true
|
|
||||||
return folder.list("atlas").isNotEmpty()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ import com.badlogic.gdx.scenes.scene2d.ui.Table
|
|||||||
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener
|
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener
|
||||||
import com.unciv.models.ruleset.Ruleset
|
import com.unciv.models.ruleset.Ruleset
|
||||||
import com.unciv.models.ruleset.RulesetCache
|
import com.unciv.models.ruleset.RulesetCache
|
||||||
import com.unciv.models.ruleset.unique.UniqueType
|
import com.unciv.models.ruleset.validation.ModCompatibility
|
||||||
import com.unciv.ui.components.extensions.pad
|
import com.unciv.ui.components.extensions.pad
|
||||||
import com.unciv.ui.components.extensions.toCheckBox
|
import com.unciv.ui.components.extensions.toCheckBox
|
||||||
import com.unciv.ui.components.input.onChange
|
import com.unciv.ui.components.input.onChange
|
||||||
@ -49,10 +49,8 @@ class ModCheckboxTable(
|
|||||||
private val expanderPadTop = if (isPortrait) 0f else 16f
|
private val expanderPadTop = if (isPortrait) 0f else 16f
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val modRulesets = RulesetCache.values.filterNot {
|
val modRulesets = RulesetCache.values.filter {
|
||||||
it.modOptions.isBaseRuleset
|
ModCompatibility.isExtensionMod(it)
|
||||||
|| it.name.isBlank()
|
|
||||||
|| it.modOptions.hasUnique(UniqueType.ModIsAudioVisualOnly)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (mod in modRulesets.sortedBy { it.name }) {
|
for (mod in modRulesets.sortedBy { it.name }) {
|
||||||
@ -77,7 +75,7 @@ class ModCheckboxTable(
|
|||||||
baseRuleset = RulesetCache[newBaseRuleset] ?: return
|
baseRuleset = RulesetCache[newBaseRuleset] ?: return
|
||||||
|
|
||||||
val compatibleMods = modWidgets
|
val compatibleMods = modWidgets
|
||||||
.filterNot { isIncompatible(it.mod, baseRuleset) }
|
.filter { ModCompatibility.meetsBaseRequirements(it.mod, baseRuleset) }
|
||||||
|
|
||||||
if (compatibleMods.none()) return
|
if (compatibleMods.none()) return
|
||||||
|
|
||||||
@ -91,7 +89,8 @@ class ModCheckboxTable(
|
|||||||
it.add(mod.widget).row()
|
it.add(mod.widget).row()
|
||||||
}
|
}
|
||||||
}).pad(10f).padTop(expanderPadTop).growX().row()
|
}).pad(10f).padTop(expanderPadTop).growX().row()
|
||||||
// I think it's not necessary to uncheck the imcompatible (now invisible) checkBoxes
|
|
||||||
|
disableIncompatibleMods()
|
||||||
|
|
||||||
runComplexModCheck()
|
runComplexModCheck()
|
||||||
}
|
}
|
||||||
@ -103,6 +102,8 @@ class ModCheckboxTable(
|
|||||||
}
|
}
|
||||||
mods.clear()
|
mods.clear()
|
||||||
disableChangeEvents = false
|
disableChangeEvents = false
|
||||||
|
|
||||||
|
disableIncompatibleMods()
|
||||||
onUpdate("-") // should match no mod
|
onUpdate("-") // should match no mod
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,6 +115,7 @@ class ModCheckboxTable(
|
|||||||
// Check over complete combination of selected mods
|
// Check over complete combination of selected mods
|
||||||
val complexModLinkCheck = RulesetCache.checkCombinedModLinks(mods, baseRulesetName)
|
val complexModLinkCheck = RulesetCache.checkCombinedModLinks(mods, baseRulesetName)
|
||||||
if (!complexModLinkCheck.isWarnUser()){
|
if (!complexModLinkCheck.isWarnUser()){
|
||||||
|
savedModcheckResult = null
|
||||||
Gdx.input.inputProcessor = currentInputProcessor
|
Gdx.input.inputProcessor = currentInputProcessor
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -167,20 +169,41 @@ class ModCheckboxTable(
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
disableIncompatibleMods()
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun modNameFilter(modName: String, filter: String): Boolean {
|
/** Deselect incompatible mods after [skipCheckBox] was selected.
|
||||||
if (modName == filter) return true
|
*
|
||||||
if (filter.length < 3 || !filter.startsWith('*') || !filter.endsWith('*')) return false
|
* Note: Inactive - we don'n even allow a conflict to be turned on using [disableIncompatibleMods].
|
||||||
val partialName = filter.substring(1, filter.length - 1).lowercase()
|
* But if we want the alternative UX instead - use this in [checkBoxChanged] near `mods.add` and skip disabling...
|
||||||
return partialName in modName.lowercase()
|
*/
|
||||||
|
@Suppress("unused")
|
||||||
|
private fun deselectIncompatibleMods(skipCheckBox: CheckBox) {
|
||||||
|
disableChangeEvents = true
|
||||||
|
for (modWidget in modWidgets) {
|
||||||
|
if (modWidget.widget == skipCheckBox) continue
|
||||||
|
if (!ModCompatibility.meetsAllRequirements(modWidget.mod, baseRuleset, getSelectedMods())) {
|
||||||
|
modWidget.widget.isChecked = false
|
||||||
|
mods.remove(modWidget.mod.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
disableChangeEvents = true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isIncompatibleWith(mod: Ruleset, otherMod: Ruleset) =
|
/** Disable incompatible mods - those that could not be turned on with the current selection */
|
||||||
mod.modOptions.getMatchingUniques(UniqueType.ModIncompatibleWith)
|
private fun disableIncompatibleMods() {
|
||||||
.any { modNameFilter(otherMod.name, it.params[0]) }
|
for (modWidget in modWidgets) {
|
||||||
private fun isIncompatible(mod: Ruleset, otherMod: Ruleset) =
|
val enable = ModCompatibility.meetsAllRequirements(modWidget.mod, baseRuleset, getSelectedMods())
|
||||||
isIncompatibleWith(mod, otherMod) || isIncompatibleWith(otherMod, mod)
|
assert(enable || !modWidget.widget.isChecked) { "Mod compatibility conflict: Trying to disable ${modWidget.mod.name} while it is selected" }
|
||||||
|
modWidget.widget.isDisabled = !enable // isEnabled is only for TextButtons
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getSelectedMods() =
|
||||||
|
modWidgets.asSequence()
|
||||||
|
.filter { it.widget.isChecked }
|
||||||
|
.map { it.mod }
|
||||||
|
.asIterable()
|
||||||
}
|
}
|
||||||
|
@ -1779,10 +1779,17 @@ Simple unique parameters are explained by mouseover. Complex parameters are expl
|
|||||||
|
|
||||||
## ModOptions uniques
|
## ModOptions uniques
|
||||||
??? example "Mod is incompatible with [modFilter]"
|
??? example "Mod is incompatible with [modFilter]"
|
||||||
|
Specifies that your Mod is incompatible with another. Always treated symmetrically, and cannot be overridden by the Mod you are declaring as incompatible.
|
||||||
Example: "Mod is incompatible with [DeCiv Redux]"
|
Example: "Mod is incompatible with [DeCiv Redux]"
|
||||||
|
|
||||||
Applicable to: ModOptions
|
Applicable to: ModOptions
|
||||||
|
|
||||||
|
??? example "Mod requires [modFilter]"
|
||||||
|
Specifies that your Extension Mod is only available if any other Mod matching the filter is active.
|
||||||
|
Example: "Mod requires [DeCiv Redux]"
|
||||||
|
|
||||||
|
Applicable to: ModOptions
|
||||||
|
|
||||||
??? example "Should only be used as permanent audiovisual mod"
|
??? example "Should only be used as permanent audiovisual mod"
|
||||||
Applicable to: ModOptions
|
Applicable to: ModOptions
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user