Only *display* first 5 missing mods but auto-download all (#9543)

* Only *display* first 5 missing mods but autodownload all

* Fix removeMissingTerrainModReferences

* Linting
This commit is contained in:
SomeTroglodyte 2023-06-08 11:13:22 +02:00 committed by GitHub
parent fe1b5825bb
commit 86aa3b842b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 48 additions and 43 deletions

View File

@ -544,12 +544,10 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion
// any mod the saved game lists that is currently not installed causes null pointer // any mod the saved game lists that is currently not installed causes null pointer
// exceptions in this routine unless it contained no new objects or was very simple. // exceptions in this routine unless it contained no new objects or was very simple.
// Player's fault, so better complain early: // Player's fault, so better complain early:
val missingMods = (gameParameters.mods + gameParameters.baseRuleset) val missingMods = (listOf(gameParameters.baseRuleset) + gameParameters.mods)
.filterNot { it in ruleset.mods } .filterNot { it in ruleset.mods }
.joinToString(limit = 5) { it } if (missingMods.isNotEmpty())
if (missingMods.isNotEmpty()) {
throw MissingModsException(missingMods) throw MissingModsException(missingMods)
}
removeMissingModReferences() removeMissingModReferences()

View File

@ -19,6 +19,14 @@ open class UncivShowableException(
override fun getLocalizedMessage() = message.tr() override fun getLocalizedMessage() = message.tr()
} }
/** An [Exception] indicating a game or map cannot be loaded because [mods][com.unciv.models.metadata.GameParameters.mods] are missing.
* @param missingMods Any [Iterable] or [Collection] of Strings - will be stored entirely,
* but be included in the Exception's message only up to its five first elements.
*/
class MissingModsException( class MissingModsException(
val missingMods: String val missingMods: Iterable<String>
) : UncivShowableException("Missing mods: [$missingMods]") ) : UncivShowableException("Missing mods: [${shorten(missingMods)}]") {
companion object {
private fun shorten(missingMods: Iterable<String>) = missingMods.joinToString(limit = 5) { it }
}
}

View File

@ -464,16 +464,13 @@ class TileMap(initialCapacity: Int = 10) : IsPartOfGameInfoSerialization {
} }
fun removeMissingTerrainModReferences(ruleSet: Ruleset) { fun removeMissingTerrainModReferences(ruleSet: Ruleset) {
// This will run before setTransients, so do not rely e.g. on Tile.ruleset being available.
// That rules out Tile.removeTerrainFeature, which refreshes object/unique caches
for (tile in this.values) { for (tile in this.values) {
for (terrainFeature in tile.terrainFeatures.filter { !ruleSet.terrains.containsKey(it) }) tile.removeMissingTerrainModReferences(ruleSet)
tile.removeTerrainFeature(terrainFeature)
if (tile.resource != null && !ruleSet.tileResources.containsKey(tile.resource!!))
tile.resource = null
if (tile.improvement != null && !ruleSet.tileImprovements.containsKey(tile.improvement!!))
tile.changeImprovement(null)
} }
for (startingLocation in startingLocations.toList()) for (startingLocation in startingLocations.toList())
if (startingLocation.nation !in ruleSet.nations.keys) if (startingLocation.nation !in ruleSet.nations)
startingLocations.remove(startingLocation) startingLocations.remove(startingLocation)
} }

View File

