Harden new game screen against bad scenarios (#13826)

* Refactor and lint ScenarioSelectTable

* Catch and display scenario file parsing errors
This commit is contained in:
SomeTroglodyte 2025-08-20 12:09:21 +02:00 committed by GitHub
parent 8c2fada877
commit c01d72feb6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 74 additions and 58 deletions

View File

@ -379,6 +379,7 @@ Custom =
Map Generation Type =
Enabled Map Generation Types =
Example map =
Scenario file [name] is invalid. =
# Map types
Default =

View File

@ -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<String, ScenarioData>()
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<Pair<FileHandle, Ruleset>>) {
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() {
@ -127,7 +71,7 @@ class MapOptionsTable(private val newGameScreen: NewGameScreen) : Table() {
add(mapTypeSpecificTable).row()
}
fun getSelectedScenario(): ScenarioSelectTable.ScenarioData? {
internal fun getSelectedScenario(): ScenarioSelectTable.ScenarioData? {
if (mapTypeSelectBox.selected.value != MapGeneratedMainType.scenario) return null
return scenarioOptionsTable.selectedScenario
}

View File

@ -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<String, ScenarioData>()
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<Pair<FileHandle, Ruleset>>) {
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
}
}