diff --git a/android/assets/jsons/TileSets/Default.json b/android/assets/jsons/TileSets/Default.json new file mode 100644 index 0000000000..1b5e5e4f41 --- /dev/null +++ b/android/assets/jsons/TileSets/Default.json @@ -0,0 +1,3 @@ +{ + "fallbackTileSet": null, +} diff --git a/android/assets/jsons/TileSets/FantasyHex.json b/android/assets/jsons/TileSets/FantasyHex.json index fa3f03c705..92e21b0a00 100644 --- a/android/assets/jsons/TileSets/FantasyHex.json +++ b/android/assets/jsons/TileSets/FantasyHex.json @@ -1,5 +1,6 @@ { "useColorAsBaseTerrain": "false", + "fallbackTileSet": null, "ruleVariants": { //Legacy hill support "Hill": ["Grassland","Hill"], diff --git a/core/src/com/unciv/models/tilesets/TileSetCache.kt b/core/src/com/unciv/models/tilesets/TileSetCache.kt index 5010b5a710..77ae7665a6 100644 --- a/core/src/com/unciv/models/tilesets/TileSetCache.kt +++ b/core/src/com/unciv/models/tilesets/TileSetCache.kt @@ -26,11 +26,11 @@ object TileSetCache : HashMap() { mods.addAll(ruleSetMods) clear() for (mod in mods.distinct()) { - val entry = allConfigs.entries.firstOrNull { it.key.mod == mod } ?: continue - - val tileSet = entry.key.tileSet - if (tileSet in this) this[tileSet]!!.updateConfig(entry.value) - else this[tileSet] = entry.value + for (entry in allConfigs.entries.filter { it.key.mod == mod } ) { // Built-in tilesets all have empty strings as their `.mod`, so loop through all of them. + val tileSet = entry.key.tileSet + if (tileSet in this) this[tileSet]!!.updateConfig(entry.value) + else this[tileSet] = entry.value + } } } diff --git a/core/src/com/unciv/models/tilesets/TileSetConfig.kt b/core/src/com/unciv/models/tilesets/TileSetConfig.kt index 76c1a7975a..67369138a1 100644 --- a/core/src/com/unciv/models/tilesets/TileSetConfig.kt +++ b/core/src/com/unciv/models/tilesets/TileSetConfig.kt @@ -6,14 +6,17 @@ class TileSetConfig { var useColorAsBaseTerrain = true var unexploredTileColor: Color = Color.DARK_GRAY var fogOfWarColor: Color = Color.BLACK + /** Name of the tileset to use when this one is missing images. Null to disable. */ + var fallbackTileSet: String? = "FantasyHex" var ruleVariants: HashMap> = HashMap() fun updateConfig(other: TileSetConfig){ useColorAsBaseTerrain = other.useColorAsBaseTerrain unexploredTileColor = other.unexploredTileColor fogOfWarColor = other.fogOfWarColor + fallbackTileSet = other.fallbackTileSet for ((tileSetString, renderOrder) in other.ruleVariants){ ruleVariants[tileSetString] = renderOrder } } -} \ No newline at end of file +} diff --git a/core/src/com/unciv/ui/tilegroups/TileGroup.kt b/core/src/com/unciv/ui/tilegroups/TileGroup.kt index 6d7d402a47..3322ee5d38 100644 --- a/core/src/com/unciv/ui/tilegroups/TileGroup.kt +++ b/core/src/com/unciv/ui/tilegroups/TileGroup.kt @@ -120,7 +120,7 @@ open class TileGroup(var tileInfo: TileInfo, val tileSetStrings:TileSetStrings, val circleCrosshairFogLayerGroup = ActionlessGroup().apply { isTransform = false; setSize(groupSize, groupSize) } val circleImage = ImageGetter.getCircle() // for blue and red circles on the tile private val crosshairImage = ImageGetter.getImage("OtherIcons/Crosshair") // for when a unit is targeted - private val fogImage = ImageGetter.getImage(tileSetStrings.crosshatchHexagon) + private val fogImage = ImageGetter.getImage(tileSetStrings.orFallback { crosshatchHexagon } ) /** * Class for representing an arrow to add to the map at this tile. @@ -132,7 +132,7 @@ open class TileGroup(var tileInfo: TileInfo, val tileSetStrings:TileSetStrings, private class MapArrow(val targetTile: TileInfo, val arrowType: MapArrowType, val tileSetStrings: TileSetStrings) { /** @return An Image from a named arrow texture. */ private fun getArrow(imageName: String): Image { - val imagePath = tileSetStrings.getString(tileSetStrings.tileSetLocation, "Arrows/", imageName) + val imagePath = tileSetStrings.orFallback { getString(tileSetLocation, "Arrows/", imageName) } return ImageGetter.getImage(imagePath) } /** @return An actor for the arrow, based on the type of the arrow. */ @@ -206,8 +206,8 @@ open class TileGroup(var tileInfo: TileInfo, val tileSetStrings:TileSetStrings, } private fun getTileBaseImageLocations(viewingCiv: CivilizationInfo?): List { - if (viewingCiv == null && !showEntireMap) return listOf(tileSetStrings.hexagon) - if (tileInfo.naturalWonder != null) return listOf(tileSetStrings.getTile(tileInfo.naturalWonder!!)) + if (viewingCiv == null && !showEntireMap) return listOf(tileSetStrings.orFallback { hexagon } ) + if (tileInfo.naturalWonder != null) return listOf(tileSetStrings.orFallback { getTile(tileInfo.naturalWonder!!) }) val shownImprovement = tileInfo.getShownImprovement(viewingCiv) val shouldShowImprovement = (shownImprovement != null && UncivGame.Current.settings.showPixelImprovements) @@ -237,13 +237,13 @@ open class TileGroup(var tileInfo: TileInfo, val tileSetStrings:TileSetStrings, return tileSetStrings.tileSetConfig.ruleVariants[allTerrains]!!.map { tileSetStrings.getTile(it) } val allTerrainTile = tileSetStrings.getTile(allTerrains) return if (ImageGetter.imageExists(allTerrainTile)) listOf(allTerrainTile) - else terrainSequence.map { tileSetStrings.getTile(it) }.toList() + else terrainSequence.map { tileSetStrings.orFallback { getTile(it) } }.toList() } private fun getImprovementAndResourceImages(resourceAndImprovementSequence: Sequence): List { val altogether = resourceAndImprovementSequence.joinToString("+").let { tileSetStrings.getTile(it) } return if (ImageGetter.imageExists(altogether)) listOf(altogether) - else resourceAndImprovementSequence.map { tileSetStrings.getTile(it) }.toList() + else resourceAndImprovementSequence.map { tileSetStrings.orFallback { getTile(it) } }.toList() } // Used for both the underlying tile and unit overlays, perhaps for other things in the future @@ -297,7 +297,7 @@ open class TileGroup(var tileInfo: TileInfo, val tileSetStrings:TileSetStrings, } if (tileBaseImages.isEmpty()) { // Absolutely nothing! This is for the 'default' tileset - val image = ImageGetter.getImage(tileSetStrings.hexagon) + val image = ImageGetter.getImage(tileSetStrings.orFallback { hexagon }) tileBaseImages.add(image) baseLayerGroup.addActor(image) setHexagonImageSize(image) @@ -381,7 +381,7 @@ open class TileGroup(var tileInfo: TileInfo, val tileSetStrings:TileSetStrings, baseTerrainOverlayImage = null } - val imagePath = tileSetStrings.getBaseTerrainOverlay(baseTerrain) + val imagePath = tileSetStrings.orFallback { getBaseTerrainOverlay(baseTerrain) } if (!ImageGetter.imageExists(imagePath)) return baseTerrainOverlayImage = ImageGetter.getImage(imagePath) baseTerrainOverlayImage!!.run { @@ -606,7 +606,7 @@ open class TileGroup(var tileInfo: TileInfo, val tileSetStrings:TileSetStrings, } if (roadStatus == RoadStatus.None) continue // no road image - val image = ImageGetter.getImage(tileSetStrings.roadsMap[roadStatus]!!) + val image = ImageGetter.getImage(tileSetStrings.orFallback { roadsMap[roadStatus]!! }) roadImage.image = image val relativeWorldPosition = tileInfo.tileMap.getNeighborTilePositionAsWorldCoords(tileInfo, neighbor) @@ -644,7 +644,7 @@ open class TileGroup(var tileInfo: TileInfo, val tileSetStrings:TileSetStrings, terrainFeatureOverlayImage = null for (terrainFeature in terrainFeatures) { - val terrainFeatureOverlayLocation = tileSetStrings.getTerrainFeatureOverlay(terrainFeature) + val terrainFeatureOverlayLocation = tileSetStrings.orFallback { getTerrainFeatureOverlay(terrainFeature) } if (!ImageGetter.imageExists(terrainFeatureOverlayLocation)) return terrainFeatureOverlayImage = ImageGetter.getImage(terrainFeatureOverlayLocation) terrainFeatureLayerGroup.addActor(terrainFeatureOverlayImage) @@ -662,31 +662,34 @@ open class TileGroup(var tileInfo: TileInfo, val tileSetStrings:TileSetStrings, val militaryUnit = tileInfo.militaryUnit if (militaryUnit != null && showMilitaryUnit) { val unitType = militaryUnit.type - val specificUnitIconLocation = tileSetStrings.unitsLocation + militaryUnit.name - newImageLocation = when { - !UncivGame.Current.settings.showPixelUnits -> "" - militaryUnit.civInfo.nation.style=="" && - ImageGetter.imageExists(specificUnitIconLocation) -> specificUnitIconLocation - ImageGetter.imageExists(specificUnitIconLocation + "-" + militaryUnit.civInfo.nation.style) -> - specificUnitIconLocation + "-" + militaryUnit.civInfo.nation.style - ImageGetter.imageExists(specificUnitIconLocation) -> specificUnitIconLocation - militaryUnit.baseUnit.replaces != null && - ImageGetter.imageExists(tileSetStrings.unitsLocation + militaryUnit.baseUnit.replaces) -> - tileSetStrings.unitsLocation + militaryUnit.baseUnit.replaces - - militaryUnit.civInfo.gameInfo.ruleSet.units.values.any { - it.unitType == unitType.name && ImageGetter.unitIconExists(it.name) - } -> - { - val unitWithSprite = militaryUnit.civInfo.gameInfo.ruleSet.units.values.first { - it.unitType == unitType.name && ImageGetter.unitIconExists(it.name) - }.name - tileSetStrings.unitsLocation + unitWithSprite - } - unitType.isLandUnit() && ImageGetter.imageExists(tileSetStrings.landUnit) -> tileSetStrings.landUnit - unitType.isWaterUnit() && ImageGetter.imageExists(tileSetStrings.waterUnit) -> tileSetStrings.waterUnit - else -> "" + fun TileSetStrings.getThisUnit(): String? { + val specificUnitIconLocation = this.unitsLocation + militaryUnit.name + return when { + !UncivGame.Current.settings.showPixelUnits -> "" + militaryUnit.civInfo.nation.style=="" && + ImageGetter.imageExists(specificUnitIconLocation) -> specificUnitIconLocation + ImageGetter.imageExists(specificUnitIconLocation + "-" + militaryUnit.civInfo.nation.style) -> + specificUnitIconLocation + "-" + militaryUnit.civInfo.nation.style + ImageGetter.imageExists(specificUnitIconLocation) -> specificUnitIconLocation + militaryUnit.baseUnit.replaces != null && + ImageGetter.imageExists(this.unitsLocation + militaryUnit.baseUnit.replaces) -> + this.unitsLocation + militaryUnit.baseUnit.replaces + + militaryUnit.civInfo.gameInfo.ruleSet.units.values.any { + it.unitType == unitType.name && ImageGetter.imageExists(this.unitsLocation + it.name) + } -> + { + val unitWithSprite = militaryUnit.civInfo.gameInfo.ruleSet.units.values.first { + it.unitType == unitType.name && ImageGetter.imageExists(this.unitsLocation + it.name) + }.name + this.unitsLocation + unitWithSprite + } + unitType.isLandUnit() && ImageGetter.imageExists(this.landUnit) -> this.landUnit + unitType.isWaterUnit() && ImageGetter.imageExists(this.waterUnit) -> this.waterUnit + else -> null + } } + newImageLocation = tileSetStrings.getThisUnit() ?: tileSetStrings.fallback?.getThisUnit() ?: "" } if (pixelMilitaryUnitImageLocation != newImageLocation) { @@ -710,16 +713,19 @@ open class TileGroup(var tileInfo: TileInfo, val tileSetStrings:TileSetStrings, val civilianUnit = tileInfo.civilianUnit if (civilianUnit != null && tileIsViewable) { - val specificUnitIconLocation = tileSetStrings.unitsLocation + civilianUnit.name - newImageLocation = when { - !UncivGame.Current.settings.showPixelUnits -> "" - civilianUnit.civInfo.nation.style=="" && - ImageGetter.imageExists(specificUnitIconLocation) -> specificUnitIconLocation - ImageGetter.imageExists(specificUnitIconLocation + "-" + civilianUnit.civInfo.nation.style) -> - specificUnitIconLocation + "-" + civilianUnit.civInfo.nation.style - ImageGetter.imageExists(specificUnitIconLocation) -> specificUnitIconLocation - else -> "" + fun TileSetStrings.getThisUnit(): String? { + val specificUnitIconLocation = this.unitsLocation + civilianUnit.name + return when { + !UncivGame.Current.settings.showPixelUnits -> "" + civilianUnit.civInfo.nation.style=="" && + ImageGetter.imageExists(specificUnitIconLocation) -> specificUnitIconLocation + ImageGetter.imageExists(specificUnitIconLocation + "-" + civilianUnit.civInfo.nation.style) -> + specificUnitIconLocation + "-" + civilianUnit.civInfo.nation.style + ImageGetter.imageExists(specificUnitIconLocation) -> specificUnitIconLocation + else -> null + } } + newImageLocation = tileSetStrings.getThisUnit() ?: tileSetStrings.fallback?.getThisUnit() ?: "" } if (pixelCivilianUnitImageLocation != newImageLocation) { @@ -743,9 +749,9 @@ open class TileGroup(var tileInfo: TileInfo, val tileSetStrings:TileSetStrings, private var bottomLeftRiverImage :Image?=null private fun updateRivers(displayBottomRight:Boolean, displayBottom:Boolean, displayBottomLeft:Boolean){ - bottomRightRiverImage = updateRiver(bottomRightRiverImage,displayBottomRight,tileSetStrings.bottomRightRiver) - bottomRiverImage = updateRiver(bottomRiverImage, displayBottom, tileSetStrings.bottomRiver) - bottomLeftRiverImage = updateRiver(bottomLeftRiverImage,displayBottomLeft,tileSetStrings.bottomLeftRiver) + bottomRightRiverImage = updateRiver(bottomRightRiverImage,displayBottomRight, tileSetStrings.orFallback { bottomRightRiver }) + bottomRiverImage = updateRiver(bottomRiverImage, displayBottom, tileSetStrings.orFallback { bottomRiver }) + bottomLeftRiverImage = updateRiver(bottomLeftRiverImage, displayBottomLeft, tileSetStrings.orFallback { bottomLeftRiver }) } private fun updateRiver(currentImage:Image?, shouldDisplay:Boolean,imageName:String): Image? { diff --git a/core/src/com/unciv/ui/tilegroups/TileSetStrings.kt b/core/src/com/unciv/ui/tilegroups/TileSetStrings.kt index 0b65e463f3..15dfcb0a02 100644 --- a/core/src/com/unciv/ui/tilegroups/TileSetStrings.kt +++ b/core/src/com/unciv/ui/tilegroups/TileSetStrings.kt @@ -4,12 +4,18 @@ import com.unciv.UncivGame import com.unciv.logic.map.RoadStatus import com.unciv.models.tilesets.TileSetCache import com.unciv.models.tilesets.TileSetConfig +import com.unciv.ui.utils.ImageGetter + +/** + * @param tileSet Name of the tileset. Defaults to active at time of instantiation. + * @param fallbackDepth Maximum number of fallback tilesets to try. Used to prevent infinite recursion. + * */ +class TileSetStrings(tileSet: String = UncivGame.Current.settings.tileSet, fallbackDepth: Int = 1) { -class TileSetStrings { // this is so that when we have 100s of TileGroups, they won't all individually come up with all these strings themselves, // it gets pretty memory-intensive (10s of MBs which is a lot for lower-end phones) - val tileSetLocation = "TileSets/" + UncivGame.Current.settings.tileSet + "/" - val tileSetConfig = TileSetCache[UncivGame.Current.settings.tileSet] ?: TileSetConfig() + val tileSetLocation = "TileSets/$tileSet/" + val tileSetConfig = TileSetCache[tileSet] ?: TileSetConfig() val hexagon = tileSetLocation + "Hexagon" val crosshatchHexagon = tileSetLocation + "CrosshatchHexagon" @@ -66,4 +72,30 @@ class TileSetStrings { if (baseTerrain != null) return getString(tilesLocation, baseTerrain, "+", city) else return cityTile } + + /** Fallback [TileSetStrings] to use when the currently chosen tileset is missing an image. */ + val fallback by lazy { + if (fallbackDepth <= 0 || tileSetConfig.fallbackTileSet == null) + null + else + TileSetStrings(tileSetConfig.fallbackTileSet!!, fallbackDepth-1) + } + /** + * @param image An image path string, such as returned from an instance of [TileSetStrings]. + * @param fallbackImage A lambda function that will be run with the [fallback] as its receiver if the original image does not exist according to [ImageGetter.imageExists]. + * @return The original image path string if its image exists, or the return result of the [fallbackImage] lambda if the original image does not exist. + * */ + fun orFallback(image: String, fallbackImage: TileSetStrings.() -> String): String { + return if (fallback == null || ImageGetter.imageExists(image)) + image + else + fallback!!.run(fallbackImage) + } + /** @see orFallback */ + fun orFallback(image: TileSetStrings.() -> String, fallbackImage: TileSetStrings.() -> String) + = orFallback(this.run(image), fallbackImage) + /** @see orFallback */ + fun orFallback(image: TileSetStrings.() -> String) + = orFallback(image, image) + }