mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-18 19:59:47 +07:00
Prevent mod conflicts better (#9586)
* Tighten mod check severity and selectivity for unit-producing triggered Uniques * Prettify display of mod check results by suppressing dupes and hiding conditionals from tr() * Extra confirmation to play with errors, colors, improved handling of mod checkboxes * Tweaks to improved mod checking in new game
This commit is contained in:
@ -439,11 +439,17 @@ No victory conditions were selected! =
|
||||
Mods: =
|
||||
Extension mods =
|
||||
Base ruleset: =
|
||||
# Note - do not translate the colour names between «». Changing them works if you know what you're doing.
|
||||
The mod you selected is incorrectly defined! =
|
||||
The mod you selected is «RED»incorrectly defined!«» =
|
||||
The mod combination you selected is incorrectly defined! =
|
||||
The mod combination you selected is «RED»incorrectly defined!«» =
|
||||
The mod combination you selected has problems. =
|
||||
You can play it, but don't expect everything to work! =
|
||||
The mod combination you selected «GOLD»has problems«». =
|
||||
You can play it, but «GOLDENROD»don't expect everything to work!«» =
|
||||
This base ruleset is not compatible with the previously selected\nextension mods. They have been disabled. =
|
||||
Are you really sure you want to play with the following known problems? =
|
||||
Base Ruleset =
|
||||
[amount] Techs =
|
||||
[amount] Nations =
|
||||
|
@ -50,6 +50,8 @@ class GameParameters : IsPartOfGameInfoSerialization { // Default values are the
|
||||
|
||||
var maxTurns = 500
|
||||
|
||||
var acceptedModCheckErrors = ""
|
||||
|
||||
fun clone(): GameParameters {
|
||||
val parameters = GameParameters()
|
||||
parameters.difficulty = difficulty
|
||||
@ -80,6 +82,7 @@ class GameParameters : IsPartOfGameInfoSerialization { // Default values are the
|
||||
parameters.baseRuleset = baseRuleset
|
||||
parameters.mods = LinkedHashSet(mods)
|
||||
parameters.maxTurns = maxTurns
|
||||
parameters.acceptedModCheckErrors = acceptedModCheckErrors
|
||||
return parameters
|
||||
}
|
||||
|
||||
|
@ -31,6 +31,7 @@ class RulesetValidator(val ruleset: Ruleset) {
|
||||
|
||||
val lines = RulesetErrorList()
|
||||
|
||||
/********************** Ruleset Invariant Part **********************/
|
||||
// Checks for all mods - only those that can succeed without loading a base ruleset
|
||||
// When not checking the entire ruleset, we can only really detect ruleset-invariant errors in uniques
|
||||
|
||||
@ -128,6 +129,8 @@ class RulesetValidator(val ruleset: Ruleset) {
|
||||
// Quit here when no base ruleset is loaded - references cannot be checked
|
||||
if (!ruleset.modOptions.isBaseRuleset) return lines
|
||||
|
||||
/********************** Ruleset Specific Part **********************/
|
||||
|
||||
val vanillaRuleset = RulesetCache.getVanillaRuleset() // for UnitTypes fallback
|
||||
|
||||
|
||||
@ -484,12 +487,11 @@ class RulesetValidator(val ruleset: Ruleset) {
|
||||
|
||||
val typeComplianceErrors = unique.type.getComplianceErrors(unique, ruleset)
|
||||
for (complianceError in typeComplianceErrors) {
|
||||
// TODO: Make this Error eventually, this is Not Good
|
||||
if (complianceError.errorSeverity <= severityToReport)
|
||||
rulesetErrors.add(RulesetError("$name's unique \"${unique.text}\" contains parameter ${complianceError.parameterName}," +
|
||||
" which does not fit parameter type" +
|
||||
" ${complianceError.acceptableParameterTypes.joinToString(" or ") { it.parameterName }} !",
|
||||
RulesetErrorSeverity.Warning
|
||||
complianceError.errorSeverity.getRulesetErrorSeverity(severityToReport)
|
||||
))
|
||||
}
|
||||
|
||||
@ -505,9 +507,11 @@ class RulesetValidator(val ruleset: Ruleset) {
|
||||
conditional.type.getComplianceErrors(conditional, ruleset)
|
||||
for (complianceError in conditionalComplianceErrors) {
|
||||
if (complianceError.errorSeverity == severityToReport)
|
||||
rulesetErrors += "$name's unique \"${unique.text}\" contains the conditional \"${conditional.text}\"." +
|
||||
rulesetErrors.add(RulesetError( "$name's unique \"${unique.text}\" contains the conditional \"${conditional.text}\"." +
|
||||
" This contains the parameter ${complianceError.parameterName} which does not fit parameter type" +
|
||||
" ${complianceError.acceptableParameterTypes.joinToString(" or ") { it.parameterName }} !"
|
||||
" ${complianceError.acceptableParameterTypes.joinToString(" or ") { it.parameterName }} !",
|
||||
complianceError.errorSeverity.getRulesetErrorSeverity(severityToReport)
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -538,6 +542,7 @@ class RulesetValidator(val ruleset: Ruleset) {
|
||||
|
||||
|
||||
class RulesetError(val text:String, val errorSeverityToReport: RulesetErrorSeverity)
|
||||
|
||||
enum class RulesetErrorSeverity(val color: Color) {
|
||||
OK(Color.GREEN),
|
||||
WarningOptionsOnly(Color.YELLOW),
|
||||
@ -554,6 +559,23 @@ class RulesetErrorList : ArrayList<RulesetError>() {
|
||||
add(RulesetError(text, errorSeverityToReport))
|
||||
}
|
||||
|
||||
override fun add(element: RulesetError): Boolean {
|
||||
// Suppress duplicates due to the double run of some checks for invariant/specific,
|
||||
// Without changing collection type or making RulesetError obey the equality contract
|
||||
val existing = firstOrNull { it.text == element.text }
|
||||
?: return super.add(element)
|
||||
if (existing.errorSeverityToReport >= element.errorSeverityToReport) return false
|
||||
remove(existing)
|
||||
return super.add(element)
|
||||
}
|
||||
|
||||
override fun addAll(elements: Collection<RulesetError>): Boolean {
|
||||
var result = false
|
||||
for (element in elements)
|
||||
if (add(element)) result = true
|
||||
return result
|
||||
}
|
||||
|
||||
fun getFinalSeverity(): RulesetErrorSeverity {
|
||||
if (isEmpty()) return RulesetErrorSeverity.OK
|
||||
return this.maxOf { it.errorSeverityToReport }
|
||||
@ -571,5 +593,10 @@ class RulesetErrorList : ArrayList<RulesetError>() {
|
||||
fun getErrorText(filter: (RulesetError)->Boolean) =
|
||||
filter(filter)
|
||||
.sortedByDescending { it.errorSeverityToReport }
|
||||
.joinToString("\n") { it.errorSeverityToReport.name + ": " + it.text }
|
||||
.joinToString("\n") {
|
||||
it.errorSeverityToReport.name + ": " +
|
||||
// This will go through tr(), unavoidably, which will move the conditionals
|
||||
// out of place. Prevent via kludge:
|
||||
it.text.replace('<','〈').replace('>','〉')
|
||||
}
|
||||
}
|
||||
|
@ -125,12 +125,12 @@ enum class UniqueParameterType(
|
||||
override fun getTranslationWriterStringsForOutput() = knownValues
|
||||
},
|
||||
|
||||
/** Only used by [BaseUnitFilter] */
|
||||
/** Used by [BaseUnitFilter] and e.g. [UniqueType.OneTimeFreeUnit] */
|
||||
UnitName("unit", "Musketman") {
|
||||
override fun getErrorSeverity(parameterText: String, ruleset: Ruleset):
|
||||
UniqueType.UniqueComplianceErrorSeverity? {
|
||||
if (ruleset.units.containsKey(parameterText)) return null
|
||||
return UniqueType.UniqueComplianceErrorSeverity.WarningOnly
|
||||
return UniqueType.UniqueComplianceErrorSeverity.RulesetSpecific // OneTimeFreeUnitRuins crashes with a bad parameter
|
||||
}
|
||||
},
|
||||
|
||||
@ -250,7 +250,7 @@ enum class UniqueParameterType(
|
||||
parameterText != "All" && getErrorSeverity(parameterText, ruleset) == null
|
||||
},
|
||||
|
||||
/** Implemented by [PopulationManager.getPopulationFilterAmount][com.unciv.logic.city.CityPopulationManager.getPopulationFilterAmount] */
|
||||
/** Implemented by [PopulationManager.getPopulationFilterAmount][com.unciv.logic.city.managers.CityPopulationManager.getPopulationFilterAmount] */
|
||||
PopulationFilter("populationFilter", "Followers of this Religion", null, "Population Filters") {
|
||||
private val knownValues = setOf("Population", "Specialists", "Unemployed", "Followers of the Majority Religion", "Followers of this Religion")
|
||||
override fun getErrorSeverity(
|
||||
|
@ -2,6 +2,7 @@ package com.unciv.models.ruleset.unique
|
||||
|
||||
import com.unciv.Constants
|
||||
import com.unciv.models.ruleset.Ruleset
|
||||
import com.unciv.models.ruleset.RulesetErrorSeverity
|
||||
import com.unciv.models.translations.getPlaceholderParameters
|
||||
import com.unciv.models.translations.getPlaceholderText
|
||||
|
||||
@ -732,9 +733,9 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags:
|
||||
///////////////////////////////////////// region TRIGGERED ONE-TIME /////////////////////////////////////////
|
||||
|
||||
|
||||
OneTimeFreeUnit("Free [baseUnitFilter] appears", UniqueTarget.Triggerable), // used in Policies, Buildings
|
||||
OneTimeAmountFreeUnits("[amount] free [baseUnitFilter] units appear", UniqueTarget.Triggerable), // used in Buildings
|
||||
OneTimeFreeUnitRuins("Free [baseUnitFilter] found in the ruins", UniqueTarget.Ruins), // Differs from "Free [] appears" in that it spawns near the ruins instead of in a city
|
||||
OneTimeFreeUnit("Free [unit] appears", UniqueTarget.Triggerable), // used in Policies, Buildings
|
||||
OneTimeAmountFreeUnits("[amount] free [unit] units appear", UniqueTarget.Triggerable), // used in Buildings
|
||||
OneTimeFreeUnitRuins("Free [unit] found in the ruins", UniqueTarget.Ruins), // Differs from "Free [] appears" in that it spawns near the ruins instead of in a city
|
||||
OneTimeFreePolicy("Free Social Policy", UniqueTarget.Triggerable), // used in Buildings
|
||||
OneTimeAmountFreePolicies("[amount] Free Social Policies", UniqueTarget.Triggerable), // Not used in Vanilla
|
||||
OneTimeEnterGoldenAge("Empire enters golden age", UniqueTarget.Triggerable), // used in Policies, Buildings
|
||||
@ -1205,15 +1206,32 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags:
|
||||
enum class UniqueComplianceErrorSeverity {
|
||||
|
||||
/** This is for filters that can also potentially accept free text, like UnitFilter and TileFilter */
|
||||
WarningOnly,
|
||||
WarningOnly {
|
||||
override fun getRulesetErrorSeverity(severityToReport: UniqueComplianceErrorSeverity) =
|
||||
RulesetErrorSeverity.WarningOptionsOnly
|
||||
},
|
||||
|
||||
/** This is a problem like "unit/resource/tech name doesn't exist in ruleset" - definite bug */
|
||||
RulesetSpecific,
|
||||
|
||||
RulesetSpecific {
|
||||
// Report Warning on the first pass of RulesetValidator only, where mods are checked standalone
|
||||
// but upgrade to error when the econd pass asks, which runs only for combined or base rulesets.
|
||||
override fun getRulesetErrorSeverity(severityToReport: UniqueComplianceErrorSeverity) =
|
||||
RulesetErrorSeverity.Warning
|
||||
},
|
||||
|
||||
/** This is a problem like "numbers don't parse", "stat isn't stat", "city filter not applicable" */
|
||||
RulesetInvariant
|
||||
RulesetInvariant {
|
||||
override fun getRulesetErrorSeverity(severityToReport: UniqueComplianceErrorSeverity) =
|
||||
RulesetErrorSeverity.Error
|
||||
},
|
||||
;
|
||||
|
||||
/** Done as function instead of property so we can in the future upgrade severities depending
|
||||
* on the [RulesetValidator] "pass": [severityToReport]==[RulesetInvariant] means it's the
|
||||
* first pass that also runs for extension mods without a base mixed in; the complex check
|
||||
* runs with [severityToReport]==[RulesetSpecific].
|
||||
*/
|
||||
abstract fun getRulesetErrorSeverity(severityToReport: UniqueComplianceErrorSeverity): RulesetErrorSeverity
|
||||
}
|
||||
|
||||
/** Maps uncompliant parameters to their required types */
|
||||
|
@ -1,6 +1,8 @@
|
||||
package com.unciv.ui.popups
|
||||
|
||||
import com.badlogic.gdx.scenes.scene2d.Stage
|
||||
import com.badlogic.gdx.utils.Align
|
||||
import com.unciv.ui.components.ColorMarkupLabel
|
||||
import com.unciv.ui.components.input.onClick
|
||||
import com.unciv.ui.screens.basescreen.BaseScreen
|
||||
import com.unciv.utils.Concurrency
|
||||
@ -10,6 +12,8 @@ import kotlinx.coroutines.delay
|
||||
/**
|
||||
* This is an unobtrusive popup which will close itself after a given amount of time.
|
||||
* Default time is two seconds (in milliseconds)
|
||||
*
|
||||
* Note: Supports color markup via [ColorMarkupLabel], using «» instead of Gdx's [].
|
||||
*/
|
||||
class ToastPopup (message: String, stageToShowOn: Stage, val time: Long = 2000) : Popup(stageToShowOn){
|
||||
|
||||
@ -20,7 +24,11 @@ class ToastPopup (message: String, stageToShowOn: Stage, val time: Long = 2000)
|
||||
setFillParent(false)
|
||||
onClick { close() } // or `touchable = Touchable.disabled` so you can operate what's behind
|
||||
|
||||
addGoodSizedLabel(message)
|
||||
add(ColorMarkupLabel(message).apply {
|
||||
wrap = true
|
||||
setAlignment(Align.center)
|
||||
}).width(stageToShowOn.width / 2)
|
||||
|
||||
open()
|
||||
//move it to the top so its not in the middle of the screen
|
||||
//have to be done after open() because open() centers the popup
|
||||
|
@ -0,0 +1,48 @@
|
||||
package com.unciv.ui.screens.newgamescreen
|
||||
|
||||
import com.badlogic.gdx.utils.Align
|
||||
import com.unciv.Constants
|
||||
import com.unciv.ui.components.ColorMarkupLabel
|
||||
import com.unciv.ui.popups.ConfirmPopup
|
||||
import com.unciv.ui.popups.closeAllPopups
|
||||
import com.unciv.ui.screens.basescreen.BaseScreen
|
||||
|
||||
internal class AcceptModErrorsPopup(
|
||||
screen: BaseScreen,
|
||||
modCheckResult: String,
|
||||
restoreDefault: () -> Unit,
|
||||
action: () -> Unit
|
||||
) : ConfirmPopup(
|
||||
screen,
|
||||
question = "", // do coloured label instead
|
||||
confirmText = "Accept",
|
||||
isConfirmPositive = false,
|
||||
restoreDefault = restoreDefault,
|
||||
action = action
|
||||
) {
|
||||
init {
|
||||
clickBehindToClose = false
|
||||
row() // skip the empty question label
|
||||
val maxRowWidth = screen.stage.width * 0.9f - 50f // total padding is 2*(20+5)
|
||||
getScrollPane()?.setScrollingDisabled(true, false)
|
||||
|
||||
// Note - using the version of ColorMarkupLabel that supports «color» but it was too garish.
|
||||
val question = "Are you really sure you want to play with the following known problems?"
|
||||
val label1 = ColorMarkupLabel(question, Constants.headingFontSize)
|
||||
val wrapWidth = label1.prefWidth.coerceIn(maxRowWidth / 2, maxRowWidth)
|
||||
label1.setAlignment(Align.center)
|
||||
if (label1.prefWidth > wrapWidth) {
|
||||
label1.wrap = true
|
||||
add(label1).width(wrapWidth).padBottom(15f).row()
|
||||
} else add(label1).padBottom(15f).row()
|
||||
|
||||
val warnings = modCheckResult.replace("Error:", "«RED»Error«»:")
|
||||
.replace("Warning:","«GOLD»Warning«»:")
|
||||
val label2 = ColorMarkupLabel(warnings)
|
||||
label2.wrap = true
|
||||
add(label2).width(wrapWidth)
|
||||
|
||||
screen.closeAllPopups() // Toasts too
|
||||
open(true)
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ import com.badlogic.gdx.utils.Align
|
||||
import com.unciv.Constants
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.logic.civilization.PlayerType
|
||||
import com.unciv.models.metadata.BaseRuleset
|
||||
import com.unciv.models.metadata.GameParameters
|
||||
import com.unciv.models.metadata.Player
|
||||
import com.unciv.models.ruleset.RulesetCache
|
||||
@ -18,20 +19,19 @@ import com.unciv.ui.audio.MusicMood
|
||||
import com.unciv.ui.audio.MusicTrackChooserFlags
|
||||
import com.unciv.ui.components.AutoScrollPane
|
||||
import com.unciv.ui.components.ExpanderTab
|
||||
import com.unciv.ui.components.input.KeyCharAndCode
|
||||
import com.unciv.ui.components.UncivSlider
|
||||
import com.unciv.ui.components.input.keyShortcuts
|
||||
import com.unciv.ui.components.input.onActivation
|
||||
import com.unciv.ui.components.input.onChange
|
||||
import com.unciv.ui.components.input.onClick
|
||||
import com.unciv.ui.components.extensions.pad
|
||||
import com.unciv.ui.components.extensions.toCheckBox
|
||||
import com.unciv.ui.components.extensions.toImageButton
|
||||
import com.unciv.ui.components.extensions.toLabel
|
||||
import com.unciv.ui.components.extensions.toTextButton
|
||||
import com.unciv.ui.components.input.KeyCharAndCode
|
||||
import com.unciv.ui.components.input.keyShortcuts
|
||||
import com.unciv.ui.components.input.onActivation
|
||||
import com.unciv.ui.components.input.onChange
|
||||
import com.unciv.ui.components.input.onClick
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
import com.unciv.ui.popups.Popup
|
||||
import com.unciv.ui.popups.ToastPopup
|
||||
import com.unciv.ui.screens.basescreen.BaseScreen
|
||||
import com.unciv.ui.screens.multiplayerscreens.MultiplayerHelpers
|
||||
import kotlin.reflect.KMutableProperty0
|
||||
@ -45,30 +45,31 @@ class GameOptionsTable(
|
||||
var gameParameters = previousScreen.gameSetupInfo.gameParameters
|
||||
val ruleset = previousScreen.ruleset
|
||||
var locked = false
|
||||
var modCheckboxes: ModCheckboxTable? = null
|
||||
private set
|
||||
|
||||
/** Holds the UI for the Extension Mods
|
||||
*
|
||||
* Attention: This Widget is a little tricky due to the UI changes to support portrait mode:
|
||||
* * With `isPortrait==false`, this Table will **contain** `modCheckboxes`
|
||||
* * With `isPortrait==true`, this Table will **only initialize** `modCheckboxes` and [NewGameScreen] will fetch and place it.
|
||||
*
|
||||
* The second reason this is public: [NewGameScreen] accesses [ModCheckboxTable.savedModcheckResult] for display.
|
||||
*/
|
||||
val modCheckboxes = getModCheckboxes(isPortrait = isPortrait)
|
||||
|
||||
// Remember this so we can unselect it when the pool dialog returns an empty pool
|
||||
private var randomNationsPoolCheckbox: CheckBox? = null
|
||||
// Allow resetting base ruleset from outside
|
||||
private var baseRulesetSelectBox: TranslatedSelectBox? = null
|
||||
|
||||
init {
|
||||
getGameOptionsTable()
|
||||
background = BaseScreen.skinStrings.getUiBackground("NewGameScreen/GameOptionsTable", tintColor = BaseScreen.skinStrings.skinConfig.clearColor)
|
||||
top()
|
||||
defaults().pad(5f)
|
||||
update()
|
||||
}
|
||||
|
||||
fun update() {
|
||||
clear()
|
||||
getGameOptionsTable()
|
||||
}
|
||||
|
||||
private fun getGameOptionsTable() {
|
||||
top()
|
||||
defaults().pad(5f)
|
||||
|
||||
// We assign this first to make sure addBaseRulesetSelectBox doesn't reference a null object
|
||||
modCheckboxes =
|
||||
if (isPortrait)
|
||||
getModCheckboxes(isPortrait = true)
|
||||
else getModCheckboxes()
|
||||
|
||||
add(Table().apply {
|
||||
defaults().pad(5f)
|
||||
@ -271,7 +272,6 @@ class GameOptionsTable(
|
||||
) {
|
||||
if (maxValue < minValue) return
|
||||
|
||||
@Suppress("JoinDeclarationAndAssignment") // it's a forward declaration!
|
||||
lateinit var maxSlider: UncivSlider // lateinit safe because the closure won't use it until the user operates a slider
|
||||
val minSlider = UncivSlider(minValue.toFloat(), maxValue.toFloat(), 1f, initial = minField.get().toFloat()) {
|
||||
val newMin = it.toInt()
|
||||
@ -341,7 +341,7 @@ class GameOptionsTable(
|
||||
return slider
|
||||
}
|
||||
|
||||
private fun Table.addSelectBox(text: String, values: Collection<String>, initialState: String, onChange: (newValue: String) -> String?) {
|
||||
private fun Table.addSelectBox(text: String, values: Collection<String>, initialState: String, onChange: (newValue: String) -> String?): TranslatedSelectBox {
|
||||
add(text.toLabel(hideIcons = true)).left()
|
||||
val selectBox = TranslatedSelectBox(values, initialState, BaseScreen.skin)
|
||||
selectBox.isDisabled = locked
|
||||
@ -351,6 +351,7 @@ class GameOptionsTable(
|
||||
}
|
||||
onChange(selectBox.selected.value)
|
||||
add(selectBox).fillX().row()
|
||||
return selectBox
|
||||
}
|
||||
|
||||
private fun Table.addDifficultySelectBox() {
|
||||
@ -359,50 +360,36 @@ class GameOptionsTable(
|
||||
}
|
||||
|
||||
private fun Table.addBaseRulesetSelectBox() {
|
||||
val sortedBaseRulesets = RulesetCache.getSortedBaseRulesets()
|
||||
if (sortedBaseRulesets.size < 2) return
|
||||
|
||||
addSelectBox(
|
||||
"{Base Ruleset}:",
|
||||
sortedBaseRulesets,
|
||||
gameParameters.baseRuleset
|
||||
) { newBaseRuleset ->
|
||||
fun onBaseRulesetSelected(newBaseRuleset: String): String? {
|
||||
val previousSelection = gameParameters.baseRuleset
|
||||
if (newBaseRuleset == gameParameters.baseRuleset) return@addSelectBox null
|
||||
if (newBaseRuleset == previousSelection) return null
|
||||
|
||||
// Check if this mod is well-defined
|
||||
val baseRulesetErrors = RulesetCache[newBaseRuleset]!!.checkModLinks()
|
||||
if (baseRulesetErrors.isError()) {
|
||||
val toastMessage = "The mod you selected is incorrectly defined!".tr() + "\n\n${baseRulesetErrors.getErrorText()}"
|
||||
ToastPopup(toastMessage, previousScreen as BaseScreen, 5000L)
|
||||
return@addSelectBox previousSelection
|
||||
baseRulesetErrors.showWarnOrErrorToast(previousScreen as BaseScreen)
|
||||
return previousSelection
|
||||
}
|
||||
|
||||
// If so, add it to the current ruleset
|
||||
gameParameters.baseRuleset = newBaseRuleset
|
||||
onChooseMod(newBaseRuleset)
|
||||
|
||||
// Check if the ruleset in it's entirety is still well-defined
|
||||
// Check if the ruleset in its entirety is still well-defined
|
||||
val modLinkErrors = ruleset.checkModLinks()
|
||||
if (modLinkErrors.isError()) {
|
||||
gameParameters.mods.clear()
|
||||
modCheckboxes.disableAllCheckboxes() // also clears gameParameters.mods
|
||||
reloadRuleset()
|
||||
val toastMessage =
|
||||
"This base ruleset is not compatible with the previously selected\nextension mods. They have been disabled.".tr()
|
||||
ToastPopup(toastMessage, previousScreen as BaseScreen, 5000L)
|
||||
}
|
||||
modLinkErrors.showWarnOrErrorToast(previousScreen as BaseScreen)
|
||||
|
||||
modCheckboxes!!.disableAllCheckboxes()
|
||||
} else if (modLinkErrors.isWarnUser()) {
|
||||
val toastMessage =
|
||||
"{The mod combination you selected has problems.}\n{You can play it, but don't expect everything to work!}".tr() +
|
||||
"\n\n${modLinkErrors.getErrorText()}"
|
||||
ToastPopup(toastMessage, previousScreen as BaseScreen, 5000L)
|
||||
modCheckboxes.setBaseRuleset(newBaseRuleset)
|
||||
return null
|
||||
}
|
||||
|
||||
modCheckboxes!!.setBaseRuleset(newBaseRuleset)
|
||||
|
||||
null
|
||||
}
|
||||
val sortedBaseRulesets = RulesetCache.getSortedBaseRulesets()
|
||||
if (sortedBaseRulesets.size < 2) return
|
||||
baseRulesetSelectBox = addSelectBox("{Base Ruleset}:", sortedBaseRulesets, gameParameters.baseRuleset, ::onBaseRulesetSelected)
|
||||
}
|
||||
|
||||
private fun Table.addGameSpeedSelectBox() {
|
||||
@ -442,6 +429,15 @@ class GameOptionsTable(
|
||||
add(victoryConditionsTable).colspan(2).row()
|
||||
}
|
||||
|
||||
fun resetRuleset() {
|
||||
val rulesetName = BaseRuleset.Civ_V_GnK.fullName
|
||||
gameParameters.baseRuleset = rulesetName
|
||||
modCheckboxes.setBaseRuleset(rulesetName)
|
||||
modCheckboxes.disableAllCheckboxes()
|
||||
baseRulesetSelectBox?.setSelected(rulesetName)
|
||||
reloadRuleset()
|
||||
}
|
||||
|
||||
private fun reloadRuleset() {
|
||||
ruleset.clear()
|
||||
val newRuleset = RulesetCache.getComplexRuleset(gameParameters)
|
||||
|
@ -5,21 +5,19 @@ 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.RulesetErrorList
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.popups.ToastPopup
|
||||
import com.unciv.ui.screens.basescreen.BaseScreen
|
||||
import com.unciv.ui.components.ExpanderTab
|
||||
import com.unciv.ui.components.input.onChange
|
||||
import com.unciv.ui.components.extensions.pad
|
||||
import com.unciv.ui.components.extensions.toCheckBox
|
||||
import com.unciv.ui.components.input.onChange
|
||||
import com.unciv.ui.popups.ToastPopup
|
||||
import com.unciv.ui.screens.basescreen.BaseScreen
|
||||
|
||||
/**
|
||||
* A widget containing one expander for extension mods.
|
||||
* Manages compatibility checks, warns or prevents incompatibilities.
|
||||
*
|
||||
* @param mods In/out set of active mods, modified in place
|
||||
* @param baseRuleset The selected base Ruleset //todo clarify
|
||||
* @param baseRuleset 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.
|
||||
@ -29,14 +27,21 @@ class ModCheckboxTable(
|
||||
private var baseRuleset: String,
|
||||
private val screen: BaseScreen,
|
||||
isPortrait: Boolean = false,
|
||||
onUpdate: (String) -> Unit
|
||||
private val onUpdate: (String) -> Unit
|
||||
): Table() {
|
||||
private val modRulesets = RulesetCache.values.filter { it.name != "" && !it.modOptions.isBaseRuleset}
|
||||
private var lastToast: ToastPopup? = null
|
||||
private val extensionRulesetModButtons = ArrayList<CheckBox>()
|
||||
|
||||
init {
|
||||
/** 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
|
||||
* initialized with such an invalid mod combination.
|
||||
* This Widget reverts User changes that cause an Error severity immediately and this field is nulled.
|
||||
*/
|
||||
var savedModcheckResult: String? = null
|
||||
|
||||
private var disableChangeEvents = false
|
||||
|
||||
init {
|
||||
val modRulesets = RulesetCache.values.filter { it.name != "" && !it.modOptions.isBaseRuleset}
|
||||
for (mod in modRulesets.sortedBy { it.name }) {
|
||||
val checkBox = mod.name.toCheckBox(mod.name in mods)
|
||||
checkBox.onChange {
|
||||
@ -57,25 +62,29 @@ class ModCheckboxTable(
|
||||
it.add(checkbox).row()
|
||||
}
|
||||
}).pad(10f).padTop(padTop).growX().row()
|
||||
|
||||
runComplexModCheck()
|
||||
}
|
||||
}
|
||||
|
||||
fun setBaseRuleset(newBaseRuleset: String) { baseRuleset = newBaseRuleset }
|
||||
fun disableAllCheckboxes() {
|
||||
disableChangeEvents = true
|
||||
for (checkBox in extensionRulesetModButtons) {
|
||||
checkBox.isChecked = false
|
||||
}
|
||||
mods.clear()
|
||||
disableChangeEvents = false
|
||||
onUpdate("-") // should match no mod
|
||||
}
|
||||
|
||||
|
||||
private fun popupToastError(rulesetErrorList: RulesetErrorList) {
|
||||
val initialText =
|
||||
if (rulesetErrorList.isError()) "The mod combination you selected is incorrectly defined!".tr()
|
||||
else "{The mod combination you selected has problems.}\n{You can play it, but don't expect everything to work!}".tr()
|
||||
val toastMessage = "$initialText\n\n${rulesetErrorList.getErrorText()}"
|
||||
|
||||
lastToast?.close()
|
||||
lastToast = ToastPopup(toastMessage, screen, 5000L)
|
||||
private fun runComplexModCheck(): Boolean {
|
||||
// Check over complete combination of selected mods
|
||||
val complexModLinkCheck = RulesetCache.checkCombinedModLinks(mods, baseRuleset)
|
||||
if (!complexModLinkCheck.isWarnUser()) return false
|
||||
savedModcheckResult = complexModLinkCheck.getErrorText()
|
||||
complexModLinkCheck.showWarnOrErrorToast(screen)
|
||||
return complexModLinkCheck.isError()
|
||||
}
|
||||
|
||||
private fun checkBoxChanged(
|
||||
@ -83,14 +92,13 @@ class ModCheckboxTable(
|
||||
changeEvent: ChangeListener.ChangeEvent,
|
||||
mod: Ruleset
|
||||
): Boolean {
|
||||
if (disableChangeEvents) return false
|
||||
|
||||
if (checkBox.isChecked) {
|
||||
// First the quick standalone check
|
||||
val modLinkErrors = mod.checkModLinks()
|
||||
if (modLinkErrors.isError()) {
|
||||
lastToast?.close()
|
||||
val toastMessage =
|
||||
"The mod you selected is incorrectly defined!".tr() + "\n\n${modLinkErrors.getErrorText()}"
|
||||
lastToast = ToastPopup(toastMessage, screen, 5000L)
|
||||
modLinkErrors.showWarnOrErrorToast(screen)
|
||||
changeEvent.cancel() // Cancel event to reset to previous state - see Button.setChecked()
|
||||
return false
|
||||
}
|
||||
@ -98,15 +106,12 @@ class ModCheckboxTable(
|
||||
mods.add(mod.name)
|
||||
|
||||
// Check over complete combination of selected mods
|
||||
val complexModLinkCheck = RulesetCache.checkCombinedModLinks(mods, baseRuleset)
|
||||
if (complexModLinkCheck.isWarnUser()) {
|
||||
popupToastError(complexModLinkCheck)
|
||||
if (complexModLinkCheck.isError()) {
|
||||
if (runComplexModCheck()) {
|
||||
changeEvent.cancel() // Cancel event to reset to previous state - see Button.setChecked()
|
||||
mods.remove(mod.name)
|
||||
savedModcheckResult = null // we just fixed it
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
/**
|
||||
@ -115,15 +120,13 @@ class ModCheckboxTable(
|
||||
*/
|
||||
|
||||
mods.remove(mod.name)
|
||||
val complexModLinkCheck = RulesetCache.checkCombinedModLinks(mods, baseRuleset)
|
||||
if (complexModLinkCheck.isWarnUser()) {
|
||||
popupToastError(complexModLinkCheck)
|
||||
if (complexModLinkCheck.isError()) {
|
||||
|
||||
if (runComplexModCheck()) {
|
||||
changeEvent.cancel() // Cancel event to reset to previous state - see Button.setChecked()
|
||||
mods.add(mod.name)
|
||||
savedModcheckResult = null // we just fixed it
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,23 @@
|
||||
package com.unciv.ui.screens.newgamescreen
|
||||
|
||||
import com.unciv.models.ruleset.RulesetErrorList
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.popups.ToastPopup
|
||||
import com.unciv.ui.popups.popups
|
||||
import com.unciv.ui.screens.basescreen.BaseScreen
|
||||
|
||||
/**
|
||||
* Show a [ToastPopup] for this if severity is at least [isWarnUser][RulesetErrorList.isWarnUser].
|
||||
*
|
||||
* Adds an appropriate header to [getErrorText][RulesetErrorList.getErrorText],
|
||||
* exists mainly to centralize those strings.
|
||||
*/
|
||||
fun RulesetErrorList.showWarnOrErrorToast(screen: BaseScreen) {
|
||||
if (!isWarnUser()) return
|
||||
val headerText =
|
||||
if (isError()) "The mod combination you selected is «RED»incorrectly defined!«»"
|
||||
else "{The mod combination you selected «GOLD»has problems«».}\n{You can play it, but «GOLDENROD»don't expect everything to work!«»}"
|
||||
val toastMessage = headerText.tr() + "\n\n{" + getErrorText() + "}"
|
||||
for (oldToast in screen.popups.filterIsInstance<ToastPopup>()) { oldToast.close() }
|
||||
ToastPopup(toastMessage, screen, 5000L)
|
||||
}
|
@ -20,17 +20,17 @@ import com.unciv.models.metadata.GameSetupInfo
|
||||
import com.unciv.models.ruleset.RulesetCache
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.components.ExpanderTab
|
||||
import com.unciv.ui.components.input.KeyCharAndCode
|
||||
import com.unciv.ui.components.extensions.addSeparator
|
||||
import com.unciv.ui.components.extensions.addSeparatorVertical
|
||||
import com.unciv.ui.components.extensions.disable
|
||||
import com.unciv.ui.components.extensions.enable
|
||||
import com.unciv.ui.components.input.keyShortcuts
|
||||
import com.unciv.ui.components.input.onActivation
|
||||
import com.unciv.ui.components.input.onClick
|
||||
import com.unciv.ui.components.extensions.pad
|
||||
import com.unciv.ui.components.extensions.toLabel
|
||||
import com.unciv.ui.components.extensions.toTextButton
|
||||
import com.unciv.ui.components.input.KeyCharAndCode
|
||||
import com.unciv.ui.components.input.keyShortcuts
|
||||
import com.unciv.ui.components.input.onActivation
|
||||
import com.unciv.ui.components.input.onClick
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
import com.unciv.ui.popups.ConfirmPopup
|
||||
import com.unciv.ui.popups.Popup
|
||||
@ -38,8 +38,8 @@ import com.unciv.ui.popups.ToastPopup
|
||||
import com.unciv.ui.screens.basescreen.BaseScreen
|
||||
import com.unciv.ui.screens.basescreen.RecreateOnResize
|
||||
import com.unciv.ui.screens.pickerscreens.PickerScreen
|
||||
import com.unciv.utils.Log
|
||||
import com.unciv.utils.Concurrency
|
||||
import com.unciv.utils.Log
|
||||
import com.unciv.utils.launchOnGLThread
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import java.net.URL
|
||||
@ -79,17 +79,17 @@ class NewGameScreen(
|
||||
updatePlayerPickerRandomLabel = { playerPickerTable.updateRandomNumberLabel() }
|
||||
)
|
||||
mapOptionsTable = MapOptionsTable(this)
|
||||
pickerPane.closeButton.onActivation {
|
||||
closeButton.onActivation {
|
||||
mapOptionsTable.cancelBackgroundJobs()
|
||||
game.popScreen()
|
||||
}
|
||||
pickerPane.closeButton.keyShortcuts.add(KeyCharAndCode.BACK)
|
||||
closeButton.keyShortcuts.add(KeyCharAndCode.BACK)
|
||||
|
||||
if (isPortrait) initPortrait()
|
||||
else initLandscape()
|
||||
|
||||
pickerPane.bottomTable.background = skinStrings.getUiBackground("NewGameScreen/BottomTable", tintColor = skinStrings.skinConfig.clearColor)
|
||||
pickerPane.topTable.background = skinStrings.getUiBackground("NewGameScreen/TopTable", tintColor = skinStrings.skinConfig.clearColor)
|
||||
bottomTable.background = skinStrings.getUiBackground("NewGameScreen/BottomTable", tintColor = skinStrings.skinConfig.clearColor)
|
||||
topTable.background = skinStrings.getUiBackground("NewGameScreen/TopTable", tintColor = skinStrings.skinConfig.clearColor)
|
||||
|
||||
if (UncivGame.Current.settings.lastGameSetup != null) {
|
||||
rightSideGroup.addActorAt(0, VerticalGroup().padBottom(5f))
|
||||
@ -166,6 +166,20 @@ class NewGameScreen(
|
||||
return
|
||||
}
|
||||
|
||||
val modCheckResult = newGameOptionsTable.modCheckboxes.savedModcheckResult
|
||||
newGameOptionsTable.modCheckboxes.savedModcheckResult = null
|
||||
if (modCheckResult != null) {
|
||||
AcceptModErrorsPopup(
|
||||
this, modCheckResult,
|
||||
restoreDefault = { newGameOptionsTable.resetRuleset() },
|
||||
action = {
|
||||
gameSetupInfo.gameParameters.acceptedModCheckErrors = modCheckResult
|
||||
onStartGameClicked()
|
||||
}
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
Gdx.input.inputProcessor = null // remove input processing - nothing will be clicked!
|
||||
|
||||
if (mapOptionsTable.mapTypeSelectBox.selected.value == MapGeneratedMainType.custom) {
|
||||
|
@ -20,7 +20,8 @@ import com.unciv.ui.components.extensions.toTextButton
|
||||
class PickerPane(
|
||||
disableScroll: Boolean = false,
|
||||
) : Table() {
|
||||
/** The close button on the lower left of [bottomTable], see [setDefaultCloseAction] */
|
||||
/** The close button on the lower left of [bottomTable], see [PickerScreen.setDefaultCloseAction].
|
||||
* Note if you don't use that helper, you'll need to do both click and keyboard support yourself. */
|
||||
val closeButton = Constants.close.toTextButton()
|
||||
/** A scrollable wrapped Label you can use to show descriptions in the [bottomTable], starts empty */
|
||||
val descriptionLabel = "".toLabel()
|
||||
|
@ -20,6 +20,8 @@ open class PickerScreen(disableScroll: Boolean = false) : BaseScreen() {
|
||||
|
||||
/** @see PickerPane.topTable */
|
||||
val topTable by pickerPane::topTable
|
||||
/** @see PickerPane.bottomTable */
|
||||
val bottomTable by pickerPane::bottomTable
|
||||
/** @see PickerPane.scrollPane */
|
||||
val scrollPane by pickerPane::scrollPane
|
||||
/** @see PickerPane.splitPane */
|
||||
|
@ -1,13 +1,13 @@
|
||||
# Uniques
|
||||
Simple unique parameters are explained by mouseover. Complex parameters are explained in [Unique parameter types](../Unique-parameters)
|
||||
## Triggerable uniques
|
||||
??? example "Free [baseUnitFilter] appears"
|
||||
Example: "Free [Melee] appears"
|
||||
??? example "Free [unit] appears"
|
||||
Example: "Free [Musketman] appears"
|
||||
|
||||
Applicable to: Triggerable
|
||||
|
||||
??? example "[amount] free [baseUnitFilter] units appear"
|
||||
Example: "[3] free [Melee] units appear"
|
||||
??? example "[amount] free [unit] units appear"
|
||||
Example: "[3] free [Musketman] units appear"
|
||||
|
||||
Applicable to: Triggerable
|
||||
|
||||
@ -1655,8 +1655,8 @@ Simple unique parameters are explained by mouseover. Complex parameters are expl
|
||||
Applicable to: Resource
|
||||
|
||||
## Ruins uniques
|
||||
??? example "Free [baseUnitFilter] found in the ruins"
|
||||
Example: "Free [Melee] found in the ruins"
|
||||
??? example "Free [unit] found in the ruins"
|
||||
Example: "Free [Musketman] found in the ruins"
|
||||
|
||||
Applicable to: Ruins
|
||||
|
||||
|
Reference in New Issue
Block a user