diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index 68b29fb713..57ae9a5d48 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -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 = diff --git a/core/src/com/unciv/logic/MapSaver.kt b/core/src/com/unciv/logic/MapSaver.kt index 8642d349f1..898021be4e 100644 --- a/core/src/com/unciv/logic/MapSaver.kt +++ b/core/src/com/unciv/logic/MapSaver.kt @@ -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 = Gdx.files.local(mapsFolder).list() diff --git a/core/src/com/unciv/logic/map/MapParameters.kt b/core/src/com/unciv/logic/map/MapParameters.kt index c25b524588..fa3b7f2348 100644 --- a/core/src/com/unciv/logic/map/MapParameters.kt +++ b/core/src/com/unciv/logic/map/MapParameters.kt @@ -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() + /** 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 = ")") diff --git a/core/src/com/unciv/logic/map/TileMap.kt b/core/src/com/unciv/logic/map/TileMap.kt index 044993a2b6..bf8415bce7 100644 --- a/core/src/com/unciv/logic/map/TileMap.kt +++ b/core/src/com/unciv/logic/map/TileMap.kt @@ -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 = 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 = diff --git a/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt b/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt index 6cfa4d65ba..0107ff72ff 100644 --- a/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt +++ b/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt @@ -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) { diff --git a/core/src/com/unciv/ui/mapeditor/MapEditorScreen.kt b/core/src/com/unciv/ui/mapeditor/MapEditorScreen.kt index ed4f813340..ca0d93184d 100644 --- a/core/src/com/unciv/ui/mapeditor/MapEditorScreen.kt +++ b/core/src/com/unciv/ui/mapeditor/MapEditorScreen.kt @@ -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)) } } } - - diff --git a/core/src/com/unciv/ui/mapeditor/SaveAndLoadMapScreen.kt b/core/src/com/unciv/ui/mapeditor/SaveAndLoadMapScreen.kt index 7afb4630ed..0a1533dd64 100644 --- a/core/src/com/unciv/ui/mapeditor/SaveAndLoadMapScreen.kt +++ b/core/src/com/unciv/ui/mapeditor/SaveAndLoadMapScreen.kt @@ -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 diff --git a/core/src/com/unciv/ui/newgamescreen/MapOptionsTable.kt b/core/src/com/unciv/ui/newgamescreen/MapOptionsTable.kt index b933186641..3c9d128cda 100644 --- a/core/src/com/unciv/ui/newgamescreen/MapOptionsTable.kt +++ b/core/src/com/unciv/ui/newgamescreen/MapOptionsTable.kt @@ -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() } diff --git a/core/src/com/unciv/ui/newgamescreen/MapParametersTable.kt b/core/src/com/unciv/ui/newgamescreen/MapParametersTable.kt index e381c1b03b..27067635ba 100644 --- a/core/src/com/unciv/ui/newgamescreen/MapParametersTable.kt +++ b/core/src/com/unciv/ui/newgamescreen/MapParametersTable.kt @@ -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() diff --git a/core/src/com/unciv/ui/newgamescreen/NewGameScreen.kt b/core/src/com/unciv/ui/newgamescreen/NewGameScreen.kt index d1e3935856..4df6176f44 100644 --- a/core/src/com/unciv/ui/newgamescreen/NewGameScreen.kt +++ b/core/src/com/unciv/ui/newgamescreen/NewGameScreen.kt @@ -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() diff --git a/core/src/com/unciv/ui/worldscreen/mainmenu/OptionsPopup.kt b/core/src/com/unciv/ui/worldscreen/mainmenu/OptionsPopup.kt index 298006dca7..58d7452a69 100644 --- a/core/src/com/unciv/ui/worldscreen/mainmenu/OptionsPopup.kt +++ b/core/src/com/unciv/ui/worldscreen/mainmenu/OptionsPopup.kt @@ -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