mirror of
https://github.com/yairm210/Unciv.git
synced 2025-09-23 03:23:17 -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",
|
||||
"color": "color"
|
||||
},
|
||||
"checkbox-disabled-c": {
|
||||
"name": "Checkbox",
|
||||
"color": "disabled"
|
||||
},
|
||||
"checkbox-pressed-disabled-c": {
|
||||
"name": "Checkbox-pressed",
|
||||
"color": "disabled"
|
||||
},
|
||||
"list-c": {
|
||||
"name": "RectangleWithOutline",
|
||||
"color": "color"
|
||||
@ -258,7 +266,9 @@
|
||||
"com.badlogic.gdx.scenes.scene2d.ui.CheckBox$CheckBoxStyle": {
|
||||
"default": {
|
||||
"checkboxOn": "checkbox-pressed-c",
|
||||
"checkboxOnDisabled": "checkbox-pressed-disabled-c",
|
||||
"checkboxOff": "checkbox-c",
|
||||
"checkboxOffDisabled": "checkbox-disabled-c",
|
||||
"font": "button",
|
||||
"fontColor": "color",
|
||||
"downFontColor": "pressed",
|
||||
|
@ -572,7 +572,7 @@ enum class UniqueParameterType(
|
||||
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") {
|
||||
override fun getErrorSeverity(parameterText: String, ruleset: Ruleset):
|
||||
UniqueType.UniqueParameterErrorSeverity? =
|
||||
|
@ -795,8 +795,14 @@ enum class UniqueType(
|
||||
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."),
|
||||
|
||||
// Declarative Mod compatibility (so far rudimentary):
|
||||
ModIncompatibleWith("Mod is incompatible with [modFilter]", UniqueTarget.ModOptions),
|
||||
// Declarative Mod compatibility (see [ModCompatibility]):
|
||||
// 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),
|
||||
ModIsAudioVisual("Can 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()
|
||||
|
||||
// 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)
|
||||
addUnitErrorsRulesetInvariant(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()
|
||||
|
||||
val lines = RulesetErrorList()
|
||||
addModOptionsErrors(lines)
|
||||
uniqueValidator.checkUniques(ruleset.globalUniques, lines, true, tryFixUnknownUniques)
|
||||
|
||||
addUnitErrorsBaseRuleset(lines, tryFixUnknownUniques)
|
||||
@ -81,7 +83,7 @@ class RulesetValidator(val ruleset: Ruleset) {
|
||||
addPromotionErrors(lines, tryFixUnknownUniques)
|
||||
addUnitTypeErrors(lines, tryFixUnknownUniques)
|
||||
addVictoryTypeErrors(lines)
|
||||
addDifficutlyErrors(lines)
|
||||
addDifficultyErrors(lines)
|
||||
addCityStateTypeErrors(tryFixUnknownUniques, lines)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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(
|
||||
tryFixUnknownUniques: Boolean,
|
||||
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 (unitName in difficulty.aiCityStateBonusStartingUnits + difficulty.aiMajorCivBonusStartingUnits + difficulty.playerBonusStartingUnits)
|
||||
if (unitName != Constants.eraSpecificUnit && !ruleset.units.containsKey(unitName))
|
||||
@ -179,6 +197,7 @@ class RulesetValidator(val ruleset: Ruleset) {
|
||||
tryFixUnknownUniques: Boolean
|
||||
) {
|
||||
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!"
|
||||
for (difficulty in reward.excludedDifficulties)
|
||||
if (!ruleset.difficulties.containsKey(difficulty))
|
||||
@ -439,7 +458,7 @@ class RulesetValidator(val ruleset: Ruleset) {
|
||||
|
||||
for (requiredTech: String in building.requiredTechs())
|
||||
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)
|
||||
if (!ruleset.specialists.containsKey(specialistName))
|
||||
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) {
|
||||
for (requiredTech: String in unit.requiredTechs())
|
||||
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())
|
||||
if (!ruleset.technologies.containsKey(obsoleteTech))
|
||||
lines += "${unit.name} obsoletes at tech ${obsoleteTech} which does not exist!"
|
||||
|
@ -1,13 +1,12 @@
|
||||
package com.unciv.ui.screens.modmanager
|
||||
|
||||
import com.badlogic.gdx.Gdx
|
||||
import com.badlogic.gdx.files.FileHandle
|
||||
import com.badlogic.gdx.graphics.Texture
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Image
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||
import com.unciv.models.metadata.BaseRuleset
|
||||
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.ui.components.extensions.UncivDateFormat.formatDate
|
||||
import com.unciv.ui.components.extensions.UncivDateFormat.parseDate
|
||||
@ -52,7 +51,7 @@ internal class ModInfoAndActionPane : Table() {
|
||||
val modName = mod.name
|
||||
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 }
|
||||
enableVisualCheckBox = shouldShowVisualCheckbox(mod)
|
||||
enableVisualCheckBox = ModCompatibility.isAudioVisualMod(mod)
|
||||
update(
|
||||
modName, modOptions.modUrl, modOptions.defaultBranch,
|
||||
modOptions.lastUpdated, modOptions.author, modOptions.modSize
|
||||
@ -170,26 +169,4 @@ internal class ModInfoAndActionPane : Table() {
|
||||
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.unciv.models.ruleset.Ruleset
|
||||
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.toCheckBox
|
||||
import com.unciv.ui.components.input.onChange
|
||||
@ -49,10 +49,8 @@ class ModCheckboxTable(
|
||||
private val expanderPadTop = if (isPortrait) 0f else 16f
|
||||
|
||||
init {
|
||||
val modRulesets = RulesetCache.values.filterNot {
|
||||
it.modOptions.isBaseRuleset
|
||||
|| it.name.isBlank()
|
||||
|| it.modOptions.hasUnique(UniqueType.ModIsAudioVisualOnly)
|
||||
val modRulesets = RulesetCache.values.filter {
|
||||
ModCompatibility.isExtensionMod(it)
|
||||
}
|
||||
|
||||
for (mod in modRulesets.sortedBy { it.name }) {
|
||||
@ -77,7 +75,7 @@ class ModCheckboxTable(
|
||||
baseRuleset = RulesetCache[newBaseRuleset] ?: return
|
||||
|
||||
val compatibleMods = modWidgets
|
||||
.filterNot { isIncompatible(it.mod, baseRuleset) }
|
||||
.filter { ModCompatibility.meetsBaseRequirements(it.mod, baseRuleset) }
|
||||
|
||||
if (compatibleMods.none()) return
|
||||
|
||||
@ -91,7 +89,8 @@ class ModCheckboxTable(
|
||||
it.add(mod.widget).row()
|
||||
}
|
||||
}).pad(10f).padTop(expanderPadTop).growX().row()
|
||||
// I think it's not necessary to uncheck the imcompatible (now invisible) checkBoxes
|
||||
|
||||
disableIncompatibleMods()
|
||||
|
||||
runComplexModCheck()
|
||||
}
|
||||
@ -103,6 +102,8 @@ class ModCheckboxTable(
|
||||
}
|
||||
mods.clear()
|
||||
disableChangeEvents = false
|
||||
|
||||
disableIncompatibleMods()
|
||||
onUpdate("-") // should match no mod
|
||||
}
|
||||
|
||||
@ -114,6 +115,7 @@ class ModCheckboxTable(
|
||||
// Check over complete combination of selected mods
|
||||
val complexModLinkCheck = RulesetCache.checkCombinedModLinks(mods, baseRulesetName)
|
||||
if (!complexModLinkCheck.isWarnUser()){
|
||||
savedModcheckResult = null
|
||||
Gdx.input.inputProcessor = currentInputProcessor
|
||||
return false
|
||||
}
|
||||
@ -167,20 +169,41 @@ class ModCheckboxTable(
|
||||
|
||||
}
|
||||
|
||||
disableIncompatibleMods()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
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()
|
||||
/** Deselect incompatible mods after [skipCheckBox] was selected.
|
||||
*
|
||||
* Note: Inactive - we don'n even allow a conflict to be turned on using [disableIncompatibleMods].
|
||||
* But if we want the alternative UX instead - use this in [checkBoxChanged] near `mods.add` and skip disabling...
|
||||
*/
|
||||
@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) =
|
||||
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)
|
||||
/** Disable incompatible mods - those that could not be turned on with the current selection */
|
||||
private fun disableIncompatibleMods() {
|
||||
for (modWidget in modWidgets) {
|
||||
val enable = ModCompatibility.meetsAllRequirements(modWidget.mod, baseRuleset, getSelectedMods())
|
||||
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
|
||||
??? 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]"
|
||||
|
||||
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"
|
||||
Applicable to: ModOptions
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user