mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-09 07:18:57 +07:00
Wesnoth map import polished up (#10580)
This commit is contained in:
@ -6,8 +6,19 @@
|
||||
"Aa":["Snow"],
|
||||
"Ai":["Coast","Ice"],
|
||||
"Br":["Railroad"],
|
||||
"C":["Plains"], // Most Castle variants look dark
|
||||
"Cd":["Desert"], // Desert Castle
|
||||
"Cea":["Plains"], // Snowy Encampment
|
||||
"Cfa":["Tundra"], // Winter Dwarven Castle
|
||||
"Cha":["Plains"], // Snowy Human Castle
|
||||
"Chs":["Marsh"], // Swamp Human Ruin
|
||||
"Chw":["Coast"], // Sunken Human Ruin
|
||||
"Cm":["Coast"], // Aquatic Castle
|
||||
"Coa":["Plains"], // Snowy Orcish Castle
|
||||
"Cv":["Grassland"], // Elven Castle
|
||||
"Cva":["Tundra"], // Winter Elven Castle
|
||||
"D":["Desert"],
|
||||
// "Dc" to listOf(Constants.mountain, "Barringer Crater"), // "Crater" - Nope can have many
|
||||
// "Dc":["Mountain","Barringer Crater"], // "Crater" - Nope can have many
|
||||
"Do":["Desert","Oasis"],
|
||||
"F":["Forest"],
|
||||
"Fda":["Tundra","Forest"], // Snowy Deciduous Forest
|
||||
@ -23,7 +34,19 @@
|
||||
"Hhd":["Plains","Hill"], // Dry Hills
|
||||
"Hd":["Desert","Hill"], // Dunes
|
||||
"Ha":["Snow","Hill"], // Snow Hills
|
||||
"K":["Plains"], // Most Keep variants look brownish
|
||||
"Kd":["Desert","Hill"], // Desert Keep
|
||||
"Kea":["Tundra"], // Snowy Encampment Keep
|
||||
"Kfa":["Tundra"], // Winter Dwarven Keep
|
||||
"Kha":["Plains"], // Snowy Human Castle Keep
|
||||
"Khr":["Plains","City ruins"], // Ruined Human Castle Keep
|
||||
"Khw":["Plains","Hill"], // Sunken Human Castle Keep - let's make such a start...
|
||||
"Km":["Plains","Hill"], // Aquatic Keep - ...a tiny island w/2 prod
|
||||
"Koa":["Tundra","Hill"], // Snowy Orcish Keep
|
||||
"Kv":["Forest"], // Elvish Keep
|
||||
"Kva":["Tundra","Forest"], // Winter Elven Keep
|
||||
"M":["Mountain"],
|
||||
"Q":["Ocean"], // "Unwalkable" (fliers can enter), e.g. underground chasms - or make them Lakes?
|
||||
"R":["Plains"], // All their "R" actually means roads - but they mean full-tile low-movement and brownish so..
|
||||
"Rd":["Desert"], // Dry Dirt Road
|
||||
"Rra":["Tundra"], // Icy Cobbles
|
||||
@ -35,8 +58,12 @@
|
||||
"Vhr":["Ancient ruins"], // Ruined Cottage
|
||||
"Vhcr":["City ruins"], // Ruined Human City
|
||||
"Vhhr":["Ancient ruins"], // Ruined Hill Stone Village
|
||||
"Vo":["Barbarian encampment"], // Orcish Village
|
||||
"Vm":["Coast"], // Mermaid village:?
|
||||
"Vo":["Barbarian encampment"], // Orcish Village: Will not function as spawner in game
|
||||
"Wo":["Ocean"],
|
||||
"Ww":["Coast"],
|
||||
"Wwrt":["Coast","Atoll"] // Tropical Coastal Reef, may be too many
|
||||
"Wwrt":["Coast","Atoll"], // Tropical Coastal Reef, may be too many
|
||||
"X":["Mountain"], // Cave walls and obstacles
|
||||
// Special, allows defining the fallback for the required Base Terrain: If this does not contain one, the importer can crash
|
||||
"fallback":["Grassland"]
|
||||
}
|
||||
|
@ -40,8 +40,15 @@ class MapEditorWesnothImporter(private val editorScreen: MapEditorScreen) : Disp
|
||||
|
||||
private var importJob: Job? = null
|
||||
|
||||
private val ignoreLines = setOf("usage=map", "border_size=1")
|
||||
|
||||
private val parseTerrain by lazy {
|
||||
Regex("""^((?<start>\d+) )?(?<base>[A-Z_][a-z\\|/]{1,3})(\^(?<layer>[A-Z_][a-z\\|/]{1,3}))?$""")
|
||||
Regex(
|
||||
"""^
|
||||
(((?<start>\d+)|[A-Za-z\d_]+)\ )* # Space-separated prefixes allowed, if numeric it's a starting location, others are scenario-specific (we ignore)
|
||||
(?<base>[A-Z_][a-z\\|/]{1,3}) # mandatory layer
|
||||
(\^(?<layer>[A-Z_][a-z\\|/]{1,3}))? # optional layer separated by `^`
|
||||
$""".trimIndent(), RegexOption.COMMENTS)
|
||||
}
|
||||
|
||||
private val translationCodes: LinkedHashMap<String,ArrayList<String>> by lazy {
|
||||
@ -52,6 +59,10 @@ class MapEditorWesnothImporter(private val editorScreen: MapEditorScreen) : Disp
|
||||
)
|
||||
}
|
||||
|
||||
// The json must define a fallback that includes a BaseTerrain to ensure we can always find a BaseTerrain.
|
||||
// (using key "" and letting translateTerrainWML loop downto 0 is close, but inserts the fallback twice, and we want it only at the end)
|
||||
private val fallback: List<String> by lazy { translationCodes["fallback"] ?: listOf(Constants.grassland) }
|
||||
|
||||
override fun dispose() {
|
||||
importJob?.cancel()
|
||||
}
|
||||
@ -81,7 +92,7 @@ class MapEditorWesnothImporter(private val editorScreen: MapEditorScreen) : Disp
|
||||
} catch (ex: UncivShowableException) {
|
||||
Log.error("Could not load map", ex)
|
||||
Concurrency.runOnGLThread {
|
||||
ToastPopup(ex.message, editorScreen)
|
||||
ToastPopup(ex.message, editorScreen, 4000L)
|
||||
}
|
||||
} catch (ex: Throwable) {
|
||||
Log.error("Could not load map", ex)
|
||||
@ -92,9 +103,13 @@ class MapEditorWesnothImporter(private val editorScreen: MapEditorScreen) : Disp
|
||||
}
|
||||
}
|
||||
|
||||
/** Parses receiver string for a complete Wesnoth map */
|
||||
private fun String.parse(): TileMap {
|
||||
// first we need to know the size. Wesnoth maps have a non-playable border - exclude.
|
||||
val lines = lineSequence().filter { it.isNotBlank() }.toList()
|
||||
// first we need to know the size.
|
||||
// There can be a header, optional, and not used in any campaign map or saved by the editor, skip anyway.
|
||||
// isNotBlank also ensures the line break after the last row won't count as line.
|
||||
// Wesnoth maps have a non-playable border - exclude.
|
||||
val lines = lineSequence().filter { it.isNotBlank() && it !in ignoreLines }.toList()
|
||||
val height = lines.size - 1
|
||||
val width = if (height <= 0) 0 else lines[0].split(',').size - 2
|
||||
if (width <= 0) throw UncivShowableException("That map is invalid!")
|
||||
@ -110,7 +125,7 @@ class MapEditorWesnothImporter(private val editorScreen: MapEditorScreen) : Disp
|
||||
val rowOffset = height / 2
|
||||
for ((row, line) in lines.withIndex()) {
|
||||
for ((column, cellCode) in line.split(',').withIndex()) {
|
||||
val effectiveRow = rowOffset - row + column % 2
|
||||
val effectiveRow = rowOffset - row + column % 2 // see comment block at the top of the file
|
||||
val pos = HexMath.getTileCoordsFromColumnRow(column - colOffset, effectiveRow)
|
||||
if (!map.contains(pos)) continue
|
||||
map[pos].paintFromWesnothCode(cellCode.trim(), map)
|
||||
@ -120,37 +135,49 @@ class MapEditorWesnothImporter(private val editorScreen: MapEditorScreen) : Disp
|
||||
return map
|
||||
}
|
||||
|
||||
/** Modify receiver Tile so it matches a Wesnoth cell code (e.g. "Wo", "Gs^Fp", "1 Ke" or "sceptre Uu^Ii") */
|
||||
private fun Tile.paintFromWesnothCode(cellCode: String, map: TileMap) {
|
||||
// See https://wiki.wesnoth.org/TerrainCodesWML
|
||||
// First do a pattern match that validates and splits off player start indices, base and layer codes, and ignores other scenario prefixes
|
||||
val matches = parseTerrain.matchEntire(cellCode)
|
||||
?: throw UncivShowableException("{That map is invalid!}\n{(\"[$cellCode]\" does not conform to TerrainCodesWML)}")
|
||||
val start = matches.groups["start"]?.value
|
||||
val base = matches.groups["base"]!!.value // This capture is not optional in the pattern
|
||||
val layer = matches.groups["layer"]?.value
|
||||
val allStrings = translateTerrainWML(base) + translateTerrainWML(layer) + Constants.grassland
|
||||
// Now build a loose collection of candidate elements
|
||||
val allStrings: Sequence<String> =
|
||||
translateTerrainWML(base) + translateTerrainWML(layer) + fallback
|
||||
// Map to RulesetObjects and ensure Hills are always under other features
|
||||
val allObjects = allStrings
|
||||
.sortedBy { it != Constants.hill }
|
||||
.mapNotNull { ruleset.tileImprovements[it] ?: ruleset.terrains[it] }
|
||||
.toList()
|
||||
// BaseTerrain is simply the first candidate, the fallback above makes first() not crash (unless the json breaks it)
|
||||
baseTerrain = allObjects.first { it is Terrain && it.type.isBaseTerrain }.name
|
||||
// Features are all valid candiates in order, enduring no duplicates
|
||||
val features = allObjects.filter { it is Terrain && it.type == TerrainType.TerrainFeature }.map { it.name }.distinct()
|
||||
if (features.isNotEmpty()) {
|
||||
setTerrainTransients() // or else can't setTerrainFeatures as baseTerrainObject may be uninitialized
|
||||
setTerrainFeatures(features)
|
||||
}
|
||||
// Optional improvement - mainly Wesnoth Villages to Ancient ruins
|
||||
allObjects.firstOrNull { it is TileImprovement }?.apply { improvement = name }
|
||||
// Start locations are separate - and for now very simple:
|
||||
// If a side number prefix is there, define an "Any" starting location. To actually match names
|
||||
// a separate Wesnoth file would have to be read - and the existing ones match no Civ nation anyway.
|
||||
// ('../scenarios/<mapname>.cfg', WMLpath('scenario/side[i-1]/user_team_name') or team_name,
|
||||
// with a Mod defining Elves, Orcs, Dwarves and so on, and decoupling Civilization from Nation that may even work)
|
||||
if (start == null) return
|
||||
map.addStartingLocation(Constants.spectator, this)
|
||||
}
|
||||
|
||||
/** Get all relevant matches for one layer code from the definition map,
|
||||
* in order from most to least specific, flattened to a Sequence.
|
||||
* (That is, match original then shorten the string stepwise and append all hits together) */
|
||||
private fun translateTerrainWML(code: String?) = sequence {
|
||||
if (code == null) return@sequence
|
||||
translationCodes[code]?.also { yieldAll(it) }
|
||||
if (code.length >= 3) // kotlin slice is unsafe, unlike python slice
|
||||
translationCodes[code.slice(0..2)]?.also { yieldAll(it) }
|
||||
if (code.length >= 2)
|
||||
translationCodes[code.slice(0..1)]?.also { yieldAll(it) }
|
||||
if (code.isNotEmpty())
|
||||
translationCodes[code.slice(0..0)]?.also { yieldAll(it) }
|
||||
for (length in code.length downTo 1) {
|
||||
translationCodes[code.slice(0 until length)]?.also { yieldAll(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user