diff --git a/core/src/com/unciv/logic/GameInfo.kt b/core/src/com/unciv/logic/GameInfo.kt index 5d916696ee..b35c04cffe 100644 --- a/core/src/com/unciv/logic/GameInfo.kt +++ b/core/src/com/unciv/logic/GameInfo.kt @@ -354,8 +354,7 @@ class GameInfo { tile.terrainFeatures.remove(terrainFeature) if (tile.resource != null && !ruleSet.tileResources.containsKey(tile.resource!!)) tile.resource = null - if (tile.improvement != null && !ruleSet.tileImprovements.containsKey(tile.improvement!!) - && !tile.improvement!!.startsWith("StartingLocation ")) // To not remove the starting locations in GameStarter.startNewGame() + if (tile.improvement != null && !ruleSet.tileImprovements.containsKey(tile.improvement!!)) tile.improvement = null for (unit in tile.getUnits()) { diff --git a/core/src/com/unciv/logic/GameStarter.kt b/core/src/com/unciv/logic/GameStarter.kt index 414b46268a..b14d93b405 100644 --- a/core/src/com/unciv/logic/GameStarter.kt +++ b/core/src/com/unciv/logic/GameStarter.kt @@ -20,62 +20,87 @@ import kotlin.collections.HashMap import kotlin.math.max object GameStarter { + // temporary instrumentation while tuning/debugging + private const val consoleOutput = true + private const val consoleTimings = true fun startNewGame(gameSetupInfo: GameSetupInfo): GameInfo { + if (consoleOutput || consoleTimings) + println("\nGameStarter run with parameters ${gameSetupInfo.gameParameters}, map ${gameSetupInfo.mapParameters}") + val gameInfo = GameInfo() + lateinit var tileMap: TileMap gameInfo.gameParameters = gameSetupInfo.gameParameters val ruleset = RulesetCache.getComplexRuleset(gameInfo.gameParameters.mods) - if (gameSetupInfo.mapParameters.name != "") { - gameInfo.tileMap = MapSaver.loadMap(gameSetupInfo.mapFile!!) + if (gameSetupInfo.mapParameters.name != "") runAndMeasure("loadMap") { + tileMap = MapSaver.loadMap(gameSetupInfo.mapFile!!) // Don't override the map parameters - this can include if we world wrap or not! - } else { - gameInfo.tileMap = MapGenerator(ruleset).generateMap(gameSetupInfo.mapParameters) - gameInfo.tileMap.mapParameters = gameSetupInfo.mapParameters + } else runAndMeasure("generateMap") { + tileMap = MapGenerator(ruleset).generateMap(gameSetupInfo.mapParameters) + tileMap.mapParameters = gameSetupInfo.mapParameters } + runAndMeasure("addCivilizations") { + gameInfo.tileMap = tileMap + tileMap.gameInfo = gameInfo // need to set this transient before placing units in the map + addCivilizations(gameSetupInfo.gameParameters, gameInfo, ruleset) // this is before gameInfo.setTransients, so gameInfo doesn't yet have the gameBasics + } - gameInfo.tileMap.gameInfo = gameInfo // need to set this transient before placing units in the map - addCivilizations(gameSetupInfo.gameParameters, gameInfo, ruleset) // this is before gameInfo.setTransients, so gameInfo doesn't yet have the gameBasics + runAndMeasure("Remove units") { + // Remove units for civs that aren't in this game + for (tile in tileMap.values) + for (unit in tile.getUnits()) + if (gameInfo.civilizations.none { it.civName == unit.owner }) { + unit.currentTile = tile + unit.setTransients(ruleset) + unit.removeFromTile() + } + } - // Remove units for civs that aren't in this game - for (tile in gameInfo.tileMap.values) - for (unit in tile.getUnits()) - if (gameInfo.civilizations.none { it.civName == unit.owner }) { - unit.currentTile = tile - unit.setTransients(ruleset) - unit.removeFromTile() - } + runAndMeasure("setTransients") { + tileMap.setTransients(ruleset) // if we're starting from a map with pre-placed units, they need the civs to exist first + tileMap.setStartingLocationsTransients() - gameInfo.tileMap.setTransients(ruleset) // if we're starting from a map with preplaced units, they need the civs to exist first + gameInfo.difficulty = gameSetupInfo.gameParameters.difficulty - gameInfo.difficulty = gameSetupInfo.gameParameters.difficulty + gameInfo.setTransients() // needs to be before placeBarbarianUnit because it depends on the tilemap having its gameInfo set + } + runAndMeasure("Techs and Stats") { + addCivTechs(gameInfo, ruleset, gameSetupInfo) - gameInfo.setTransients() // needs to be before placeBarbarianUnit because it depends on the tilemap having its gameinfo set + addCivStats(gameInfo) + } - addCivTechs(gameInfo, ruleset, gameSetupInfo) - - addCivStats(gameInfo) - - // and only now do we add units for everyone, because otherwise both the gameInfo.setTransients() and the placeUnit will both add the unit to the civ's unit list! - addCivStartingUnits(gameInfo) + runAndMeasure("addCivStartingUnits") { + // and only now do we add units for everyone, because otherwise both the gameInfo.setTransients() and the placeUnit will both add the unit to the civ's unit list! + addCivStartingUnits(gameInfo) + } // remove starting locations once we're done - for (tile in gameInfo.tileMap.values) { - if (tile.improvement != null && tile.improvement!!.startsWith("StartingLocation ")) - tile.improvement = null - // set max starting movement for units loaded from map + tileMap.clearStartingLocations() + + // set max starting movement for units loaded from map + for (tile in tileMap.values) { for (unit in tile.getUnits()) unit.currentMovement = unit.getMaxMovement().toFloat() } - + // This triggers the one-time greeting from Nation.startIntroPart1/2 addPlayerIntros(gameInfo) return gameInfo } + private fun runAndMeasure(text: String, action: ()->Unit) { + if (!consoleTimings) return action() + val startNanos = System.nanoTime() + action() + val delta = System.nanoTime() - startNanos + println("GameStarter.$text took ${delta/1000000L}.${(delta/10000L).rem(100)}ms") + } + private fun addPlayerIntros(gameInfo: GameInfo) { gameInfo.civilizations.filter { // isNotEmpty should also exclude a spectator @@ -138,6 +163,8 @@ object GameStarter { availableCivNames.addAll(ruleset.nations.filter { it.value.isMajorCiv() }.keys.shuffled()) availableCivNames.removeAll(newGameParameters.players.map { it.chosenCiv }) availableCivNames.remove(Constants.barbarians) + + val startingTechs = ruleset.technologies.values.filter { it.uniques.contains("Starting tech") } if (!newGameParameters.noBarbarians && ruleset.nations.containsKey(Constants.barbarians)) { val barbarianCivilization = CivilizationInfo(Constants.barbarians) @@ -149,44 +176,36 @@ object GameStarter { else availableCivNames.pop() val playerCiv = CivilizationInfo(nationName) - for (tech in ruleset.technologies.values.filter { it.uniques.contains("Starting tech") }) + for (tech in startingTechs) playerCiv.tech.techsResearched.add(tech.name) // can't be .addTechnology because the civInfo isn't assigned yet playerCiv.playerType = player.playerType playerCiv.playerId = player.playerId gameInfo.civilizations.add(playerCiv) } - val cityStatesWithStartingLocations = - gameInfo.tileMap.values - .filter { it.improvement != null && it.improvement!!.startsWith("StartingLocation ") } - .map { it.improvement!!.replace("StartingLocation ", "") } + val civNamesWithStartingLocations = gameInfo.tileMap.startingLocationsByNation.keys val availableCityStatesNames = Stack() // since we shuffle and then order by, we end up with all the City-States with starting tiles first in a random order, // and then all the other City-States in a random order! Because the sortedBy function is stable! availableCityStatesNames.addAll(ruleset.nations.filter { it.value.isCityState() }.keys - .shuffled().sortedByDescending { it in cityStatesWithStartingLocations }) + .shuffled().sortedByDescending { it in civNamesWithStartingLocations }) - val unusedMercantileResources = ruleset.tileResources.values.filter { it.unique == "Can only be created by Mercantile City-States" }.toMutableList() + val allMercantileResources = ruleset.tileResources.values.filter { it.unique == "Can only be created by Mercantile City-States" }.map { it.name } + val unusedMercantileResources = Stack() + unusedMercantileResources.addAll(allMercantileResources.shuffled()) for (cityStateName in availableCityStatesNames.take(newGameParameters.numberOfCityStates)) { val civ = CivilizationInfo(cityStateName) civ.cityStatePersonality = CityStatePersonality.values().random() - if (ruleset.nations[cityStateName]?.cityStateType == CityStateType.Mercantile) { - if (!ruleset.tileResources.values.any { it.unique == "Can only be created by Mercantile City-States" }) { - civ.cityStateResource = null - } else if (unusedMercantileResources.isNotEmpty()) { - // First pick an unused luxury if possible - val unusedResource = unusedMercantileResources.random() - civ.cityStateResource = unusedResource.name - unusedMercantileResources.remove(unusedResource) - } else { - // Then random - civ.cityStateResource = ruleset.tileResources.values.filter { it.unique == "Can only be created by Mercantile City-States" }.random().name - } + civ.cityStateResource = when { + ruleset.nations[cityStateName]?.cityStateType != CityStateType.Mercantile -> null + allMercantileResources.isEmpty() -> null + unusedMercantileResources.empty() -> allMercantileResources.random() // When unused luxuries exhausted, random + else -> unusedMercantileResources.pop() // First pick an unused luxury if possible } gameInfo.civilizations.add(civ) - for (tech in ruleset.technologies.values.filter { it.uniques.contains("Starting tech") }) + for (tech in startingTechs) civ.tech.techsResearched.add(tech.name) // can't be .addTechnology because the civInfo isn't assigned yet } } @@ -194,38 +213,35 @@ object GameStarter { private fun addCivStartingUnits(gameInfo: GameInfo) { val ruleSet = gameInfo.ruleSet + val tileMap = gameInfo.tileMap val startingEra = gameInfo.gameParameters.startingEra var startingUnits: MutableList var eraUnitReplacement: String - val startScores = HashMap() - for (tile in gameInfo.tileMap.values) { + val startScores = HashMap(tileMap.values.size) + for (tile in tileMap.values) { startScores[tile] = tile.getTileStartScore() } // First we get start locations for the major civs, on the second pass the city states (without predetermined starts) can squeeze in wherever // I hear copying code is good - val cityStatesWithStartingLocations = - gameInfo.tileMap.values - .filter { it.improvement != null && it.improvement!!.startsWith("StartingLocation ") } - .map { it.improvement!!.replace("StartingLocation ", "") } - val bestCivs = gameInfo.civilizations.filter { !it.isBarbarian() && (!it.isCityState() || it.civName in cityStatesWithStartingLocations) } - val bestLocations = getStartingLocations(bestCivs, gameInfo.tileMap, startScores) - for (civ in bestCivs) - { - if (civ.isCityState()) // Already have explicit starting locations + val civNamesWithStartingLocations = tileMap.startingLocationsByNation.keys + val bestCivs = gameInfo.civilizations.filter { !it.isBarbarian() && (!it.isCityState() || it.civName in civNamesWithStartingLocations) } + val bestLocations = getStartingLocations(bestCivs, tileMap, startScores) + for ((civ, tile) in bestLocations) { + if (civ.civName in civNamesWithStartingLocations) // Already have explicit starting locations continue // Mark the best start locations so we remember them for the second pass - bestLocations[civ]!!.improvement = "StartingLocation " + civ.civName + tileMap.addStartingLocation(civ.civName, tile) } val startingLocations = getStartingLocations( gameInfo.civilizations.filter { !it.isBarbarian() }, - gameInfo.tileMap, startScores) + tileMap, startScores) val settlerLikeUnits = ruleSet.units.filter { - it.value.uniqueObjects.any { it.placeholderText == Constants.settlerUnique } + it.value.uniqueObjects.any { unique -> unique.placeholderText == Constants.settlerUnique } } // no starting units for Barbarians and Spectators @@ -241,8 +257,7 @@ object GameStarter { addCityStateLuxury(gameInfo, startingLocation) for (tile in startingLocation.getTilesInDistance(3)) { - if (tile.improvement != null - && !tile.improvement!!.startsWith("StartingLocation") + if (tile.improvement != null && tile.getTileImprovement()!!.isAncientRuinsEquivalent() ) { tile.improvement = null // Remove ancient ruins in immediate vicinity @@ -305,7 +320,7 @@ object GameStarter { } if (unit == "Worker" && "Worker" !in ruleSet.units) { val buildableWorkerLikeUnits = ruleSet.units.filter { - it.value.uniqueObjects.any { it.placeholderText == Constants.canBuildImprovements } + it.value.uniqueObjects.any { unique -> unique.placeholderText == Constants.canBuildImprovements } && it.value.isBuildable(civ) && it.value.isCivilian() } @@ -353,18 +368,14 @@ object GameStarter { landTilesInBigEnoughGroup.addAll(tilesInGroup) } - val tilesWithStartingLocations = tileMap.values - .filter { it.improvement != null && it.improvement!!.startsWith("StartingLocation ") } - - val civsOrderedByAvailableLocations = civs.shuffled() // Order should be random since it determines who gets best start .sortedBy { civ -> when { - tilesWithStartingLocations.any { it.improvement == "StartingLocation " + civ.civName } -> 1 // harshest requirements + civ.civName in tileMap.startingLocationsByNation -> 1 // harshest requirements civ.nation.startBias.contains("Tundra") -> 2 // Tundra starts are hard to find, so let's do them first civ.nation.startBias.isNotEmpty() -> 3 // less harsh - else -> 4 - } // no requirements + else -> 4 // no requirements + } } for (minimumDistanceBetweenStartingLocations in tileMap.tileMatrix.size / 4 downTo 0) { @@ -375,7 +386,7 @@ object GameStarter { val startingLocations = HashMap() for (civ in civsOrderedByAvailableLocations) { var startingLocation: TileInfo - val presetStartingLocation = tilesWithStartingLocations.firstOrNull { it.improvement == "StartingLocation " + civ.civName } + val presetStartingLocation = tileMap.startingLocationsByNation[civ.civName]?.randomOrNull() // in case map editor is extended to allow alternate starting locations for a nation var distanceToNext = minimumDistanceBetweenStartingLocations if (presetStartingLocation != null) startingLocation = presetStartingLocation @@ -389,11 +400,14 @@ object GameStarter { var preferredTiles = freeTiles.toList() for (startBias in civ.nation.startBias) { - if (startBias.startsWith("Avoid ")) { - val tileToAvoid = startBias.removePrefix("Avoid [").removeSuffix("]") - preferredTiles = preferredTiles.filter { !it.matchesTerrainFilter(tileToAvoid) } - } else if (startBias == Constants.coast) preferredTiles = preferredTiles.filter { it.isCoastalTile() } - else preferredTiles = preferredTiles.filter { it.matchesTerrainFilter(startBias) } + preferredTiles = when { + startBias.startsWith("Avoid [") -> { + val tileToAvoid = startBias.removePrefix("Avoid [").removeSuffix("]") + preferredTiles.filter { !it.matchesTerrainFilter(tileToAvoid) } + } + startBias == Constants.coast -> preferredTiles.filter { it.isCoastalTile() } + else -> preferredTiles.filter { it.matchesTerrainFilter(startBias) } + } } startingLocation = if (preferredTiles.isNotEmpty()) preferredTiles.last() else freeTiles.last() diff --git a/core/src/com/unciv/logic/MapSaver.kt b/core/src/com/unciv/logic/MapSaver.kt index 81c951189a..11f02f0de1 100644 --- a/core/src/com/unciv/logic/MapSaver.kt +++ b/core/src/com/unciv/logic/MapSaver.kt @@ -10,20 +10,32 @@ object MapSaver { fun json() = GameSaver.json() private const val mapsFolder = "maps" + private const val saveZipped = false private fun getMap(mapName:String) = Gdx.files.local("$mapsFolder/$mapName") + fun mapFromSavedString(mapString: String): TileMap { + val unzippedJson = try { + Gzip.unzip(mapString) + } catch (ex: Exception) { + mapString + } + return mapFromJson(unzippedJson) + } + fun mapToSavedString(tileMap: TileMap): String { + val mapJson = json().toJson(tileMap) + return if (saveZipped) Gzip.zip(mapJson) else mapJson + } + fun saveMap(mapName: String,tileMap: TileMap) { - getMap(mapName).writeString(Gzip.zip(json().toJson(tileMap)), false) + getMap(mapName).writeString(mapToSavedString(tileMap), false) } fun loadMap(mapFile:FileHandle):TileMap { - val gzippedString = mapFile.readString() - val unzippedJson = Gzip.unzip(gzippedString) - return json().fromJson(TileMap::class.java, unzippedJson) + return mapFromSavedString(mapFile.readString()) } - fun getMaps() = Gdx.files.local(mapsFolder).list() + fun getMaps(): Array = Gdx.files.local(mapsFolder).list() - fun mapFromJson(json:String): TileMap = json().fromJson(TileMap::class.java, json) + private fun mapFromJson(json:String): TileMap = json().fromJson(TileMap::class.java, json) } \ No newline at end of file diff --git a/core/src/com/unciv/logic/map/MapParameters.kt b/core/src/com/unciv/logic/map/MapParameters.kt index 2532f33943..8035d6e3e4 100644 --- a/core/src/com/unciv/logic/map/MapParameters.kt +++ b/core/src/com/unciv/logic/map/MapParameters.kt @@ -158,5 +158,6 @@ class MapParameters { } // For debugging and MapGenerator console output - override fun toString() = "($mapSize ${if (worldWrap)"wrapped " else ""}$shape $type, Seed $seed, $elevationExponent/$temperatureExtremeness/$resourceRichness/$vegetationRichness/$rareFeaturesRichness/$maxCoastExtension/$tilesPerBiomeArea/$waterThreshold)" + override fun toString() = if (name.isNotEmpty()) "\"$name\"" + else "($mapSize ${if (worldWrap)"wrapped " else ""}$shape $type, Seed $seed, $elevationExponent/$temperatureExtremeness/$resourceRichness/$vegetationRichness/$rareFeaturesRichness/$maxCoastExtension/$tilesPerBiomeArea/$waterThreshold)" } diff --git a/core/src/com/unciv/logic/map/MapUnit.kt b/core/src/com/unciv/logic/map/MapUnit.kt index b5e29f5652..1e4f670b4f 100644 --- a/core/src/com/unciv/logic/map/MapUnit.kt +++ b/core/src/com/unciv/logic/map/MapUnit.kt @@ -733,7 +733,6 @@ class MapUnit { if (civInfo.isMajorCiv() && tile.improvement != null - && !tile.improvement!!.startsWith("StartingLocation ") && tile.getTileImprovement()!!.isAncientRuinsEquivalent() ) getAncientRuinBonus(tile) diff --git a/core/src/com/unciv/logic/map/TileInfo.kt b/core/src/com/unciv/logic/map/TileInfo.kt index 0f0c50f495..cb3eae889a 100644 --- a/core/src/com/unciv/logic/map/TileInfo.kt +++ b/core/src/com/unciv/logic/map/TileInfo.kt @@ -659,8 +659,7 @@ open class TileInfo { out.add("Terrain feature [$terrainFeature] does not exist in ruleset!") if (resource != null && !ruleset.tileResources.containsKey(resource)) out.add("Resource [$resource] does not exist in ruleset!") - if (improvement != null && !improvement!!.startsWith("StartingLocation") - && !ruleset.tileImprovements.containsKey(improvement)) + if (improvement != null && !ruleset.tileImprovements.containsKey(improvement)) out.add("Improvement [$improvement] does not exist in ruleset!") return out } @@ -756,9 +755,9 @@ open class TileInfo { roadStatus = RoadStatus.None } - private fun normalizeTileImprovement(ruleset: Ruleset) { - if (improvement!!.startsWith("StartingLocation")) { + // This runs from map editor too, so the Pseudo-improvements for starting locations need to stay. + if (improvement!!.startsWith(TileMap.startingLocationPrefix)) { if (!isLand || getLastTerrain().impassable) improvement = null return } diff --git a/core/src/com/unciv/logic/map/TileMap.kt b/core/src/com/unciv/logic/map/TileMap.kt index b4a9eff6dd..e6b39fc0d8 100644 --- a/core/src/com/unciv/logic/map/TileMap.kt +++ b/core/src/com/unciv/logic/map/TileMap.kt @@ -10,11 +10,47 @@ import com.unciv.models.ruleset.Nation import com.unciv.models.ruleset.Ruleset import kotlin.math.abs +/** An Unciv map with all properties as produced by the [map editor][com.unciv.ui.mapeditor.MapEditorScreen] + * or [MapGenerator][com.unciv.logic.map.mapgenerator.MapGenerator]; or as part of a running [game][GameInfo]. + * + * Note: Will be Serialized -> Take special care with lateinit and lazy! + */ class TileMap { + companion object { + const val startingLocationPrefix = "StartingLocation " + /** + * To be backwards compatible, a json without a startingLocations element will be recognized by an entry with this marker + * New saved maps will never have this marker and will always have a serialized startingLocations list even if empty. + * New saved maps will also never have "StartingLocation" improvements, these _must_ be converted before use anywhere outside map editor. + */ + private const val legacyMarker = " Legacy " + } + + //region Fields, Serialized + + var mapParameters = MapParameters() + + private var tileList = ArrayList() + + /** Structure geared for simple serialization by Gdx.Json (which is a little blind to kotlin collections, especially HashSet) + * @param position [Vector2] of the location + * @param nation Name of the nation + */ + private data class StartingLocation(val position: Vector2 = Vector2.Zero, val nation: String = "") + private val startingLocations = arrayListOf(StartingLocation(Vector2.Zero, legacyMarker)) + + //endregion + //region Fields, Transient + + /** Attention: lateinit will _stay uninitialized_ while in MapEditorScreen! */ @Transient lateinit var gameInfo: GameInfo + /** Keep a copy of the [Ruleset] object passer to setTransients, for now only to allow subsequent setTransients without. Copied on [clone]. */ + @Transient + var ruleset: Ruleset? = null + @Transient var tileMatrix = ArrayList>() // this works several times faster than a hashmap, the performance difference is really astounding @@ -33,25 +69,31 @@ class TileMap { @delegate:Transient val naturalWonders: List by lazy { tileList.asSequence().filter { it.isNaturalWonder() }.map { it.naturalWonder!! }.distinct().toList() } - var mapParameters = MapParameters() - - private var tileList = ArrayList() - + // Excluded from Serialization by having no own backing field val values: Collection get() = tileList + @Transient + val startingLocationsByNation = HashMap>() + + //endregion + //region Constructors + /** for json parsing, we need to have a default constructor */ constructor() - /** generates an hexagonal map of given radius */ + /** creates a hexagonal map of given radius (filled with grassland) */ constructor(radius: Int, ruleset: Ruleset, worldWrap: Boolean = false) { + startingLocations.clear() for (vector in HexMath.getVectorsInDistance(Vector2.Zero, radius, worldWrap)) tileList.add(TileInfo().apply { position = vector; baseTerrain = Constants.grassland }) setTransients(ruleset) } - /** generates a rectangular map of given width and height*/ + /** creates a rectangular map of given width and height (filled with grassland) */ constructor(width: Int, height: Int, ruleset: Ruleset, worldWrap: Boolean = false) { + 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 @@ -67,39 +109,57 @@ class TileMap { setTransients(ruleset) } + //endregion + //region Operators and Standards + + /** @return a deep-copy clone of the serializable fields, no transients initialized */ fun clone(): TileMap { val toReturn = TileMap() toReturn.tileList.addAll(tileList.map { it.clone() }) toReturn.mapParameters = mapParameters + toReturn.ruleset = ruleset + toReturn.startingLocations.clear() + toReturn.startingLocations.ensureCapacity(startingLocations.size) + toReturn.startingLocations.addAll(startingLocations) return toReturn } - operator fun contains(vector: Vector2) = contains(vector.x.toInt(), vector.y.toInt()) + operator fun contains(vector: Vector2) = + contains(vector.x.toInt(), vector.y.toInt()) - fun contains(x: Int, y: Int): Boolean { + operator fun get(vector: Vector2) = + get(vector.x.toInt(), vector.y.toInt()) + + fun contains(x: Int, y: Int) = + getOrNull(x, y) != null + + operator fun get(x: Int, y: Int) = + tileMatrix[x - leftX][y - bottomY]!! + + /** @return tile at hex coordinates ([x],[y]) or null if they are outside the map. Does *not* respect world wrap, use [getIfTileExistsOrNull] for that. */ + private fun getOrNull (x: Int, y: Int): TileInfo? { val arrayXIndex = x - leftX - if (arrayXIndex < 0 || arrayXIndex >= tileMatrix.size) return false + if (arrayXIndex < 0 || arrayXIndex >= tileMatrix.size) return null val arrayYIndex = y - bottomY - if (arrayYIndex < 0 || arrayYIndex >= tileMatrix[arrayXIndex].size) return false - return tileMatrix[arrayXIndex][arrayYIndex] != null + if (arrayYIndex < 0 || arrayYIndex >= tileMatrix[arrayXIndex].size) return null + return tileMatrix[arrayXIndex][arrayYIndex] } - operator fun get(x: Int, y: Int): TileInfo { - val arrayXIndex = x - leftX - val arrayYIndex = y - bottomY - return tileMatrix[arrayXIndex][arrayYIndex]!! - } - - operator fun get(vector: Vector2): TileInfo { - return get(vector.x.toInt(), vector.y.toInt()) - } + //endregion + //region Pure Functions + /** @return All tiles in a hexagon of radius [distance], including the tile at [origin] and all up to [distance] steps away. + * 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 = range.asSequence().flatMap { getTilesAtDistance(origin, it) } + /** @return All tiles in a hexagonal ring 1 tile wide around [origin] with the [distance]. Contains the [origin] if and only if [distance] is <= 0. + * Respects map edges and world wrap. */ fun getTilesAtDistance(origin: Vector2, distance: Int): Sequence = if (distance <= 0) // silently take negatives. sequenceOf(get(origin)) @@ -133,6 +193,7 @@ class TileMap { } }.filterNotNull() + /** @return tile at hex coordinates ([x],[y]) or null if they are outside the map. Respects map edges and world wrap. */ private fun getIfTileExistsOrNull(x: Int, y: Int): TileInfo? { if (contains(x, y)) return get(x, y) @@ -156,6 +217,166 @@ class TileMap { return null } + /** + * Returns the clockPosition of [otherTile] seen from [tile]'s position + * Returns -1 if not neighbors + */ + fun getNeighborTileClockPosition(tile: TileInfo, otherTile: TileInfo): Int { + val radius = if (mapParameters.shape == MapShape.rectangular) + mapParameters.mapSize.width / 2 + else mapParameters.mapSize.radius + + val xDifference = tile.position.x - otherTile.position.x + val yDifference = tile.position.y - otherTile.position.y + val xWrapDifferenceBottom = tile.position.x - (otherTile.position.x - radius) + val yWrapDifferenceBottom = tile.position.y - (otherTile.position.y - radius) + val xWrapDifferenceTop = tile.position.x - (otherTile.position.x + radius) + val yWrapDifferenceTop = tile.position.y - (otherTile.position.y + radius) + + return when { + xDifference == 1f && yDifference == 1f -> 6 // otherTile is below + xDifference == -1f && yDifference == -1f -> 12 // otherTile is above + xDifference == 1f || xWrapDifferenceBottom == 1f -> 4 // otherTile is bottom-right + yDifference == 1f || yWrapDifferenceBottom == 1f -> 8 // otherTile is bottom-left + xDifference == -1f || xWrapDifferenceTop == -1f -> 10 // otherTile is top-left + yDifference == -1f || yWrapDifferenceTop == -1f -> 2 // otherTile is top-right + else -> -1 + } + } + + /** Convert relative direction of [otherTile] seen from [tile]'s position into a vector + * in world coordinates of length sqrt(3), so that it can be used to go from tile center to + * the edge of the hex in that direction (meaning the center of the border between the hexes) + */ + fun getNeighborTilePositionAsWorldCoords(tile: TileInfo, otherTile: TileInfo): Vector2 = + HexMath.getClockDirectionToWorldVector(getNeighborTileClockPosition(tile, otherTile)) + + /** + * Returns the closest position to (0, 0) outside the map which can be wrapped + * to the position of the given vector + */ + fun getUnWrappedPosition(position: Vector2): Vector2 { + if (!contains(position)) + return position //The position is outside the map so its unwrapped already + + val radius = if (mapParameters.shape == MapShape.rectangular) + mapParameters.mapSize.width / 2 + else mapParameters.mapSize.radius + + val vectorUnwrappedLeft = Vector2(position.x + radius, position.y - radius) + val vectorUnwrappedRight = Vector2(position.x - radius, position.y + radius) + + return if (vectorUnwrappedRight.len() < vectorUnwrappedLeft.len()) + vectorUnwrappedRight + else + vectorUnwrappedLeft + } + + /** @return List of tiles visible from location [position] for a unit with sight range [sightDistance] */ + fun getViewableTiles(position: Vector2, sightDistance: Int): List { + val viewableTiles = getTilesInDistance(position, 1).toMutableList() + val currentTileHeight = get(position).getHeight() + + for (i in 1..sightDistance) { // in each layer, + // This is so we don't use tiles in the same distance to "see over", + // that is to say, the "viewableTiles.contains(it) check will return false for neighbors from the same distance + val tilesToAddInDistanceI = ArrayList() + + for (cTile in getTilesAtDistance(position, i)) { // for each tile in that layer, + val cTileHeight = cTile.getHeight() + + /* + Okay so, if we're looking at a tile from a to c with b in the middle, + we have several scenarios: + 1. a>b - - I can see everything, b does not hide c + 2. a==b + 2.1 c>b - c is tall enough I can see it over b! + 2.2 b blocks view from same-elevation tiles - hides c + 2.3 none of the above - I can see c + 3. a=c - b hides c + 3.2 bb || c>b || (a==b && b !blocks same-elevation view)" + */ + + val containsViewableNeighborThatCanSeeOver = cTile.neighbors.any { + bNeighbor: TileInfo -> + val bNeighborHeight = bNeighbor.getHeight() + viewableTiles.contains(bNeighbor) && ( + currentTileHeight > bNeighborHeight // a>b + || cTileHeight > bNeighborHeight // c>b + || currentTileHeight == bNeighborHeight // a==b + && !bNeighbor.hasUnique("Blocks line-of-sight from tiles at same elevation")) + } + if (containsViewableNeighborThatCanSeeOver) tilesToAddInDistanceI.add(cTile) + } + viewableTiles.addAll(tilesToAddInDistanceI) + } + + return viewableTiles + } + + /** Strips all units from [TileMap] + * @return stripped [clone] of [TileMap] + */ + fun stripAllUnits(): TileMap { + return clone().apply { tileList.forEach { it.stripUnits() } } + } + + /** Build a list of incompatibilities of a map with a ruleset for the new game loader + * + * Is run before setTransients, so make do without startingLocationsByNation + */ + fun getRulesetIncompatibility(ruleset: Ruleset): HashSet { + val rulesetIncompatibilities = HashSet() + for (set in values.map { it.getRulesetIncompatibility(ruleset) }) + rulesetIncompatibilities.addAll(set) + for ((_, nationName) in startingLocations) { + if (nationName !in ruleset.nations) + rulesetIncompatibilities.add("Nation [$nationName] does not exist in ruleset!") + } + rulesetIncompatibilities.remove("") + return rulesetIncompatibilities + } + + //endregion + //region State-Changing Methods + + /** Initialize transients - without, most operations, like [get] from coordinates, will fail. + * @param ruleset Required unless this is a clone of an initialized TileMap including one + * @param setUnitCivTransients when false Civ-specific parts of unit initialization are skipped, for the map editor. + */ + fun setTransients(ruleset: Ruleset? = null, setUnitCivTransients: Boolean = true) { + if (ruleset != null) this.ruleset = ruleset + if (this.ruleset == null) throw(IllegalStateException("TileMap.setTransients called without ruleset")) + + if (tileMatrix.isEmpty()) { + val topY = tileList.asSequence().map { it.position.y.toInt() }.maxOrNull()!! + bottomY = tileList.asSequence().map { it.position.y.toInt() }.minOrNull()!! + val rightX = tileList.asSequence().map { it.position.x.toInt() }.maxOrNull()!! + leftX = tileList.asSequence().map { it.position.x.toInt() }.minOrNull()!! + + for (x in leftX..rightX) { + val row = ArrayList() + for (y in bottomY..topY) row.add(null) + tileMatrix.add(row) + } + } else { + // Yes the map generator calls this repeatedly, and we don't want to end up with an oversized tileMatrix + // rightX is -leftX or -leftX + 1 + if (tileMatrix.size != 1 - 2 * leftX && tileMatrix.size != 2 - 2 * leftX) + throw(IllegalStateException("TileMap.setTransients called on existing tileMatrix of different size")) + } + + for (tileInfo in values) { + tileMatrix[tileInfo.position.x.toInt() - leftX][tileInfo.position.y.toInt() - bottomY] = tileInfo + tileInfo.tileMap = this + tileInfo.ruleset = this.ruleset!! + tileInfo.setTerrainTransients() + tileInfo.setUnitTransients(setUnitCivTransients) + } + } /** Tries to place the [unitName] into the [TileInfo] closest to the given [position] * @param position where to try to place the unit (or close - max 10 tiles distance) @@ -224,64 +445,13 @@ class TileMap { } - fun getViewableTiles(position: Vector2, sightDistance: Int): List { - val viewableTiles = getTilesInDistance(position, 1).toMutableList() - val currentTileHeight = get(position).getHeight() - - for (i in 1..sightDistance) { // in each layer, - // This is so we don't use tiles in the same distance to "see over", - // that is to say, the "viewableTiles.contains(it) check will return false for neighbors from the same distance - val tilesToAddInDistanceI = ArrayList() - - for (cTile in getTilesAtDistance(position, i)) { // for each tile in that layer, - val cTileHeight = cTile.getHeight() - - /* - Okay so, if we're looking at a tile from a to c with b in the middle, - we have several scenarios: - 1. a>b - - I can see everything, b does not hide c - 2. a==b - 2.1 c>b - c is tall enough I can see it over b! - 2.2 b blocks view from same-elevation tiles - hides c - 2.3 none of the above - I can see c - 3. a=c - b hides c - 3.2 bb || c>b || (a==b && b !blocks same-elevation view)" - */ - - val containsViewableNeighborThatCanSeeOver = cTile.neighbors.any { - bNeighbor: TileInfo -> - val bNeighborHeight = bNeighbor.getHeight() - viewableTiles.contains(bNeighbor) && ( - currentTileHeight > bNeighborHeight // a>b - || cTileHeight > bNeighborHeight // c>b - || currentTileHeight == bNeighborHeight // a==b - && !bNeighbor.hasUnique("Blocks line-of-sight from tiles at same elevation")) - } - if (containsViewableNeighborThatCanSeeOver) tilesToAddInDistanceI.add(cTile) - } - viewableTiles.addAll(tilesToAddInDistanceI) - } - - return viewableTiles - } - - /** Strips all units from [TileMap] - * @return stripped clone of [TileMap] - */ - fun stripAllUnits(): TileMap { - return clone().apply { tileList.forEach { it.stripUnits() } } - } - - /** Strips all units and starting location from [TileMap] for specified [Player] + /** Strips all units and starting locations from [TileMap] for specified [Player] * Operation in place - * @param player units of player to be stripped off + * @param player units of this player will be removed */ fun stripPlayer(player: Player) { tileList.forEach { - if (it.improvement == "StartingLocation " + player.chosenCiv) { + if (it.improvement == startingLocationPrefix + player.chosenCiv) { it.improvement = null } for (unit in it.getUnits()) if (unit.owner == player.chosenCiv) unit.removeFromTile() @@ -295,8 +465,8 @@ class TileMap { */ fun switchPlayersNation(player: Player, newNation: Nation) { tileList.forEach { - if (it.improvement == "StartingLocation " + player.chosenCiv) { - it.improvement = "StartingLocation " + newNation.name + if (it.improvement == startingLocationPrefix + player.chosenCiv) { + it.improvement = startingLocationPrefix + newNation.name } for (unit in it.getUnits()) if (unit.owner == player.chosenCiv) { unit.owner = newNation.name @@ -305,80 +475,61 @@ class TileMap { } } - fun setTransients(ruleset: Ruleset, setUnitCivTransients: Boolean = true) { // In the map editor, no Civs or Game exist, so we won't set the unit transients - val topY = tileList.asSequence().map { it.position.y.toInt() }.maxOrNull()!! - bottomY = tileList.asSequence().map { it.position.y.toInt() }.minOrNull()!! - val rightX = tileList.asSequence().map { it.position.x.toInt() }.maxOrNull()!! - leftX = tileList.asSequence().map { it.position.x.toInt() }.minOrNull()!! - - for (x in leftX..rightX) { - val row = ArrayList() - for (y in bottomY..topY) row.add(null) - tileMatrix.add(row) - } - - for (tileInfo in values) { - tileMatrix[tileInfo.position.x.toInt() - leftX][tileInfo.position.y.toInt() - bottomY] = tileInfo - tileInfo.tileMap = this - tileInfo.ruleset = ruleset - tileInfo.setTerrainTransients() - tileInfo.setUnitTransients(setUnitCivTransients) + /** + * Initialize startingLocations transients, including legacy support (maps saved with placeholder improvements) + */ + fun setStartingLocationsTransients() { + if (startingLocations.size == 1 && startingLocations[0].nation == legacyMarker) + return translateStartingLocationsFromMap() + startingLocationsByNation.clear() + for ((position, nationName) in startingLocations) { + val nationSet = startingLocationsByNation[nationName] ?: hashSetOf().also { startingLocationsByNation[nationName] = it } + nationSet.add(get(position)) } } /** - * Returns the clockPosition of otherTile seen from tile's position - * Returns -1 if not neighbors + * Scan and remove placeholder improvements from map and build startingLocations from them */ - fun getNeighborTileClockPosition(tile: TileInfo, otherTile: TileInfo): Int { - val radius = if (mapParameters.shape == MapShape.rectangular) - mapParameters.mapSize.width / 2 - else mapParameters.mapSize.radius + fun translateStartingLocationsFromMap() { + startingLocations.clear() + tileList.asSequence() + .filter { it.improvement?.startsWith(startingLocationPrefix) == true } + .map { it to StartingLocation(it.position, it.improvement!!.removePrefix(startingLocationPrefix)) } + .sortedBy { it.second.nation } // vanity, or to make diffs between un-gzipped map files easier + .forEach { (tile, startingLocation) -> + tile.improvement = null + startingLocations.add(startingLocation) + } + setStartingLocationsTransients() + } - val xDifference = tile.position.x - otherTile.position.x - val yDifference = tile.position.y - otherTile.position.y - val xWrapDifferenceBottom = tile.position.x - (otherTile.position.x - radius) - val yWrapDifferenceBottom = tile.position.y - (otherTile.position.y - radius) - val xWrapDifferenceTop = tile.position.x - (otherTile.position.x + radius) - val yWrapDifferenceTop = tile.position.y - (otherTile.position.y + radius) - - return when { - xDifference == 1f && yDifference == 1f -> 6 // otherTile is below - xDifference == -1f && yDifference == -1f -> 12 // otherTile is above - xDifference == 1f || xWrapDifferenceBottom == 1f -> 4 // otherTile is bottom-right - yDifference == 1f || yWrapDifferenceBottom == 1f -> 8 // otherTile is bottom-left - xDifference == -1f || xWrapDifferenceTop == -1f -> 10 // otherTile is top-left - yDifference == -1f || yWrapDifferenceTop == -1f -> 2 // otherTile is top-right - else -> -1 + /** + * Place placeholder improvements on the map for the startingLocations entries. + * + * **For use by the map editor only** + * + * This is a copy, the startingLocations array and transients are untouched. + * Any actual improvements on the tiles will be overwritten. + */ + fun translateStartingLocationsToMap() { + for ((position, nationName) in startingLocations) { + get(position).improvement = startingLocationPrefix + nationName } } - /** Convert relative direction of otherTile seen from tile's position into a vector - * in world coordinates of length sqrt(3), so that it can be used to go from tile center to - * the edge of the hex in that direction (meaning the center of the border between the hexes) - */ - fun getNeighborTilePositionAsWorldCoords(tile: TileInfo, otherTile: TileInfo): Vector2 = - HexMath.getClockDirectionToWorldVector(getNeighborTileClockPosition(tile, otherTile)) - - - /** - * Returns the closest position to (0, 0) outside the map which can be wrapped - * to the position of the given vector - */ - fun getUnWrappedPosition(position: Vector2): Vector2 { - if (!contains(position)) - return position //The position is outside the map so its unwrapped already - - var radius = mapParameters.mapSize.radius - if (mapParameters.shape == MapShape.rectangular) - radius = mapParameters.mapSize.width / 2 - - val vectorUnwrappedLeft = Vector2(position.x + radius, position.y - radius) - val vectorUnwrappedRight = Vector2(position.x - radius, position.y + radius) - - return if (vectorUnwrappedRight.len() < vectorUnwrappedLeft.len()) - vectorUnwrappedRight - else - vectorUnwrappedLeft + /** Adds a starting position, maintaining the transients */ + fun addStartingLocation(nationName: String, tile: TileInfo) { + startingLocations.add(StartingLocation(tile.position, nationName)) + val nationSet = startingLocationsByNation[nationName] ?: hashSetOf().also { startingLocationsByNation[nationName] = it } + nationSet.add(tile) } + + /** Clears starting positions, e.g. after GameStarter is done with them. Does not clear the pseudo-improvements. */ + fun clearStartingLocations() { + startingLocations.clear() + startingLocationsByNation.clear() + } + + //endregion } diff --git a/core/src/com/unciv/models/metadata/GameParameters.kt b/core/src/com/unciv/models/metadata/GameParameters.kt index cc5aef0cae..a4752e16e9 100644 --- a/core/src/com/unciv/models/metadata/GameParameters.kt +++ b/core/src/com/unciv/models/metadata/GameParameters.kt @@ -40,6 +40,7 @@ class GameParameters { // Default values are the default new game parameters.noBarbarians = noBarbarians parameters.oneCityChallenge = oneCityChallenge parameters.nuclearWeaponsEnabled = nuclearWeaponsEnabled + parameters.religionEnabled = religionEnabled parameters.victoryTypes = ArrayList(victoryTypes) parameters.startingEra = startingEra parameters.isOnlineMultiplayer = isOnlineMultiplayer @@ -47,4 +48,24 @@ class GameParameters { // Default values are the default new game parameters.mods = LinkedHashSet(mods) return parameters } + + // For debugging and MapGenerator console output + override fun toString() = "($difficulty $gameSpeed $startingEra, " + + "${players.count { it.playerType == PlayerType.Human }} ${PlayerType.Human} " + + "${players.count { it.playerType == PlayerType.AI }} ${PlayerType.AI} " + + "$numberOfCityStates CS, " + + sequence { + if (isOnlineMultiplayer) yield("Online Multiplayer") + if (noBarbarians) yield("No barbs") + if (oneCityChallenge) yield("OCC") + if (!nuclearWeaponsEnabled) yield("No nukes") + if (religionEnabled) yield("Religion") + if (godMode) yield("God mode") + if (VictoryType.Cultural !in victoryTypes) yield("No ${VictoryType.Cultural} Victory") + if (VictoryType.Diplomatic in victoryTypes) yield("${VictoryType.Diplomatic} Victory") + if (VictoryType.Domination !in victoryTypes) yield("No ${VictoryType.Domination} Victory") + if (VictoryType.Scientific !in victoryTypes) yield("No ${VictoryType.Scientific} Victory") + }.joinToString() + + (if (mods.isEmpty()) ", no mods" else mods.joinToString(",", ", mods=(", ")", 6) ) + + ")" } \ No newline at end of file diff --git a/core/src/com/unciv/ui/mapeditor/MapEditorOptionsTable.kt b/core/src/com/unciv/ui/mapeditor/MapEditorOptionsTable.kt index ef991c4916..82bb319997 100644 --- a/core/src/com/unciv/ui/mapeditor/MapEditorOptionsTable.kt +++ b/core/src/com/unciv/ui/mapeditor/MapEditorOptionsTable.kt @@ -162,7 +162,7 @@ class MapEditorOptionsTable(val mapEditorScreen: MapEditorScreen): Table(CameraS val nationImage = getHex(ImageGetter.getNationIndicator(nation, 40f)) nationImage.onClick { - val improvementName = "StartingLocation " + nation.name + val improvementName = TileMap.startingLocationPrefix + nation.name tileAction = { it.improvement = improvementName for ((tileInfo, tileGroups) in mapEditorScreen.mapHolder.tileGroups) { @@ -267,17 +267,6 @@ class MapEditorOptionsTable(val mapEditorScreen: MapEditorScreen): Table(CameraS editorPickTable.add(AutoScrollPane(unitsTable)).height(scrollPanelHeight) } - private fun nationsFromMap(tileMap: TileMap): ArrayList { - val tilesWithStartingLocations = tileMap.values - .filter { it.improvement != null && it.improvement!!.startsWith("StartingLocation ") } - var nations = ArrayList() - for (tile in tilesWithStartingLocations) { - var civName = tile.improvement!!.removePrefix("StartingLocation ") - nations.add(ruleset.nations[civName]!!) - } - return nations - } - private fun getPlayerIndexString(player: Player): String { val index = gameParameters.players.indexOf(player) + 1 return "Player [$index]".tr() diff --git a/core/src/com/unciv/ui/mapeditor/MapEditorScreen.kt b/core/src/com/unciv/ui/mapeditor/MapEditorScreen.kt index 65b0c6fb32..8a6f2c4ec8 100644 --- a/core/src/com/unciv/ui/mapeditor/MapEditorScreen.kt +++ b/core/src/com/unciv/ui/mapeditor/MapEditorScreen.kt @@ -35,6 +35,8 @@ class MapEditorScreen(): CameraStageBaseScreen() { private fun initialize() { ImageGetter.setNewRuleset(ruleset) tileMap.setTransients(ruleset,false) + tileMap.setStartingLocationsTransients() + tileMap.translateStartingLocationsToMap() UncivGame.Current.translations.translationActiveMods = ruleset.mods mapHolder = EditorMapHolder(this, tileMap) diff --git a/core/src/com/unciv/ui/mapeditor/SaveAndLoadMapScreen.kt b/core/src/com/unciv/ui/mapeditor/SaveAndLoadMapScreen.kt index e93ab867ca..123573085e 100644 --- a/core/src/com/unciv/ui/mapeditor/SaveAndLoadMapScreen.kt +++ b/core/src/com/unciv/ui/mapeditor/SaveAndLoadMapScreen.kt @@ -6,14 +6,12 @@ import com.badlogic.gdx.graphics.Color 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.badlogic.gdx.utils.Json import com.unciv.logic.MapSaver import com.unciv.logic.map.MapType import com.unciv.logic.map.TileMap import com.unciv.models.ruleset.RulesetCache import com.unciv.models.translations.tr import com.unciv.ui.pickerscreens.PickerScreen -import com.unciv.ui.saves.Gzip import com.unciv.ui.utils.* import kotlin.concurrent.thread import com.unciv.ui.utils.AutoScrollPane as ScrollPane @@ -35,7 +33,7 @@ class SaveAndLoadMapScreen(mapToSave: TileMap?, save:Boolean = false, previousSc mapToSave.mapParameters.type = MapType.custom thread(name = "SaveMap") { try { - MapSaver.saveMap(mapNameTextField.text, mapToSave) + MapSaver.saveMap(mapNameTextField.text, getMapCloneForSave(mapToSave)) Gdx.app.postRunnable { Gdx.input.inputProcessor = null // This is to stop ANRs happening here, until the map editor screen sets up. game.setScreen(MapEditorScreen(mapToSave)) @@ -119,9 +117,7 @@ class SaveAndLoadMapScreen(mapToSave: TileMap?, save:Boolean = false, previousSc if (save) { val copyMapAsTextButton = "Copy to clipboard".toTextButton() val copyMapAsTextAction = { - val json = Json().toJson(mapToSave) - val base64Gzip = Gzip.zip(json) - Gdx.app.clipboard.contents = base64Gzip + Gdx.app.clipboard.contents = MapSaver.mapToSavedString(getMapCloneForSave(mapToSave!!)) } copyMapAsTextButton.onClick (copyMapAsTextAction) keyPressDispatcher[KeyCharAndCode.ctrl('C')] = copyMapAsTextAction @@ -132,8 +128,7 @@ class SaveAndLoadMapScreen(mapToSave: TileMap?, save:Boolean = false, previousSc val loadFromClipboardAction = { try { val clipboardContentsString = Gdx.app.clipboard.contents.trim() - val decoded = Gzip.unzip(clipboardContentsString) - val loadedMap = MapSaver.mapFromJson(decoded) + val loadedMap = MapSaver.mapFromSavedString(clipboardContentsString) game.setScreen(MapEditorScreen(loadedMap)) } catch (ex: Exception) { couldNotLoadMapLabel.isVisible = true @@ -187,4 +182,8 @@ class SaveAndLoadMapScreen(mapToSave: TileMap?, save:Boolean = false, previousSc } } + fun getMapCloneForSave(mapToSave: TileMap) = mapToSave!!.clone().also { + it.setTransients(setUnitCivTransients = false) + it.translateStartingLocationsFromMap() + } } diff --git a/core/src/com/unciv/ui/newgamescreen/NewGameScreen.kt b/core/src/com/unciv/ui/newgamescreen/NewGameScreen.kt index 0cc835da92..a70962ec93 100644 --- a/core/src/com/unciv/ui/newgamescreen/NewGameScreen.kt +++ b/core/src/com/unciv/ui/newgamescreen/NewGameScreen.kt @@ -91,10 +91,7 @@ class NewGameScreen( if (mapOptionsTable.mapTypeSelectBox.selected.value == MapType.custom){ val map = MapSaver.loadMap(gameSetupInfo.mapFile!!) - val rulesetIncompatibilities = HashSet() - for (set in map.values.map { it.getRulesetIncompatibility(ruleset) }) - rulesetIncompatibilities.addAll(set) - rulesetIncompatibilities.remove("") + val rulesetIncompatibilities = map.getRulesetIncompatibility(ruleset) if (rulesetIncompatibilities.isNotEmpty()) { val incompatibleMap = Popup(this) diff --git a/core/src/com/unciv/ui/tilegroups/TileGroup.kt b/core/src/com/unciv/ui/tilegroups/TileGroup.kt index 28b3ae035a..70d7e4b41b 100644 --- a/core/src/com/unciv/ui/tilegroups/TileGroup.kt +++ b/core/src/com/unciv/ui/tilegroups/TileGroup.kt @@ -11,6 +11,7 @@ import com.unciv.UncivGame import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.map.RoadStatus import com.unciv.logic.map.TileInfo +import com.unciv.logic.map.TileMap import com.unciv.ui.cityscreen.YieldGroup import com.unciv.ui.utils.ImageGetter import com.unciv.ui.utils.center @@ -331,9 +332,11 @@ open class TileGroup(var tileInfo: TileInfo, var tileSetStrings:TileSetStrings, } private fun removeMissingModReferences() { + // This runs from map editor too, so the Pseudo-improvements for starting locations need to stay. + // The nations can be checked. val improvementName = tileInfo.improvement - if(improvementName != null && improvementName.startsWith("StartingLocation ")){ - val nationName = improvementName.removePrefix("StartingLocation ") + if (improvementName != null && improvementName.startsWith(TileMap.startingLocationPrefix)) { + val nationName = improvementName.removePrefix(TileMap.startingLocationPrefix) if (!tileInfo.ruleset.nations.containsKey(nationName)) tileInfo.improvement = null } diff --git a/core/src/com/unciv/ui/utils/ImageGetter.kt b/core/src/com/unciv/ui/utils/ImageGetter.kt index bccbd21561..5a1149e214 100644 --- a/core/src/com/unciv/ui/utils/ImageGetter.kt +++ b/core/src/com/unciv/ui/utils/ImageGetter.kt @@ -16,6 +16,7 @@ import com.badlogic.gdx.scenes.scene2d.utils.TextureRegionDrawable import com.badlogic.gdx.utils.Align import com.unciv.Constants import com.unciv.UncivGame +import com.unciv.logic.map.TileMap import com.unciv.models.ruleset.Era import com.unciv.models.ruleset.Nation import com.unciv.models.ruleset.Ruleset @@ -253,8 +254,8 @@ object ImageGetter { fun getImprovementIcon(improvementName: String, size: Float = 20f): Actor { if (improvementName.startsWith("Remove") || improvementName == Constants.cancelImprovementOrder) return Table().apply { add(getImage("OtherIcons/Stop")).size(size) } - if (improvementName.startsWith("StartingLocation ")) { - val nationName = improvementName.removePrefix("StartingLocation ") + if (improvementName.startsWith(TileMap.startingLocationPrefix)) { + val nationName = improvementName.removePrefix(TileMap.startingLocationPrefix) val nation = ruleset.nations[nationName]!! return getNationIndicator(nation, size) } diff --git a/core/src/com/unciv/ui/worldscreen/bottombar/TileInfoTable.kt b/core/src/com/unciv/ui/worldscreen/bottombar/TileInfoTable.kt index c0ffab54f2..e5fc0e4c3a 100644 --- a/core/src/com/unciv/ui/worldscreen/bottombar/TileInfoTable.kt +++ b/core/src/com/unciv/ui/worldscreen/bottombar/TileInfoTable.kt @@ -24,9 +24,9 @@ class TileInfoTable(private val viewingCiv :CivilizationInfo) : Table(CameraStag add(getStatsTable(tile)) add( MarkupRenderer.render(tile.toMarkup(viewingCiv), padding = 0f, noLinkImages = true) { UncivGame.Current.setScreen(CivilopediaScreen(viewingCiv.gameInfo.ruleSet, link = it)) - } ).pad(5f) - // For debug only! -// add(tile.position.toString().toLabel()).colspan(2).pad(10f) + } ).pad(5f).row() + if (UncivGame.Current.viewEntireMapForDebug) + add(tile.position.run { "(${x.toInt()},${y.toInt()})" }.toLabel()).colspan(2).pad(5f) } pack()