Fix Permanent Audiovisual toggle and start on declarative Mod Compatibility (#9970)

* Fix Mod Manager not offering "Permanent Audiovisual" for audio-only mods

* Beginnings of declarative Mod Compatibility
This commit is contained in:
SomeTroglodyte 2023-08-28 09:51:14 +02:00 committed by GitHub
parent 0db070a25f
commit 96292cbf4d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 126 additions and 28 deletions

View File

@ -510,6 +510,16 @@ enum class UniqueParameterType(
override fun getTranslationWriterStringsForOutput() = knownValues
},
/** Mod declarative compatibility: Behaves like [Unknown], but makes for nicer auto-generated documentation. */
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.UniqueComplianceErrorSeverity? =
if ('-' !in parameterText && ('*' !in parameterText || parameterText.matches(Regex("""^\*[^*]+\*$""")))) null
else UniqueType.UniqueComplianceErrorSeverity.RulesetInvariant
override fun getTranslationWriterStringsForOutput() = scanExistingValues(this)
},
/** Behaves like [Unknown], but states explicitly the parameter is OK and its contents are ignored */
Comment("comment", "comment", null, "Unique Specials") {
override fun getErrorSeverity(parameterText: String, ruleset: Ruleset):

View File

@ -766,6 +766,12 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags:
UniqueTarget.Tech, UniqueTarget.Terrain, UniqueTarget.Resource, UniqueTarget.Policy, UniqueTarget.Promotion,
UniqueTarget.Nation, UniqueTarget.Ruins, flags = UniqueFlag.setOfHiddenToUsers),
// Declarative Mod compatibility (so far rudimentary):
ModIncompatibleWith("Mod is incompatible with [modFilter]", UniqueTarget.ModOptions),
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),
// endregion
///////////////////////////////////////////// region 99 DEPRECATED AND REMOVED /////////////////////////////////////////////

View File

