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:
SomeTroglodyte 2023-12-22 08:59:38 +01:00 committed by GitHub
parent 746918b1d3
commit f529d969f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 211 additions and 50 deletions

View File

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

View File

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

View File

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

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

View File

@ -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)
@ -62,6 +63,7 @@ class RulesetValidator(val ruleset: Ruleset) {
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!"

View File

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

View File

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

View File

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