diff --git a/core/src/com/unciv/ui/components/tilegroups/TileSetStrings.kt b/core/src/com/unciv/ui/components/tilegroups/TileSetStrings.kt index ed2fe6dd36..99bdcdd8cb 100644 --- a/core/src/com/unciv/ui/components/tilegroups/TileSetStrings.kt +++ b/core/src/com/unciv/ui/components/tilegroups/TileSetStrings.kt @@ -9,11 +9,29 @@ import com.unciv.models.tilesets.TileSetConfig import com.unciv.ui.images.ImageAttempter import com.unciv.ui.images.ImageGetter +@Suppress("MemberVisibilityCanBePrivate") // No advandage hiding them + /** + * Resolver translating more abstract tile data to paint on a map into actual texture names. + * + * Deals with variants, e.g. there could be a "City center-asian-Ancient era.png" that would be chosen + * for a "City center"-containing Tile when it is to be drawn for a Nation defining it's style as "asian" + * and whose techs say it's still in the first vanilla Era. + * + * Instantiated once per [TileGroupMap] and per [TileSet][com.unciv.models.tilesets.TileSet] - + * typically once for HexaRealm and once for FantasyHex (fallback) at the start of a player turn, + * and the same two every time they enter a CityScreen. + * * @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, unitSet: String? = UncivGame.Current.settings.unitSet, fallbackDepth: Int = 1) { +class TileSetStrings( + tileSet: String = UncivGame.Current.settings.tileSet, + unitSet: String? = UncivGame.Current.settings.unitSet, + fallbackDepth: Int = 1 +) { + /** Separator used to mark variants, e.g. nation style or era specific */ + val tag = "-" // 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) @@ -24,7 +42,7 @@ class TileSetStrings(tileSet: String = UncivGame.Current.settings.tileSet, unitS val tileSetConfig = TileSetCache[tileSet]?.config ?: TileSetConfig() // These need to be by lazy since the orFallback expects a tileset, which it may not get. - val hexagon: String by lazy { orFallback {tileSetLocation + "Hexagon"} } + val hexagon: String by lazy { orFallback { tileSetLocation + "Hexagon"} } val hexagonList by lazy { listOf(hexagon) } val crosshatchHexagon by lazy { orFallback { tileSetLocation + "CrosshatchHexagon" } } val unexploredTile by lazy { orFallback { tileSetLocation + "UnexploredTile" } } @@ -68,7 +86,6 @@ class TileSetStrings(tileSet: String = UncivGame.Current.settings.tileSet, unitS return currentString } - val tag = "-" fun getTile(baseTerrain: String) = getString(tilesLocation, baseTerrain) fun getBorder(borderShapeString: String, innerOrOuter:String) = getString(bordersLocation, borderShapeString, innerOrOuter) @@ -81,7 +98,6 @@ class TileSetStrings(tileSet: String = UncivGame.Current.settings.tileSet, unitS TileSetStrings(tileSetConfig.fallbackTileSet!!, tileSetConfig.fallbackTileSet!!, fallbackDepth-1) } - @Suppress("MemberVisibilityCanBePrivate") /** * @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]. @@ -90,18 +106,18 @@ class TileSetStrings(tileSet: String = UncivGame.Current.settings.tileSet, unitS fun orFallback(image: String, fallbackImage: TileSetStrings.() -> String): String { return if (fallback == null || ImageGetter.imageExists(image)) image - else fallback!!.run(fallbackImage) + else fallbackImage.invoke(fallback!!) } /** @see orFallback */ fun orFallback(image: TileSetStrings.() -> String) - = orFallback(this.run(image), image) + = orFallback(image.invoke(this), image) /** For caching image locations based on given parameters (era, style, etc) * Based on what the final image would look like if all parameters existed, - * like "pikeman-Medieval era-France": "pikeman" */ + * like "pikeman-France-Medieval era": "pikeman" */ val imageParamsToImageLocation = HashMap() @@ -110,6 +126,7 @@ class TileSetStrings(tileSet: String = UncivGame.Current.settings.tileSet, unitS val embarkedCivilianUnitLocation = getString(unitsLocation, "EmbarkedUnit-Civilian") val hasEmbarkedCivilianUnitImage = ImageGetter.imageExists(embarkedCivilianUnitLocation) + /** * Image fallbacks work by precedence. * So currently, if you're france, it's the modern era, and you have a pikeman: @@ -119,7 +136,6 @@ class TileSetStrings(tileSet: String = UncivGame.Current.settings.tileSet, unitS * This means that if there's a "pikeman-France" and a "pikeman-Medieval era", * The era-based image wins out, even though it's not the current era. */ - private fun tryGetUnitImageLocation(unit: MapUnit): String? { var baseUnitIconLocation = getString(this.unitsLocation, unit.name) @@ -139,13 +155,13 @@ class TileSetStrings(tileSet: String = UncivGame.Current.settings.tileSet, unitS val style = civInfo.nation.getStyleOrCivName() var imageAttempter = ImageAttempter(baseUnitIconLocation) - // Era+style image: looks like "pikeman-Medieval era-France" + // Era+style image: looks like "pikeman-France-Medieval era" // More advanced eras default to older eras .tryEraImage(civInfo, baseUnitIconLocation, style, this) // Era-only image: looks like "pikeman-Medieval era" .tryEraImage(civInfo, baseUnitIconLocation, null, this) // Style era: looks like "pikeman-France" or "pikeman-European" - .tryImage { getString(baseUnitIconLocation, tag, civInfo.nation.getStyleOrCivName()) } + .tryImage { getString(baseUnitIconLocation, tag, style) } .tryImage { baseUnitIconLocation } if (unit.baseUnit.replaces != null) @@ -154,7 +170,7 @@ class TileSetStrings(tileSet: String = UncivGame.Current.settings.tileSet, unitS return imageAttempter.getPathOrNull() } - fun getUnitImageLocation(unit: MapUnit):String { + fun getUnitImageLocation(unit: MapUnit): String { val imageKey = getString( unit.name, tag, unit.civ.getEra().name, tag, @@ -163,7 +179,7 @@ class TileSetStrings(tileSet: String = UncivGame.Current.settings.tileSet, unitS ) // if in cache return that val currentImageMapping = imageParamsToImageLocation[imageKey] - if (currentImageMapping!=null) return currentImageMapping + if (currentImageMapping != null) return currentImageMapping val imageLocation = tryGetUnitImageLocation(unit) ?: fallback?.tryGetUnitImageLocation(unit) @@ -172,7 +188,7 @@ class TileSetStrings(tileSet: String = UncivGame.Current.settings.tileSet, unitS return imageLocation } - private fun tryGetOwnedTileImageLocation(baseLocation:String, owner:Civilization): String? { + private fun tryGetOwnedTileImageLocation(baseLocation: String, owner: Civilization): String? { val ownersStyle = owner.nation.getStyleOrCivName() return ImageAttempter(baseLocation) .tryEraImage(owner, baseLocation, ownersStyle, this) @@ -181,12 +197,12 @@ class TileSetStrings(tileSet: String = UncivGame.Current.settings.tileSet, unitS .getPathOrNull() } - fun getOwnedTileImageLocation(baseLocation:String, owner:Civilization): String { + fun getOwnedTileImageLocation(baseLocation: String, owner: Civilization): String { val imageKey = getString(baseLocation, tag, owner.getEra().name, tag, owner.nation.getStyleOrCivName()) val currentImageMapping = imageParamsToImageLocation[imageKey] - if (currentImageMapping!=null) return currentImageMapping + if (currentImageMapping != null) return currentImageMapping val imageLocation = tryGetOwnedTileImageLocation(baseLocation, owner) ?: baseLocation diff --git a/core/src/com/unciv/ui/images/ImageAttempter.kt b/core/src/com/unciv/ui/images/ImageAttempter.kt index 2110378735..4da8b3ce2c 100644 --- a/core/src/com/unciv/ui/images/ImageAttempter.kt +++ b/core/src/com/unciv/ui/images/ImageAttempter.kt @@ -3,16 +3,24 @@ package com.unciv.ui.images import com.unciv.logic.civilization.Civilization import com.unciv.ui.components.tilegroups.TileSetStrings +@Suppress("MemberVisibilityCanBePrivate") // no problem for clients to see scope, imageFound, unused/internally used API + /** * Metaprogrammy class for short-circuitingly finding the first existing of multiple image options according to [ImageGetter.imageExists]. * - * Has a [tryImage] method that can be chain-called with functions which return candidate image paths. The first function to return a valid image path stops subsequently chained calls from having any effect, and its result is saved to be retrieved by the [getPath] and [getImage] methods at the end of the candidate chain. + * Has a [tryImage] method that can be chain-called with functions which return candidate image paths. + * The first function to return a valid image path stops subsequently chained calls from having any effect, + * and its result is saved to be retrieved by the [getPath] and [getImage] methods at the end of the candidate chain. * - * Binds candidate functions to a [scope] instance provided to primary constructor, for syntactic convenience. Bind to [Unit] when not needed. + * (So it is similar to Sequence in that intermediate "transforms" are only evaluated when necessary, + * but it is also different as resolution happens early, while chaining, not triggered by a terminating transform.) + * + * Binds candidate functions to a [scope] instance of type [T] provided to primary constructor, for syntactic convenience. Bind to [Unit] when not needed. * * Non-reusable. * - * @property scope Instance to which to bind the candidate-returning functions. For syntactic terseness when making lots of calls to, E.G., [com.unciv.ui.tilegroups.TileSetStrings]. + * @param T type of [scope] + * @property scope Instance to which to bind the candidate-returning functions. For syntactic terseness when making lots of calls to, e.g., [TileSetStrings][com.unciv.ui.components.tilegroups.TileSetStrings]. */ class ImageAttempter(val scope: T) { /** The first valid filename tried if any, or the last filename tried if none have succeeded. */ @@ -26,11 +34,11 @@ class ImageAttempter(val scope: T) { * * @see ImageAttempter * @param fileName Function that returns the filename of the image to check. Bound to [scope]. Will not be run if a valid image has already been found. May return `null` to skip this candidate entirely. - * @return This [ImageAttempter], for chaining. + * @return Chainable `this` [ImageAttempter] extended by one check for [fileName] */ fun tryImage(fileName: T.() -> String?): ImageAttempter { if (!imageFound) { - val imagePath = scope.run(fileName) + val imagePath = fileName.invoke(scope) lastTriedFileName = imagePath ?: lastTriedFileName if (imagePath != null && ImageGetter.imageExists(imagePath)) imageFound = true @@ -42,7 +50,7 @@ class ImageAttempter(val scope: T) { * * @see tryImage * @param fileNames Any number of image candidate returning functions to pass to [tryImage]. - * @return This [ImageAttempter], for chaining. + * @return Chainable `this` [ImageAttempter] extended by zero or more checks for [fileNames] */ fun tryImages(fileNames: Sequence String?>): ImageAttempter { for (fileName in fileNames) { @@ -52,9 +60,14 @@ class ImageAttempter(val scope: T) { } /** Try to load era-specific image variants - * [civInfo]: the civ who owns the tile or unit - * [locationToCheck]: the beginning of the filename to check - * [style]: an optional string to load a civ- or style-specific sprite + * + * Tries eras from the civ's current one down to the first era defined, by json order of eras. + * Result looks like "Plains-Rome-Ancient era": [style] goes before era if supplied. + * + * @param civInfo the civ who owns the tile or unit, used for getEraNumber and ruleset (but not for nation.getStyleOrCivName) + * @param locationToCheck the beginning of the filename to check + * @param style an optional string to load a civ- or style-specific sprite + * @return Chainable `this` [ImageAttempter] extended by one or more checks for era-specific images * */ fun tryEraImage(civInfo: Civilization, locationToCheck: String, style: String?, tileSetStrings: TileSetStrings): ImageAttempter { return tryImages(