From c01d72feb6cc613c69c5869c4d2320d79d2fc895 Mon Sep 17 00:00:00 2001 From: SomeTroglodyte <63000004+SomeTroglodyte@users.noreply.github.com> Date: Wed, 20 Aug 2025 12:09:21 +0200 Subject: [PATCH] Harden new game screen against bad scenarios (#13826) * Refactor and lint ScenarioSelectTable * Catch and display scenario file parsing errors --- .../jsons/translations/template.properties | 1 + .../screens/newgamescreen/MapOptionsTable.kt | 60 +--------------- .../newgamescreen/ScenarioSelectTable.kt | 71 +++++++++++++++++++ 3 files changed, 74 insertions(+), 58 deletions(-) create mode 100644 core/src/com/unciv/ui/screens/newgamescreen/ScenarioSelectTable.kt diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index d897e3a109..924fa3258f 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -379,6 +379,7 @@ Custom = Map Generation Type = Enabled Map Generation Types = Example map = +Scenario file [name] is invalid. = # Map types Default = diff --git a/core/src/com/unciv/ui/screens/newgamescreen/MapOptionsTable.kt b/core/src/com/unciv/ui/screens/newgamescreen/MapOptionsTable.kt index f9c61cc01a..0f261f6123 100644 --- a/core/src/com/unciv/ui/screens/newgamescreen/MapOptionsTable.kt +++ b/core/src/com/unciv/ui/screens/newgamescreen/MapOptionsTable.kt @@ -1,67 +1,11 @@ package com.unciv.ui.screens.newgamescreen -import com.badlogic.gdx.files.FileHandle import com.badlogic.gdx.scenes.scene2d.ui.Table -import com.unciv.Constants -import com.unciv.logic.GameInfoPreview import com.unciv.logic.map.MapGeneratedMainType -import com.unciv.models.ruleset.Ruleset import com.unciv.ui.components.extensions.toLabel import com.unciv.ui.components.input.onChange import com.unciv.ui.components.widgets.TranslatedSelectBox import com.unciv.ui.screens.basescreen.BaseScreen -import com.unciv.utils.Concurrency - -class ScenarioSelectTable(val newGameScreen: NewGameScreen) : Table() { - - data class ScenarioData(val name:String, val file: FileHandle){ - var preview: GameInfoPreview? = null - } - - val scenarios = HashMap() - lateinit var selectedScenario: ScenarioData - var scenarioSelectBox: TranslatedSelectBox? = null - - init { - // Only the first so it's fast - val firstScenarioFile = newGameScreen.game.files.getScenarioFiles().firstOrNull() - if (firstScenarioFile != null) { - createScenarioSelectBox(listOf(firstScenarioFile)) - Concurrency.run { - val scenarioFiles = newGameScreen.game.files.getScenarioFiles().toList() - Concurrency.runOnGLThread { - createScenarioSelectBox(scenarioFiles) - } - } - } - } - - private fun createScenarioSelectBox(scenarioFiles: List>) { - for ((file, _) in scenarioFiles) - scenarios[file.name()] = ScenarioData(file.name(), file) - - scenarioSelectBox = TranslatedSelectBox(scenarios.keys.sorted(), scenarios.keys.first()) - scenarioSelectBox!!.onChange { selectScenario() } - clear() - add(scenarioSelectBox) - } - - fun selectScenario(){ - val scenario = scenarios[scenarioSelectBox!!.selected.value]!! - val preload = if (scenario.preview != null) scenario.preview!! else { - val preview = newGameScreen.game.files.loadGamePreviewFromFile(scenario.file) - scenario.preview = preview - preview - } - newGameScreen.gameSetupInfo.gameParameters.players = preload.gameParameters.players - .apply { removeAll { it.chosenCiv == Constants.spectator } } - newGameScreen.gameSetupInfo.gameParameters.baseRuleset = preload.gameParameters.baseRuleset - newGameScreen.gameSetupInfo.gameParameters.mods = preload.gameParameters.mods - newGameScreen.tryUpdateRuleset(true) - newGameScreen.playerPickerTable.update() - selectedScenario = scenario - } -} class MapOptionsTable(private val newGameScreen: NewGameScreen) : Table() { @@ -126,8 +70,8 @@ class MapOptionsTable(private val newGameScreen: NewGameScreen) : Table() { add(mapTypeSelectWrapper).pad(10f).fillX().row() add(mapTypeSpecificTable).row() } - - fun getSelectedScenario(): ScenarioSelectTable.ScenarioData? { + + internal fun getSelectedScenario(): ScenarioSelectTable.ScenarioData? { if (mapTypeSelectBox.selected.value != MapGeneratedMainType.scenario) return null return scenarioOptionsTable.selectedScenario } diff --git a/core/src/com/unciv/ui/screens/newgamescreen/ScenarioSelectTable.kt b/core/src/com/unciv/ui/screens/newgamescreen/ScenarioSelectTable.kt new file mode 100644 index 0000000000..655da8190d --- /dev/null +++ b/core/src/com/unciv/ui/screens/newgamescreen/ScenarioSelectTable.kt @@ -0,0 +1,71 @@ +package com.unciv.ui.screens.newgamescreen + +import com.badlogic.gdx.files.FileHandle +import com.badlogic.gdx.scenes.scene2d.ui.Table +import com.badlogic.gdx.utils.SerializationException +import com.unciv.Constants +import com.unciv.logic.GameInfoPreview +import com.unciv.logic.files.UncivFiles +import com.unciv.models.ruleset.Ruleset +import com.unciv.ui.components.input.onChange +import com.unciv.ui.components.widgets.TranslatedSelectBox +import com.unciv.ui.popups.ToastPopup +import com.unciv.utils.Concurrency + +internal class ScenarioSelectTable(private val newGameScreen: NewGameScreen) : Table() { + + data class ScenarioData(val name:String, val file: FileHandle) { + var preview: GameInfoPreview? = null + } + + val scenarios = HashMap() + var selectedScenario: ScenarioData? = null + private var scenarioSelectBox: TranslatedSelectBox? = null + + init { + // Only the first so it's fast + val firstScenarioFile = newGameScreen.game.files.getScenarioFiles().firstOrNull() + if (firstScenarioFile != null) { + createScenarioSelectBox(listOf(firstScenarioFile)) + Concurrency.run { + val scenarioFiles = newGameScreen.game.files.getScenarioFiles().toList() + Concurrency.runOnGLThread { + createScenarioSelectBox(scenarioFiles) + } + } + } + } + + private fun createScenarioSelectBox(scenarioFiles: List>) { + for ((file, _) in scenarioFiles) + scenarios[file.name()] = ScenarioData(file.name(), file) + + scenarioSelectBox = TranslatedSelectBox(scenarios.keys.sorted(), scenarios.keys.first()) + scenarioSelectBox!!.onChange { selectScenario() } + clear() + add(scenarioSelectBox) + } + + fun selectScenario() { + val scenario = scenarios[scenarioSelectBox!!.selected.value]!! + val preload = scenario.getCachedPreview() ?: return + newGameScreen.gameSetupInfo.gameParameters.players = preload.gameParameters.players + .apply { removeAll { it.chosenCiv == Constants.spectator } } + newGameScreen.gameSetupInfo.gameParameters.baseRuleset = preload.gameParameters.baseRuleset + newGameScreen.gameSetupInfo.gameParameters.mods = preload.gameParameters.mods + newGameScreen.tryUpdateRuleset(true) + newGameScreen.playerPickerTable.update() + selectedScenario = scenario + } + + private fun ScenarioData.getCachedPreview(): GameInfoPreview? { + if (preview == null) { + try { + preview = newGameScreen.game.files.loadGamePreviewFromFile(file) + } catch (_: SerializationException) { + ToastPopup("Scenario file [${file.name()}] is invalid.", newGameScreen) + } + } + return preview + } +}