From 12adc0d65a78da41969f9e1625ae7bbd46d4b9d9 Mon Sep 17 00:00:00 2001 From: SomeTroglodyte <63000004+SomeTroglodyte@users.noreply.github.com> Date: Mon, 15 Apr 2024 22:55:13 +0200 Subject: [PATCH] Console autocompletion can display *all* possibilities (#11461) * RiverDirections is now namespaced to RiverGenerator with some pertinent helpers * Autocomplete of RiverDirections is now handled in the default autocomplete * Allow display of possible autocompletions for empty "start" * Brighten that autocomplete possibilities message * Fix a missed spot * Improve removepromotion autocomplete --- .../logic/map/mapgenerator/RiverGenerator.kt | 20 ++++++++++++++++ .../screens/devconsole/ConsoleTileCommands.kt | 21 +++-------------- .../screens/devconsole/ConsoleUnitCommands.kt | 12 ++++++---- .../screens/devconsole/DevConsoleCommand.kt | 23 +++++++++---------- .../ui/screens/devconsole/DevConsolePopup.kt | 6 ++--- 5 files changed, 45 insertions(+), 37 deletions(-) diff --git a/core/src/com/unciv/logic/map/mapgenerator/RiverGenerator.kt b/core/src/com/unciv/logic/map/mapgenerator/RiverGenerator.kt index 9070996ee3..029006e537 100644 --- a/core/src/com/unciv/logic/map/mapgenerator/RiverGenerator.kt +++ b/core/src/com/unciv/logic/map/mapgenerator/RiverGenerator.kt @@ -9,6 +9,12 @@ import com.unciv.models.ruleset.unique.UniqueType import com.unciv.utils.debug import kotlin.math.roundToInt +/** + * Handles River generation for [MapGenerator], [UniqueType.OneTimeChangeTerrain] and console. + * + * Map generation follows the [vertices of map hexes][RiverCoordinate]: [spawnRivers]. + * In-game new rivers work on edges: [continueRiverOn] + */ class RiverGenerator( private val tileMap: TileMap, private val randomness: MapGenerationRandomness, @@ -191,6 +197,20 @@ class RiverGenerator( }.count { it } } + enum class RiverDirections(private val clockPosition: Int) { + North(12), + NorthEast(2), + SouthEast(4), + South(6), + SouthWest(8), + NorthWest(10); + fun getNeighborTile(selectedTile: Tile): Tile? = + selectedTile.tileMap.getClockPositionNeighborTile(selectedTile, clockPosition) + companion object { + val names get() = values().map { it.name } + } + } + companion object { /** [UniqueType.OneTimeChangeTerrain] tries to place a "River" feature. * diff --git a/core/src/com/unciv/ui/screens/devconsole/ConsoleTileCommands.kt b/core/src/com/unciv/ui/screens/devconsole/ConsoleTileCommands.kt index 35a52c9d3a..9fb18d3bbe 100644 --- a/core/src/com/unciv/ui/screens/devconsole/ConsoleTileCommands.kt +++ b/core/src/com/unciv/ui/screens/devconsole/ConsoleTileCommands.kt @@ -3,6 +3,7 @@ package com.unciv.ui.screens.devconsole import com.unciv.Constants import com.unciv.logic.civilization.Civilization import com.unciv.logic.map.mapgenerator.RiverGenerator +import com.unciv.logic.map.mapgenerator.RiverGenerator.RiverDirections import com.unciv.logic.map.tile.RoadStatus import com.unciv.models.ruleset.tile.TerrainType @@ -102,32 +103,16 @@ class ConsoleTileCommands: ConsoleCommandNode { .filter { it.type == TerrainType.TerrainFeature }.findCliInput(param) ?: throw ConsoleErrorException("Unknown feature") - private enum class RiverDirections(val clockPosition: Int) { - North(12), - NorthEast(2), - SouthEast(4), - South(6), - SouthWest(8), - NorthWest(10) - } - private class ConsoleRiverAction(format: String, newValue: Boolean) : ConsoleAction( format, action = { console, params -> action(console, params, newValue) } ) { - override fun autocomplete(console: DevConsolePopup, params: List): String? { - if (params.isEmpty()) return null - // Note this could filter which directions are allowed on the selected tile... too lazy - val options = RiverDirections.values().map { it.name } - return getAutocompleteString(params.last(), options, console) - } - companion object { private fun action(console: DevConsolePopup, params: List, newValue: Boolean): DevConsoleResponse { val selectedTile = console.getSelectedTile() val direction = findCliInput(params[0]) - ?: throw ConsoleErrorException("Unknown direction - use " + RiverDirections.values().joinToString { it.name }) - val otherTile = selectedTile.tileMap.getClockPositionNeighborTile(selectedTile, direction.clockPosition) + ?: throw ConsoleErrorException("Unknown direction - use " + RiverDirections.names.joinToString()) + val otherTile = direction.getNeighborTile(selectedTile) ?: throw ConsoleErrorException("tile has no neighbor to the " + direction.name) if (!otherTile.isLand) throw ConsoleErrorException("there's no land to the " + direction.name) diff --git a/core/src/com/unciv/ui/screens/devconsole/ConsoleUnitCommands.kt b/core/src/com/unciv/ui/screens/devconsole/ConsoleUnitCommands.kt index 81630b4f99..14d0b7f764 100644 --- a/core/src/com/unciv/ui/screens/devconsole/ConsoleUnitCommands.kt +++ b/core/src/com/unciv/ui/screens/devconsole/ConsoleUnitCommands.kt @@ -25,17 +25,18 @@ class ConsoleUnitCommands : ConsoleCommandNode { unit.promotions.addPromotion(promotion.name, true) DevConsoleResponse.OK }) { - override fun autocomplete(console: DevConsolePopup, params: List): String? { - if (params.isEmpty()) return null + override fun autocomplete(console: DevConsolePopup, params: List): String { + // Note: filtering by unit.type.name in promotion.unitTypes sounds good (No [Zero]-Ability on an Archer), + // but would also prevent promotions that can be legally obtained like Morale and Rejuvenation val promotions = console.getSelectedUnit().promotions.promotions val options = console.gameInfo.ruleset.unitPromotions.keys.asSequence() .filter { it !in promotions } .asIterable() - return getAutocompleteString(params.last(), options, console) + return getAutocompleteString(params.lastOrNull().orEmpty(), options, console) } }, - "removepromotion" to ConsoleAction("unit removepromotion ") { console, params -> + "removepromotion" to object : ConsoleAction("unit removepromotion ", { console, params -> val unit = console.getSelectedUnit() val promotion = unit.promotions.getPromotions().findCliInput(params[0]) ?: throw ConsoleErrorException("Promotion not found on unit") @@ -44,6 +45,9 @@ class ConsoleUnitCommands : ConsoleCommandNode { unit.updateUniques() unit.updateVisibleTiles() DevConsoleResponse.OK + }) { + override fun autocomplete(console: DevConsolePopup, params: List) = + getAutocompleteString(params.lastOrNull().orEmpty(), console.getSelectedUnit().promotions.promotions, console) }, "setmovement" to ConsoleAction("unit setmovement [amount]") { console, params -> diff --git a/core/src/com/unciv/ui/screens/devconsole/DevConsoleCommand.kt b/core/src/com/unciv/ui/screens/devconsole/DevConsoleCommand.kt index 4f7fd97782..0807873c61 100644 --- a/core/src/com/unciv/ui/screens/devconsole/DevConsoleCommand.kt +++ b/core/src/com/unciv/ui/screens/devconsole/DevConsoleCommand.kt @@ -1,6 +1,7 @@ package com.unciv.ui.screens.devconsole import com.badlogic.gdx.graphics.Color +import com.unciv.logic.map.mapgenerator.RiverGenerator import com.unciv.models.ruleset.IRulesetObject import com.unciv.models.ruleset.tile.TerrainType import com.unciv.models.stats.Stat @@ -25,14 +26,14 @@ internal inline fun > findCliInput(param: String): T? { } /** Returns the string to *add* to the existing command */ -internal fun getAutocompleteString(lastWord: String, allOptions: Iterable, console: DevConsolePopup): String? { +internal fun getAutocompleteString(lastWord: String, allOptions: Iterable, console: DevConsolePopup): String { console.showResponse(null, Color.WHITE) val matchingOptions = allOptions.map { it.toCliInput() }.filter { it.startsWith(lastWord.toCliInput()) } - if (matchingOptions.isEmpty()) return null + if (matchingOptions.isEmpty()) return "" if (matchingOptions.size == 1) return matchingOptions.first().drop(lastWord.length) + " " - console.showResponse("Matching completions: " + matchingOptions.joinToString(), Color.FOREST) + console.showResponse("Matching completions: " + matchingOptions.joinToString(), Color.LIME.lerp(Color.OLIVE.cpy(), 0.5f)) val firstOption = matchingOptions.first() for ((index, char) in firstOption.withIndex()) { @@ -45,11 +46,11 @@ internal fun getAutocompleteString(lastWord: String, allOptions: Iterable): DevConsoleResponse + /** Returns the string to *add* to the existing command. * The function should add a space at the end if and only if the "match" is an unambiguous choice! - * Returning `null` means there was no match at all, while returning an empty string means the last word is a match as-is. */ - fun autocomplete(console: DevConsolePopup, params: List): String? = "" + fun autocomplete(console: DevConsolePopup, params: List): String = "" } class ConsoleHintException(val hint: String) : Exception() @@ -67,13 +68,11 @@ open class ConsoleAction(val format: String, val action: (console: DevConsolePop } } - override fun autocomplete(console: DevConsolePopup, params: List): String? { - if (params.isEmpty()) return null - + override fun autocomplete(console: DevConsolePopup, params: List): String { val formatParams = format.split(" ").drop(2).map { it.removeSurrounding("<",">").removeSurrounding("[","]").removeSurrounding("\"") } - if (formatParams.size < params.size) return null + if (formatParams.size < params.size) return "" val formatParam = formatParams[params.lastIndex] val lastParam = params.last() @@ -88,6 +87,7 @@ open class ConsoleAction(val format: String, val action: (console: DevConsolePop "stat" -> Stat.names() "religionName" -> console.gameInfo.religions.keys "buildingName" -> console.gameInfo.ruleset.buildings.keys + "direction" -> RiverGenerator.RiverDirections.names else -> listOf() } return getAutocompleteString(lastParam, options, console) @@ -113,9 +113,8 @@ interface ConsoleCommandNode : ConsoleCommand { return handler.handle(console, params.drop(1)) } - override fun autocomplete(console: DevConsolePopup, params: List): String? { - if (params.isEmpty()) return null - val firstParam = params[0] + override fun autocomplete(console: DevConsolePopup, params: List): String { + val firstParam = params.firstOrNull().orEmpty() if (firstParam in subcommands) return subcommands[firstParam]!!.autocomplete(console, params.drop(1)) return getAutocompleteString(firstParam, subcommands.keys, console) } diff --git a/core/src/com/unciv/ui/screens/devconsole/DevConsolePopup.kt b/core/src/com/unciv/ui/screens/devconsole/DevConsolePopup.kt index 2699782d20..ab29a7a142 100644 --- a/core/src/com/unciv/ui/screens/devconsole/DevConsolePopup.kt +++ b/core/src/com/unciv/ui/screens/devconsole/DevConsolePopup.kt @@ -20,7 +20,7 @@ class DevConsolePopup(val screen: WorldScreen) : Popup(screen) { private var currentHistoryEntry = history.size private val textField = TextField("", BaseScreen.skin) - private val responseLabel = "".toLabel(Color.RED) + private val responseLabel = "".toLabel(Color.RED).apply { wrap = true } private val commandRoot = ConsoleCommandRoot() internal val gameInfo = screen.gameInfo @@ -32,7 +32,7 @@ class DevConsolePopup(val screen: WorldScreen) : Popup(screen) { // Without this, console popup will always contain a ` textField.addAction(Actions.delay(0.05f, Actions.run { textField.text = "" })) - add(responseLabel) + add(responseLabel).maxWidth(screen.stage.width * 0.8f) open(true) @@ -89,7 +89,7 @@ class DevConsolePopup(val screen: WorldScreen) : Popup(screen) { private fun getAutocomplete(): String { val params = getParams(textField.text) - return commandRoot.autocomplete(this, params).orEmpty() + return commandRoot.autocomplete(this, params) } internal fun getCivByName(name: String) = gameInfo.civilizations.firstOrNull { it.civName.toCliInput() == name.toCliInput() }