Fix mod check not offering to auto-update Uniques for extension mods (#13535)

* Reenable Unique auto-update by fixing the validation on the replacement

* Make mod name search in Mod Checker find those disabled placeholders too
This commit is contained in:
SomeTroglodyte 2025-06-29 17:02:12 +02:00 committed by GitHub
parent dbf28c675a
commit be354785c4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 62 additions and 59 deletions

View File

@ -174,15 +174,15 @@ object RulesetCache : HashMap<String, Ruleset>() {
mods: LinkedHashSet<String>, mods: LinkedHashSet<String>,
baseRuleset: String? = null, baseRuleset: String? = null,
tryFixUnknownUniques: Boolean = false tryFixUnknownUniques: Boolean = false
): RulesetErrorList { ): Pair<Ruleset?, RulesetErrorList> {
return try { return try {
val newRuleset = getComplexRuleset(mods, baseRuleset) val newRuleset = getComplexRuleset(mods, baseRuleset)
newRuleset.modOptions.isBaseRuleset = true // This is so the checkModLinks finds all connections newRuleset.modOptions.isBaseRuleset = true // This is so the checkModLinks finds all connections
newRuleset.getErrorList(tryFixUnknownUniques) newRuleset to newRuleset.getErrorList(tryFixUnknownUniques)
} catch (ex: UncivShowableException) { } catch (ex: UncivShowableException) {
// This happens if a building is dependent on a tech not in the base ruleset // This happens if a building is dependent on a tech not in the base ruleset
// because newRuleset.updateBuildingCosts() in getComplexRuleset() throws an error // because newRuleset.updateBuildingCosts() in getComplexRuleset() throws an error
RulesetErrorList.of(ex.message, RulesetErrorSeverity.Error) null to RulesetErrorList.of(ex.message, RulesetErrorSeverity.Error)
} }
} }
} }

View File

