Wesnoth map import polished up (#10580)

This commit is contained in:
SomeTroglodyte
2023-11-28 11:01:53 +01:00
committed by GitHub
parent 208ad8a641
commit 48ede93bfa
2 changed files with 70 additions and 16 deletions

View File

@ -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) }
}
}
}