mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-04 15:27:50 +07:00
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:
@ -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 =
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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 = ")")
|
||||
|
@ -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> =
|
||||
|
@ -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) {
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user