@ -4,13 +4,17 @@ import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.RulesetFile import com.unciv.models.ruleset.RulesetFile
import com.unciv.models.ruleset.unique.Unique import com.unciv.models.ruleset.unique.Unique
import com.unciv.utils.Log import com.unciv.utils.Log
import com.unciv.utils.debug
object UniqueAutoUpdater { object UniqueAutoUpdater {
/** Apply Unique replacements.
* @param mod The Ruleset to process. It must have a `folderLocation`.
* @param replaceableUniques A map old to new. The default is only for use with the 'mod-ci' command line argument from `DeskTopLauncher`.
* The ModCheck UI will have produced this map using a combined Ruleset for replacement validation when appropriate.
*/
fun autoupdateUniques( fun autoupdateUniques(
mod: Ruleset, mod: Ruleset,
replaceableUniques: HashMap<String, String> = getDeprecatedReplaceableUniques(mod) replaceableUniques: HashMap<String, String> = getDeprecatedReplaceableUniques(mod, mod)
) { ) {
val filesToReplace = RulesetFile.entries.map { it.filename } val filesToReplace = RulesetFile.entries.map { it.filename }
val jsonFolder = mod.folderLocation!!.child("jsons") val jsonFolder = mod.folderLocation!!.child("jsons")
@ -26,12 +30,18 @@ object UniqueAutoUpdater {
} }
} }
/** Determine possible auto-corrections from Deprecation annotations.
* @param mod The Ruleset to scan
fun getDeprecatedReplaceableUniques(mod: Ruleset): HashMap<String, String> { * @param rulesetForValidation The Ruleset to test potential fixed Uniques against. Can be same as [mod],
* otherwise it should be a complex Ruleset combining [mod] with an appropriate base ruleset.
* @return A map of old to new. Will not include 'new' values that don't pass Unique validation against [rulesetForValidation].
*/
fun getDeprecatedReplaceableUniques(mod: Ruleset, rulesetForValidation: Ruleset): HashMap<String, String> {
val allUniques = mod.allUniques() val allUniques = mod.allUniques()
val allDeprecatedUniques = HashSet<String>() val allDeprecatedUniques = HashSet<String>()
val deprecatedUniquesToReplacementText = HashMap<String, String>() val deprecatedUniquesToReplacementText = HashMap<String, String>()
val validator = UniqueValidator(rulesetForValidation)
val reportRulesetSpecificErrors = rulesetForValidation.modOptions.isBaseRuleset
val deprecatedUniques = allUniques val deprecatedUniques = allUniques
.filter { it.getDeprecationAnnotation() != null } .filter { it.getDeprecationAnnotation() != null }
@ -52,30 +62,13 @@ object UniqueAutoUpdater {
uniqueReplacementText += " <${conditional.text}>" uniqueReplacementText += " <${conditional.text}>"
val replacementUnique = Unique(uniqueReplacementText) val replacementUnique = Unique(uniqueReplacementText)
val modInvariantErrors = UniqueValidator(mod).checkUnique( val modErrors = validator.checkUnique(replacementUnique, false, null, reportRulesetSpecificErrors)
replacementUnique, for (error in modErrors)
false, Log.error("ModError: %s - %s", error.text, error.errorSeverityToReport)
null, if (modErrors.isNotEmpty()) continue // errors means no autoreplace
true
)
for (error in modInvariantErrors)
Log.error("ModInvariantError: %s - %s", error.text, error.errorSeverityToReport)
if (modInvariantErrors.isNotEmpty()) continue // errors means no autoreplace
if (mod.modOptions.isBaseRuleset) {
val modSpecificErrors = UniqueValidator(mod).checkUnique(
replacementUnique,
false,
null,
true
)
for (error in modSpecificErrors)
Log.error("ModSpecificError: %s - %s", error.text, error.errorSeverityToReport)
if (modSpecificErrors.isNotEmpty()) continue
}
deprecatedUniquesToReplacementText[deprecatedUnique.text] = uniqueReplacementText deprecatedUniquesToReplacementText[deprecatedUnique.text] = uniqueReplacementText
debug("Replace \"%s\" with \"%s\"", deprecatedUnique.text, uniqueReplacementText) Log.debug("Replace \"%s\" with \"%s\"", deprecatedUnique.text, uniqueReplacementText)
} }
return deprecatedUniquesToReplacementText return deprecatedUniquesToReplacementText

View File

@ -50,10 +50,12 @@ class ModCheckTab(
private var modCheckBaseSelect: TranslatedSelectBox? = null private var modCheckBaseSelect: TranslatedSelectBox? = null
private val searchModsTextField: UncivTextField private val searchModsTextField: UncivTextField
private val modCheckResultTable = Table() private val modCheckResultTable = Table().apply {
defaults().uniformX().fillX().top()
}
private val loadingImage = LoadingImage(48f, LoadingImage.Style(loadingColor = Color.SCARLET)) private val loadingImage = LoadingImage(48f, LoadingImage.Style(loadingColor = Color.SCARLET))
/** Used to repopulate modCheckResultTable when searching */ /** Used to repopulate modCheckResultTable when searching */
private val modResultExpanderTabs = ArrayList<ExpanderTab>() private val modResultTables = ArrayList<Table>()
private var runningCheck: Job? = null private var runningCheck: Job? = null
@ -65,7 +67,8 @@ class ModCheckTab(
private val emptyRuleset = Ruleset() private val emptyRuleset = Ruleset()
init { init {
defaults().pad(10f).align(Align.top) top()
defaults().pad(10f).top()
fixedContent.defaults().pad(10f).align(Align.top) fixedContent.defaults().pad(10f).align(Align.top)
val reloadModsButton = "Reload mods".toTextButton().onClick(::runAction) val reloadModsButton = "Reload mods".toTextButton().onClick(::runAction)
@ -127,18 +130,19 @@ class ModCheckTab(
loadingImage.show() loadingImage.show()
val openedExpanderTitles = modResultExpanderTabs val openedExpanderTitles = modResultTables
.filterIsInstance<ExpanderTab>()
.filter { it.isOpen }.map { it.title }.toSet() .filter { it.isOpen }.map { it.title }.toSet()
modCheckResultTable.clear() modCheckResultTable.clear()
modResultExpanderTabs.clear() modResultTables.clear()
val loadingErrors = RulesetCache.loadRulesets() val loadingErrors = RulesetCache.loadRulesets()
if (loadingErrors.isNotEmpty()) { if (loadingErrors.isNotEmpty()) {
val errorTable = Table().apply { defaults().pad(2f) } val errorTable = Table().apply { defaults().pad(2f) }
for (loadingError in loadingErrors) for (loadingError in loadingErrors)
errorTable.add(loadingError.toLabel()).width(stage.width / 2).row() errorTable.add(loadingError.toLabel()).width(stage.width / 2).row()
modCheckResultTable.add(errorTable) modCheckResultTable.add(errorTable).row()
} }
runningCheck = Concurrency.run("ModChecker") { runningCheck = Concurrency.run("ModChecker") {
@ -162,9 +166,14 @@ class ModCheckTab(
if (!shouldCheckMod(mod, baseForThisMod)) continue if (!shouldCheckMod(mod, baseForThisMod)) continue
if (!isActive) break if (!isActive) break
val modLinks = val (combinedRuleset, modLinks) =
if (baseForThisMod == MOD_CHECK_WITHOUT_BASE) mod.getErrorList(tryFixUnknownUniques = true) if (baseForThisMod == MOD_CHECK_WITHOUT_BASE) {
else RulesetCache.checkCombinedModLinks(linkedSetOf(mod.name), baseForThisMod, tryFixUnknownUniques = true) mod to mod.getErrorList(tryFixUnknownUniques = true)
} else {
val (ruleset, errors) = RulesetCache.checkCombinedModLinks(linkedSetOf(mod.name), baseForThisMod)
(ruleset ?: mod) to errors
}
if (!isActive) break if (!isActive) break
modLinks.sortByDescending { it.errorSeverityToReport } modLinks.sortByDescending { it.errorSeverityToReport }
@ -172,7 +181,7 @@ class ModCheckTab(
if (!modLinks.isNotOK()) modLinks.add("No problems found.".tr(), RulesetErrorSeverity.OK, sourceObject = null) if (!modLinks.isNotOK()) modLinks.add("No problems found.".tr(), RulesetErrorSeverity.OK, sourceObject = null)
launchOnGLThread { launchOnGLThread {
addModResult(mod, baseForThisMod, modLinks, mod.name in openedExpanderTitles) addModResult(mod, combinedRuleset, modLinks, mod.name in openedExpanderTitles)
} }
} }
@ -189,8 +198,8 @@ class ModCheckTab(
// The last check, whether finished or not, included all mods we want to filter // The last check, whether finished or not, included all mods we want to filter
synchronized(modCheckResultTable) { synchronized(modCheckResultTable) {
modCheckResultTable.clear() modCheckResultTable.clear()
for (expanderTab in modResultExpanderTabs) { for (expanderTab in modResultTables) {
if (expanderTab.title.filterApplies()) if (expanderTab.name.filterApplies())
modCheckResultTable.add(expanderTab).row() modCheckResultTable.add(expanderTab).row()
} }
} }
@ -220,7 +229,17 @@ class ModCheckTab(
?.name ?.name
} }
private fun addModResult(mod: Ruleset, base: String, modLinks: RulesetErrorList, startsOutOpened: Boolean) { private fun addResultCommon(mod: Ruleset, table: Table) {
table.name = mod.name // used when searching
synchronized(modCheckResultTable) {
modResultTables.add(table)
if (mod.name.filterApplies())
modCheckResultTable.add(table).row()
}
}
private fun addModResult(mod: Ruleset, combinedRuleset: Ruleset, modLinks: RulesetErrorList, startsOutOpened: Boolean) {
// When the options popup is already closed before this postRunnable is run, // When the options popup is already closed before this postRunnable is run,
// Don't add the labels, as otherwise the game will crash // Don't add the labels, as otherwise the game will crash
if (stage == null) return if (stage == null) return
@ -238,11 +257,11 @@ class ModCheckTab(
it.defaults().pad(10f) it.defaults().pad(10f)
val openUniqueBuilderButton = "Open unique builder".toTextButton() val openUniqueBuilderButton = "Open unique builder".toTextButton()
openUniqueBuilderButton.onClick { openUniqueBuilder(mod, base) } openUniqueBuilderButton.onClick { openUniqueBuilder(combinedRuleset) }
it.add(openUniqueBuilderButton).row() it.add(openUniqueBuilderButton).row()
if (severity != RulesetErrorSeverity.OK && mod.folderLocation != null) { if (severity != RulesetErrorSeverity.OK && mod.folderLocation != null) {
val replaceableUniques = UniqueAutoUpdater.getDeprecatedReplaceableUniques(mod) val replaceableUniques = UniqueAutoUpdater.getDeprecatedReplaceableUniques(mod, combinedRuleset)
if (replaceableUniques.isNotEmpty()) if (replaceableUniques.isNotEmpty())
it.add("Autoupdate mod uniques".toTextButton() it.add("Autoupdate mod uniques".toTextButton()
.onClick { autoUpdateUniques(screen, mod, replaceableUniques) }).row() .onClick { autoUpdateUniques(screen, mod, replaceableUniques) }).row()
@ -260,12 +279,7 @@ class ModCheckTab(
}).row() }).row()
} }
expanderTab.header.left() expanderTab.header.left()
addResultCommon(mod, expanderTab)
synchronized(modCheckResultTable) {
modResultExpanderTabs.add(expanderTab)
if (mod.name.filterApplies())
modCheckResultTable.add(expanderTab).row()
}
} }
private fun addDisabledPlaceholder(mod: Ruleset) { private fun addDisabledPlaceholder(mod: Ruleset) {
@ -277,15 +291,11 @@ class ModCheckTab(
add(mod.name.toLabel(Color.LIGHT_GRAY, Constants.headingFontSize, alignment = Align.left)).left().grow() add(mod.name.toLabel(Color.LIGHT_GRAY, Constants.headingFontSize, alignment = Align.left)).left().grow()
addTooltip("Requirements could not be determined.\nChoose a base to check this Mod.", 16f, targetAlign = Align.top) addTooltip("Requirements could not be determined.\nChoose a base to check this Mod.", 16f, targetAlign = Align.top)
} }
synchronized(modCheckResultTable) { addResultCommon(mod, table)
modCheckResultTable.add(table).growX().row()
}
} }
private fun openUniqueBuilder(mod: Ruleset, base: String) { private fun openUniqueBuilder(combinedRuleset: Ruleset) {
val ruleset = if (base == MOD_CHECK_WITHOUT_BASE) mod UncivGame.Current.pushScreen(UniqueBuilderScreen(combinedRuleset))
else RulesetCache.getComplexRuleset(linkedSetOf(mod.name), base)
UncivGame.Current.pushScreen(UniqueBuilderScreen(ruleset))
} }
private fun autoUpdateUniques(screen: BaseScreen, mod: Ruleset, replaceableUniques: HashMap<String, String>) { private fun autoUpdateUniques(screen: BaseScreen, mod: Ruleset, replaceableUniques: HashMap<String, String>) {

View File

@ -143,7 +143,7 @@ class ModCheckboxTable(
/** Runs in parallel thread */ /** Runs in parallel thread */
private fun complexModCheckReturnsErrors(): Boolean { private fun complexModCheckReturnsErrors(): Boolean {
// 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 savedModcheckResult = null
return false return false