Handle maps with invalid mapSize more gracefully (#5190)

* Handle maps with invalid mapSize more gracefully

* Handle maps with invalid mapSize more gracefully - new game exception handling

* Handle maps with invalid mapSize more gracefully - moved version save init
This commit is contained in:
SomeTroglodyte
2021-09-12 18:15:21 +02:00
committed by GitHub
parent 03a7288656
commit af20124e5d
11 changed files with 108 additions and 30 deletions

View File

@ -272,6 +272,8 @@ Civilizations =
Map Type =
Map file =
Could not load map! =
Invalid map: Area ([area]) does not match saved dimensions ([dimensions]). =
The dimensions have now been fixed for you. =
Generated =
Existing =
Custom =
@ -310,7 +312,6 @@ Rare features richness =
Max Coast extension =
Biome areas extension =
Water level =
Reset to default =
Online Multiplayer =

View File

@ -10,17 +10,27 @@ object MapSaver {
fun json() = GameSaver.json()
private const val mapsFolder = "maps"
private const val saveZipped = true
var saveZipped = true
private fun getMap(mapName:String) = Gdx.files.local("$mapsFolder/$mapName")
fun mapFromSavedString(mapString: String): TileMap {
fun mapFromSavedString(mapString: String, checkSizeErrors: Boolean = true): TileMap {
val unzippedJson = try {
Gzip.unzip(mapString)
} catch (ex: Exception) {
mapString
}
return mapFromJson(unzippedJson)
return mapFromJson(unzippedJson).apply {
// old maps (rarely) can come with mapSize fields not matching tile list
if (checkSizeErrors && mapParameters.getArea() != values.size)
throw UncivShowableException("Invalid map: Area ([${values.size}]) does not match saved dimensions ([${mapParameters.displayMapDimensions()}]).")
// compatibility with rare maps saved with old mod names
if (!checkSizeErrors)
mapParameters.mods.filter { '-' in it }.forEach {
mapParameters.mods.remove(it)
mapParameters.mods.add(it.replace('-',' '))
}
}
}
fun mapToSavedString(tileMap: TileMap): String {
val mapJson = json().toJson(tileMap)
@ -31,8 +41,8 @@ object MapSaver {
getMap(mapName).writeString(mapToSavedString(tileMap), false)
}
fun loadMap(mapFile:FileHandle):TileMap {
return mapFromSavedString(mapFile.readString())
fun loadMap(mapFile:FileHandle, checkSizeErrors: Boolean = true):TileMap {
return mapFromSavedString(mapFile.readString(), checkSizeErrors)
}
fun getMaps(): Array<FileHandle> = Gdx.files.local(mapsFolder).list()

View File

@ -3,6 +3,7 @@ package com.unciv.logic.map
import com.unciv.Constants
import com.unciv.logic.HexMath.getEquivalentHexagonalRadius
import com.unciv.logic.HexMath.getEquivalentRectangularSize
import com.unciv.logic.HexMath.getNumberOfTilesInHexagon
enum class MapSize(val radius: Int, val width: Int, val height: Int) {
@ -44,10 +45,7 @@ class MapSizeNew {
constructor(radius: Int) {
name = Constants.custom
this.radius = radius
val size = getEquivalentRectangularSize(radius)
this.width = size.x.toInt()
this.height = size.y.toInt()
setNewRadius(radius)
}
constructor(width: Int, height: Int) {
@ -86,20 +84,24 @@ class MapSizeNew {
} ?: return null
// fix the size - not knowing whether hexagonal or rectangular is used
radius = when {
setNewRadius(when {
radius < 2 -> 2
radius > 500 -> 500
worldWrap && radius < 15 -> 15 // minimum for hexagonal but more than required for rectangular
else -> radius
}
val size = getEquivalentRectangularSize(radius)
width = size.x.toInt()
height = size.y.toInt()
})
// tell the caller that map dimensions have changed and why
return message
}
private fun setNewRadius(radius: Int) {
this.radius = radius
val size = getEquivalentRectangularSize(radius)
width = size.x.toInt()
height = size.y.toInt()
}
// For debugging and MapGenerator console output
override fun toString() = if (name == Constants.custom) "${width}x${height}" else name
}
@ -138,6 +140,9 @@ class MapParameters {
/** This is used mainly for the map editor, so you can continue editing a map under the same ruleset you started with */
var mods = LinkedHashSet<String>()
/** Unciv Version of creation for support cases */
var createdWithVersion = ""
var seed: Long = System.currentTimeMillis()
var tilesPerBiomeArea = 6
var maxCoastExtension = 2
@ -166,6 +171,7 @@ class MapParameters {
it.rareFeaturesRichness = rareFeaturesRichness
it.resourceRichness = resourceRichness
it.waterThreshold = waterThreshold
it.createdWithVersion = createdWithVersion
}
fun reseed() {
@ -184,14 +190,26 @@ class MapParameters {
waterThreshold = 0f
}
fun getArea() = when {
shape == MapShape.hexagonal -> getNumberOfTilesInHexagon(mapSize.radius)
worldWrap && mapSize.width % 2 != 0 -> (mapSize.width - 1) * mapSize.height
else -> mapSize.width * mapSize.height
}
fun displayMapDimensions() = mapSize.run {
(if (shape == MapShape.hexagonal) "R$radius" else "${width}x$height") +
(if (worldWrap) "w" else "")
}
// For debugging and MapGenerator console output
override fun toString() = sequence {
if (name.isNotEmpty()) yield("\"$name\" ")
yield("($mapSize ")
yield("(")
if (mapSize.name != Constants.custom) yield(mapSize.name + " ")
if (worldWrap) yield("wrapped ")
yield(shape)
yield(" " + displayMapDimensions())
if (name.isEmpty()) return@sequence
yield(" $type, Seed $seed, ")
yield(", $type, Seed $seed, ")
yield("$elevationExponent/$temperatureExtremeness/$resourceRichness/$vegetationRichness/")
yield("$rareFeaturesRichness/$maxCoastExtension/$tilesPerBiomeArea/$waterThreshold")
}.joinToString("", postfix = ")")

View File

@ -2,6 +2,7 @@ package com.unciv.logic.map
import com.badlogic.gdx.math.Vector2
import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.GameInfo
import com.unciv.logic.HexMath
import com.unciv.logic.civilization.CivilizationInfo
@ -96,7 +97,7 @@ class TileMap {
startingLocations.clear()
// world-wrap maps must always have an even width, so round down
val wrapAdjustedWidth = if (worldWrap && width % 2 != 0 ) width -1 else width
val wrapAdjustedWidth = if (worldWrap && width % 2 != 0) width -1 else width
// Even widths will have coordinates ranging -x..(x-1), not -x..x, which is always an odd-sized range
// e.g. w=4 -> -2..1, w=5 -> -2..2, w=6 -> -3..2, w=7 -> -3..3
@ -153,7 +154,7 @@ class TileMap {
* Respects map edges and world wrap. */
fun getTilesInDistance(origin: Vector2, distance: Int): Sequence<TileInfo> =
getTilesInDistanceRange(origin, 0..distance)
/** @return All tiles in a hexagonal ring around [origin] with the distances in [range]. Excludes the [origin] tile unless [range] starts at 0.
* Respects map edges and world wrap. */
fun getTilesInDistanceRange(origin: Vector2, range: IntRange): Sequence<TileInfo> =

View File

@ -1,6 +1,7 @@
package com.unciv.logic.map.mapgenerator
import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.HexMath
import com.unciv.logic.map.*
import com.unciv.models.Counter
@ -38,6 +39,7 @@ class MapGenerator(val ruleset: Ruleset) {
else
TileMap(mapSize.radius, ruleset, mapParameters.worldWrap)
mapParameters.createdWithVersion = UncivGame.Current.version
map.mapParameters = mapParameters
if (mapType == MapType.empty) {

View File

@ -1,16 +1,21 @@
package com.unciv.ui.mapeditor
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.math.Vector2
import com.badlogic.gdx.scenes.scene2d.InputEvent
import com.badlogic.gdx.scenes.scene2d.InputListener
import com.badlogic.gdx.scenes.scene2d.actions.Actions
import com.unciv.UncivGame
import com.unciv.logic.HexMath
import com.unciv.logic.map.MapShape
import com.unciv.logic.map.MapSizeNew
import com.unciv.logic.map.TileInfo
import com.unciv.logic.map.TileMap
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.metadata.GameSetupInfo
import com.unciv.models.translations.tr
import com.unciv.ui.utils.*
class MapEditorScreen(): CameraStageBaseScreen() {
@ -28,6 +33,7 @@ class MapEditorScreen(): CameraStageBaseScreen() {
constructor(map: TileMap) : this() {
tileMap = map
checkAndFixMapSize()
ruleset = RulesetCache.getComplexRuleset(map.mapParameters.mods)
initialize()
}
@ -136,11 +142,34 @@ class MapEditorScreen(): CameraStageBaseScreen() {
})
}
private fun checkAndFixMapSize() {
val areaFromTiles = tileMap.values.size
tileMap.mapParameters.run {
val areaFromSize = getArea()
if (areaFromSize == areaFromTiles) return
Gdx.app.postRunnable {
val message = ("Invalid map: Area ([$areaFromTiles]) does not match saved dimensions ([" +
displayMapDimensions() + "]).").tr() +
"\n" + "The dimensions have now been fixed for you.".tr()
ToastPopup(message, this@MapEditorScreen, 4000L )
}
if (shape == MapShape.hexagonal) {
mapSize = MapSizeNew(HexMath.getHexagonalRadiusForArea(areaFromTiles).toInt())
return
}
// These mimic tileMap.max* without the abs()
val minLatitude = (tileMap.values.map { it.latitude }.minOrNull() ?: 0f).toInt()
val minLongitude = (tileMap.values.map { it.longitude }.minOrNull() ?: 0f).toInt()
val maxLatitude = (tileMap.values.map { it.latitude }.maxOrNull() ?: 0f).toInt()
val maxLongitude = (tileMap.values.map { it.longitude }.maxOrNull() ?: 0f).toInt()
mapSize = MapSizeNew((maxLongitude - minLongitude + 1), (maxLatitude - minLatitude + 1) / 2)
}
}
override fun resize(width: Int, height: Int) {
if (stage.viewport.screenWidth != width || stage.viewport.screenHeight != height) {
game.setScreen(MapEditorScreen(mapHolder.tileMap))
}
}
}

View File

@ -7,6 +7,7 @@ import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.scenes.scene2d.ui.TextButton
import com.badlogic.gdx.scenes.scene2d.ui.TextField
import com.unciv.logic.MapSaver
import com.unciv.logic.UncivShowableException
import com.unciv.logic.map.MapType
import com.unciv.logic.map.TileMap
import com.unciv.models.ruleset.RulesetCache
@ -64,7 +65,8 @@ class SaveAndLoadMapScreen(mapToSave: TileMap?, save:Boolean = false, previousSc
}
}
try {
val map = MapSaver.loadMap(chosenMap!!)
val map = MapSaver.loadMap(chosenMap!!, checkSizeErrors = false)
val missingMods = map.mapParameters.mods.filter { it !in RulesetCache }
if (missingMods.isNotEmpty()) {
Gdx.app.postRunnable {
@ -90,7 +92,8 @@ class SaveAndLoadMapScreen(mapToSave: TileMap?, save:Boolean = false, previousSc
Gdx.app.postRunnable {
popup?.close()
println("Error loading map \"$chosenMap\": ${ex.localizedMessage}")
ToastPopup("Error loading map!", this)
ToastPopup("Error loading map!".tr() +
(if (ex is UncivShowableException) "\n" + ex.message else ""), this)
}
}
}
@ -128,7 +131,7 @@ class SaveAndLoadMapScreen(mapToSave: TileMap?, save:Boolean = false, previousSc
val loadFromClipboardAction = {
try {
val clipboardContentsString = Gdx.app.clipboard.contents.trim()
val loadedMap = MapSaver.mapFromSavedString(clipboardContentsString)
val loadedMap = MapSaver.mapFromSavedString(clipboardContentsString, checkSizeErrors = false)
game.setScreen(MapEditorScreen(loadedMap))
} catch (ex: Exception) {
couldNotLoadMapLabel.isVisible = true

View File

@ -6,6 +6,7 @@ import com.badlogic.gdx.scenes.scene2d.ui.SelectBox
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.utils.Array
import com.unciv.logic.MapSaver
import com.unciv.logic.UncivShowableException
import com.unciv.logic.map.MapType
import com.unciv.logic.map.TileMap
import com.unciv.ui.utils.CameraStageBaseScreen
@ -90,7 +91,9 @@ class MapOptionsTable(private val newGameScreen: NewGameScreen): Table() {
map = MapSaver.loadMap(mapFile)
} catch (ex:Exception){
Popup(newGameScreen).apply {
addGoodSizedLabel("Could not load map!")
addGoodSizedLabel("Could not load map!").row()
if (ex is UncivShowableException)
addGoodSizedLabel(ex.message!!).row()
addCloseButton()
open()
}

View File

@ -252,7 +252,7 @@ class MapParametersTable(
addSlider("Water level", {mapParameters.waterThreshold}, -0.1f, 0.1f)
{ mapParameters.waterThreshold = it }
val resetToDefaultButton = "Reset to default".toTextButton()
val resetToDefaultButton = "Reset to defaults".toTextButton()
resetToDefaultButton.onClick {
mapParameters.resetAdvancedSettings()
seedTextField.text = mapParameters.seed.toString()

View File

@ -4,6 +4,7 @@ import com.badlogic.gdx.Gdx
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.ui.SelectBox
import com.badlogic.gdx.scenes.scene2d.ui.Skin
import com.badlogic.gdx.scenes.scene2d.ui.VerticalGroup
import com.badlogic.gdx.utils.Array
import com.unciv.UncivGame
import com.unciv.logic.*
@ -45,8 +46,8 @@ class NewGameScreen(
updateRuleset()
if (UncivGame.Current.settings.lastGameSetup != null) {
rightSideGroup.addActorAt(0, VerticalGroup().padBottom(5f))
val resetToDefaultsButton = "Reset to defaults".toTextButton()
resetToDefaultsButton.padBottom(5f)
rightSideGroup.addActorAt(0, resetToDefaultsButton)
resetToDefaultsButton.onClick {
game.setScreen(NewGameScreen(previousScreen, GameSetupInfo()))
@ -80,10 +81,16 @@ class NewGameScreen(
Gdx.input.inputProcessor = null // remove input processing - nothing will be clicked!
if (mapOptionsTable.mapTypeSelectBox.selected.value == MapType.custom){
val map = MapSaver.loadMap(gameSetupInfo.mapFile!!)
val rulesetIncompatibilities = map.getRulesetIncompatibility(ruleset)
if (mapOptionsTable.mapTypeSelectBox.selected.value == MapType.custom) {
val map = try {
MapSaver.loadMap(gameSetupInfo.mapFile!!)
} catch (ex: Throwable) {
game.setScreen(this)
ToastPopup("Could not load map!", this)
return@onClick
}
val rulesetIncompatibilities = map.getRulesetIncompatibility(ruleset)
if (rulesetIncompatibilities.isNotEmpty()) {
val incompatibleMap = Popup(this)
incompatibleMap.addGoodSizedLabel("Map is incompatible with the chosen ruleset!".tr()).row()

View File

@ -11,6 +11,7 @@ import com.badlogic.gdx.utils.Align
import com.unciv.Constants
import com.unciv.MainMenuScreen
import com.unciv.UncivGame
import com.unciv.logic.MapSaver
import com.unciv.logic.civilization.PlayerType
import com.unciv.models.UncivSound
import com.unciv.models.metadata.BaseRuleset
@ -314,6 +315,9 @@ class OptionsPopup(val previousScreen: CameraStageBaseScreen) : Popup(previousSc
game.gameInfo.gameParameters.godMode = it
}).row()
}
add("Save maps compressed".toCheckBox(MapSaver.saveZipped) {
MapSaver.saveZipped = it
}).row()
}
//endregion