mirror of
https://github.com/yairm210/Unciv.git
synced 2025-01-05 21:11:35 +07:00
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:
parent
0db070a25f
commit
96292cbf4d
@ -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):
|
||||
|
@ -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 /////////////////////////////////////////////
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
||||
}
|
||||
|
@ -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).
|
||||
|
Loading…
Reference in New Issue
Block a user