@ -1,11 +1,13 @@
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.translations.tr
import com.unciv.ui.components.extensions.UncivDateFormat.formatDate
import com.unciv.ui.components.extensions.UncivDateFormat.parseDate
@ -22,7 +24,9 @@ internal class ModInfoAndActionPane : Table() {
private val imageHolder = Table()
private val sizeLabel = "".toLabel()
private var isBuiltin = false
private var disableVisualCheckBox = false
/** controls "Permanent audiovisual mod" checkbox existence */
private var enableVisualCheckBox = false
init {
defaults().pad(10f)
@ -33,7 +37,7 @@ internal class ModInfoAndActionPane : Table() {
*/
fun update(repo: Github.Repo) {
isBuiltin = false
disableVisualCheckBox = true
enableVisualCheckBox = false
update(
repo.name, repo.html_url, repo.default_branch,
repo.pushed_at, repo.owner.login, repo.size,
@ -48,7 +52,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 }
disableVisualCheckBox = mod.folderLocation?.list("atlas")?.isEmpty() ?: true // Also catches isBuiltin
enableVisualCheckBox = shouldShowVisualCheckbox(mod)
update(
modName, modOptions.modUrl, modOptions.defaultBranch,
modOptions.lastUpdated, modOptions.author, modOptions.modSize
@ -106,8 +110,8 @@ internal class ModInfoAndActionPane : Table() {
}
fun addVisualCheckBox(startsOutChecked: Boolean = false, changeAction: ((Boolean)->Unit)? = null) {
if (disableVisualCheckBox) return
add("Permanent audiovisual mod".toCheckBox(startsOutChecked, changeAction)).row()
if (enableVisualCheckBox)
add("Permanent audiovisual mod".toCheckBox(startsOutChecked, changeAction)).row()
}
fun addUpdateModButton(modInfo: ModUIData, doDownload: () -> Unit) {
@ -166,4 +170,25 @@ 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
return folder.list("atlas").isNotEmpty()
}
}

View File

@ -373,6 +373,7 @@ class GameOptionsTable(
// If so, add it to the current ruleset
gameParameters.baseRuleset = newBaseRuleset
modCheckboxes.setBaseRuleset(newBaseRuleset) // Treats declared incompatibility
onChooseMod(newBaseRuleset)
// Check if the ruleset in its entirety is still well-defined
@ -383,7 +384,6 @@ class GameOptionsTable(
}
modLinkErrors.showWarnOrErrorToast(previousScreen as BaseScreen)
modCheckboxes.setBaseRuleset(newBaseRuleset)
return null
}

View File

@ -5,6 +5,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.ui.components.ExpanderTab
import com.unciv.ui.components.extensions.pad
import com.unciv.ui.components.extensions.toCheckBox
@ -17,19 +18,23 @@ import com.unciv.ui.screens.basescreen.BaseScreen
* Manages compatibility checks, warns or prevents incompatibilities.
*
* @param mods In/out set of active mods, modified in place
* @param baseRuleset The selected base Ruleset, only for running mod checks against. Use [setBaseRuleset] to change on the fly.
* @param initialBaseRuleset The selected base Ruleset, only for running mod checks against. Use [setBaseRuleset] to change on the fly.
* @param screen Parent screen, used only to show [ToastPopup]s
* @param isPortrait Used only for minor layout tweaks, arrangement is always vertical
* @param onUpdate Callback, parameter is the mod name, called after any checks that may prevent mod selection succeed.
*/
class ModCheckboxTable(
private val mods: LinkedHashSet<String>,
private var baseRuleset: String,
initialBaseRuleset: String,
private val screen: BaseScreen,
isPortrait: Boolean = false,
private val onUpdate: (String) -> Unit
): Table() {
private val extensionRulesetModButtons = ArrayList<CheckBox>()
private var baseRulesetName = ""
private lateinit var baseRuleset: Ruleset
private class ModWithCheckBox(val mod: Ruleset, val widget: CheckBox)
private val modWidgets = ArrayList<ModWithCheckBox>()
/** Saved result from any complex mod check unless the causing selection has already been reverted.
* In other words, this can contain the text for an "Error" level check only if the Widget was
@ -40,8 +45,15 @@ class ModCheckboxTable(
private var disableChangeEvents = false
private val expanderPadTop = if (isPortrait) 0f else 16f
init {
val modRulesets = RulesetCache.values.filter { it.name != "" && !it.modOptions.isBaseRuleset}
val modRulesets = RulesetCache.values.filterNot {
it.modOptions.isBaseRuleset
|| it.name.isBlank()
|| it.modOptions.hasUnique(UniqueType.ModIsAudioVisualOnly)
}
for (mod in modRulesets.sortedBy { it.name }) {
val checkBox = mod.name.toCheckBox(mod.name in mods)
checkBox.onChange {
@ -49,29 +61,44 @@ class ModCheckboxTable(
onUpdate(mod.name)
}
}
extensionRulesetModButtons.add(checkBox)
checkBox.left()
modWidgets += ModWithCheckBox(mod, checkBox)
}
val padTop = if (isPortrait) 0f else 16f
if (extensionRulesetModButtons.any()) {
add(ExpanderTab("Extension mods", persistenceID = "NewGameExpansionMods") {
it.defaults().pad(5f,0f)
for (checkbox in extensionRulesetModButtons) {
checkbox.left()
it.add(checkbox).row()
}
}).pad(10f).padTop(padTop).growX().row()
runComplexModCheck()
}
setBaseRuleset(initialBaseRuleset)
}
fun setBaseRuleset(newBaseRuleset: String) {
baseRulesetName = newBaseRuleset
savedModcheckResult = null
clear()
mods.clear() // We'll regenerate this from checked widgets
baseRuleset = RulesetCache[newBaseRuleset] ?: return
val compatibleMods = modWidgets
.filterNot { isIncompatible(it.mod, baseRuleset) }
if (compatibleMods.none()) return
for (mod in compatibleMods) {
if (mod.widget.isChecked) mods += mod.mod.name
}
add(ExpanderTab("Extension mods", persistenceID = "NewGameExpansionMods") {
it.defaults().pad(5f,0f)
for (mod in compatibleMods) {
it.add(mod.widget).row()
}
}).pad(10f).padTop(expanderPadTop).growX().row()
// I think it's not necessary to uncheck the imcompatible (now invisible) checkBoxes
runComplexModCheck()
}
fun setBaseRuleset(newBaseRuleset: String) { baseRuleset = newBaseRuleset }
fun disableAllCheckboxes() {
disableChangeEvents = true
for (checkBox in extensionRulesetModButtons) {
checkBox.isChecked = false
for (mod in modWidgets) {
mod.widget.isChecked = false
}
mods.clear()
disableChangeEvents = false
@ -80,7 +107,7 @@ class ModCheckboxTable(
private fun runComplexModCheck(): Boolean {
// Check over complete combination of selected mods
val complexModLinkCheck = RulesetCache.checkCombinedModLinks(mods, baseRuleset)
val complexModLinkCheck = RulesetCache.checkCombinedModLinks(mods, baseRulesetName)
if (!complexModLinkCheck.isWarnUser()) return false
savedModcheckResult = complexModLinkCheck.getErrorText()
complexModLinkCheck.showWarnOrErrorToast(screen)
@ -132,4 +159,18 @@ class ModCheckboxTable(
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()
}
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)
}

View File

@ -1728,6 +1728,21 @@ Simple unique parameters are explained by mouseover. Complex parameters are expl
??? example "Provides a unique luxury"
Applicable to: CityState
## ModOptions uniques
??? example "Mod is incompatible with [modFilter]"
Example: "Mod is incompatible with [DeCiv Redux]"
Applicable to: ModOptions
??? example "Should only be used as permanent audiovisual mod"
Applicable to: ModOptions
??? example "Can be used as permanent audiovisual mod"
Applicable to: ModOptions
??? example "Cannot be used as permanent audiovisual mod"
Applicable to: ModOptions
## Conditional uniques
!!! note ""
@ -2132,6 +2147,7 @@ Simple unique parameters are explained by mouseover. Complex parameters are expl
*[era]: The name of any era.
*[foundingOrEnhancing]: `founding` or `enhancing`.
*[improvementName]: The name of any improvement.
*[modFilter]: A Mod name, case-sensitive _or_ a simple wildcard filter beginning and ending in an Asterisk, case-insensitive.
*[policy]: The name of any policy.
*[promotion]: The name of any promotion.
*[relativeAmount]: This indicates a number, usually with a + or - sign, such as `+25` (this kind of parameter is often followed by '%' which is nevertheless not part of the value).