@ -821,7 +821,7 @@ open class Tile : IsPartOfGameInfoSerialization {
else probability else probability
} }
fun setTerrainFeatures(terrainFeatureList:List<String>) { fun setTerrainFeatures(terrainFeatureList: List<String>) {
terrainFeatures = terrainFeatureList terrainFeatures = terrainFeatureList
terrainFeatureObjects = terrainFeatureList.mapNotNull { ruleset.terrains[it] } terrainFeatureObjects = terrainFeatureList.mapNotNull { ruleset.terrains[it] }
allTerrains = sequence { allTerrains = sequence {
@ -843,7 +843,7 @@ open class Tile : IsPartOfGameInfoSerialization {
terrainUniqueMap = newUniqueMap terrainUniqueMap = newUniqueMap
} }
fun addTerrainFeature(terrainFeature:String) = fun addTerrainFeature(terrainFeature: String) =
setTerrainFeatures(ArrayList(terrainFeatures).apply { add(terrainFeature) }) setTerrainFeatures(ArrayList(terrainFeatures).apply { add(terrainFeature) })
fun removeTerrainFeature(terrainFeature: String) = fun removeTerrainFeature(terrainFeature: String) =
@ -852,6 +852,16 @@ open class Tile : IsPartOfGameInfoSerialization {
fun removeTerrainFeatures() = fun removeTerrainFeatures() =
setTerrainFeatures(listOf()) setTerrainFeatures(listOf())
/** Clean stuff missing in [ruleset] - called from [TileMap.removeMissingTerrainModReferences]
* Must be able to run before [setTransients] - and does not need to fix transients.
*/
fun removeMissingTerrainModReferences(ruleset: Ruleset) {
terrainFeatures = terrainFeatures.filter { it in ruleset.terrains }
if (resource != null && resource !in ruleset.tileResources)
resource = null
if (improvement != null && improvement !in ruleset.tileImprovements)
changeImprovement(null)
}
/** If the unit isn't in the ruleset we can't even know what type of unit this is! So check each place /** If the unit isn't in the ruleset we can't even know what type of unit this is! So check each place
* This works with no transients so can be called from gameInfo.setTransients with no fear * This works with no transients so can be called from gameInfo.setTransients with no fear

View File

@ -4,7 +4,7 @@ import com.badlogic.gdx.Gdx
import com.badlogic.gdx.files.FileHandle import com.badlogic.gdx.files.FileHandle
import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.unciv.Constants import com.unciv.logic.MissingModsException
import com.unciv.logic.files.MapSaver import com.unciv.logic.files.MapSaver
import com.unciv.logic.UncivShowableException import com.unciv.logic.UncivShowableException
import com.unciv.models.ruleset.RulesetCache import com.unciv.models.ruleset.RulesetCache
@ -112,29 +112,21 @@ class MapEditorLoadTab(
val map = MapSaver.loadMap(chosenMap!!) val map = MapSaver.loadMap(chosenMap!!)
if (!isActive) return if (!isActive) return
val missingMods = map.mapParameters.mods.filter { it !in RulesetCache }.toMutableList() // For deprecated maps, set the base ruleset field if it's still saved in the mods field
// [TEMPORARY] conversion of old maps with a base ruleset contained in the mods val modBaseRuleset = map.mapParameters.mods.firstOrNull { RulesetCache[it]?.modOptions?.isBaseRuleset == true }
val newBaseRuleset = map.mapParameters.mods.filter { it !in missingMods }.firstOrNull { RulesetCache[it]!!.modOptions.isBaseRuleset } if (modBaseRuleset != null) {
if (newBaseRuleset != null) map.mapParameters.baseRuleset = newBaseRuleset map.mapParameters.baseRuleset = modBaseRuleset
// map.mapParameters.mods -= modBaseRuleset
if (map.mapParameters.baseRuleset !in RulesetCache) missingMods += map.mapParameters.baseRuleset }
if (missingMods.isNotEmpty()) { val missingMods = (setOf(map.mapParameters.baseRuleset) + map.mapParameters.mods)
Concurrency.runOnGLThread { .filterNot { it in RulesetCache }
needPopup = false if (missingMods.isNotEmpty())
popup?.close() throw MissingModsException(missingMods)
ToastPopup("Missing mods: [${missingMods.joinToString()}]", editorScreen)
} Concurrency.runOnGLThread {
} else Concurrency.runOnGLThread {
Gdx.input.inputProcessor = null // This is to stop ANRs happening here, until the map editor screen sets up. Gdx.input.inputProcessor = null // This is to stop ANRs happening here, until the map editor screen sets up.
try { try {
// For deprecated maps, set the base ruleset field if it's still saved in the mods field
val modBaseRuleset = map.mapParameters.mods.firstOrNull { RulesetCache[it]!!.modOptions.isBaseRuleset }
if (modBaseRuleset != null) {
map.mapParameters.baseRuleset = modBaseRuleset
map.mapParameters.mods -= modBaseRuleset
}
val ruleset = RulesetCache.getComplexRuleset(map.mapParameters) val ruleset = RulesetCache.getComplexRuleset(map.mapParameters)
val rulesetIncompatibilities = map.getRulesetIncompatibility(ruleset) val rulesetIncompatibilities = map.getRulesetIncompatibility(ruleset)
if (rulesetIncompatibilities.isNotEmpty()) { if (rulesetIncompatibilities.isNotEmpty()) {

View File

@ -35,7 +35,7 @@ class LoadGameScreen : LoadOrSaveScreen() {
private val copySavedGameToClipboardButton = getCopyExistingSaveToClipboardButton() private val copySavedGameToClipboardButton = getCopyExistingSaveToClipboardButton()
private val errorLabel = "".toLabel(Color.RED).apply { isVisible = false } private val errorLabel = "".toLabel(Color.RED).apply { isVisible = false }
private val loadMissingModsButton = getLoadMissingModsButton() private val loadMissingModsButton = getLoadMissingModsButton()
private var missingModsToLoad = "" private var missingModsToLoad: Iterable<String> = emptyList()
companion object { companion object {
private const val loadGame = "Load game" private const val loadGame = "Load game"
@ -61,11 +61,11 @@ class LoadGameScreen : LoadOrSaveScreen() {
isUserFixable = false isUserFixable = false
} }
is FileNotFoundException -> { is FileNotFoundException -> {
if (ex.cause?.message?.contains("Permission denied") == true) { isUserFixable = if (ex.cause?.message?.contains("Permission denied") == true) {
errorText.append("You do not have sufficient permissions to access the file.".tr()) errorText.append("You do not have sufficient permissions to access the file.".tr())
isUserFixable = true true
} else { } else {
isUserFixable = false false
} }
} }
else -> { else -> {
@ -245,8 +245,8 @@ class LoadGameScreen : LoadOrSaveScreen() {
descriptionLabel.setText(Constants.loading.tr()) descriptionLabel.setText(Constants.loading.tr())
Concurrency.runOnNonDaemonThreadPool(downloadMissingMods) { Concurrency.runOnNonDaemonThreadPool(downloadMissingMods) {
try { try {
val mods = missingModsToLoad.replace(' ', '-').lowercase().splitToSequence(",-") for (rawName in missingModsToLoad) {
for (modName in mods) { val modName = rawName.replace(' ', '-').lowercase()
val repos = Github.tryGetGithubReposWithTopic(10, 1, modName) val repos = Github.tryGetGithubReposWithTopic(10, 1, modName)
?: throw UncivShowableException("Could not download mod list.") ?: throw UncivShowableException("Could not download mod list.")
val repo = repos.items.firstOrNull { it.name.lowercase() == modName } val repo = repos.items.firstOrNull { it.name.lowercase() == modName }
@ -264,7 +264,7 @@ class LoadGameScreen : LoadOrSaveScreen() {
} }
launchOnGLThread { launchOnGLThread {
RulesetCache.loadRulesets() RulesetCache.loadRulesets()
missingModsToLoad = "" missingModsToLoad = emptyList()
loadMissingModsButton.isVisible = false loadMissingModsButton.isVisible = false
errorLabel.isVisible = false errorLabel.isVisible = false
rightSideTable.pack() rightSideTable.pack()