diff --git a/core/src/com/unciv/ui/screens/devconsole/DevConsoleCommand.kt b/core/src/com/unciv/ui/screens/devconsole/DevConsoleCommand.kt index 88c0db94ae..e913cf517c 100644 --- a/core/src/com/unciv/ui/screens/devconsole/DevConsoleCommand.kt +++ b/core/src/com/unciv/ui/screens/devconsole/DevConsoleCommand.kt @@ -2,26 +2,29 @@ package com.unciv.ui.screens.devconsole import com.unciv.logic.civilization.Civilization import com.unciv.models.ruleset.tile.TerrainType +import com.unciv.models.stats.Stat -fun String.toCliInput() = this.lowercase().replace(" ","-") +internal fun String.toCliInput() = this.lowercase().replace(" ","-") interface ConsoleCommand { - fun handle(console: DevConsolePopup, params: List): String? + fun handle(console: DevConsolePopup, params: List): DevConsoleResponse fun autocomplete(params: List): String? = "" } -class ConsoleAction(val action: (console: DevConsolePopup, params: List)->String?):ConsoleCommand{ - override fun handle(console: DevConsolePopup, params: List): String? { +class ConsoleAction(val action: (console: DevConsolePopup, params: List) -> DevConsoleResponse) : ConsoleCommand { + override fun handle(console: DevConsolePopup, params: List): DevConsoleResponse { return action(console, params) } } -interface ConsoleCommandNode:ConsoleCommand{ +interface ConsoleCommandNode : ConsoleCommand { val subcommands: HashMap - override fun handle(console: DevConsolePopup, params: List): String? { - if (params.isEmpty()) return "Available commands: " + subcommands.keys.joinToString() - val handler = subcommands[params[0]] ?: return "Invalid command.\nAvailable commands: " + subcommands.keys.joinToString("") { "\n- $it" } + override fun handle(console: DevConsolePopup, params: List): DevConsoleResponse { + if (params.isEmpty()) + return DevConsoleResponse.hint("Available commands: " + subcommands.keys.joinToString()) + val handler = subcommands[params[0]] + ?: return DevConsoleResponse.error("Invalid command.\nAvailable commands:" + subcommands.keys.joinToString("") { "\n- $it" }) return handler.handle(console, params.drop(1)) } @@ -43,112 +46,126 @@ interface ConsoleCommandNode:ConsoleCommand{ } } -class ConsoleCommandRoot:ConsoleCommandNode { +class ConsoleCommandRoot : ConsoleCommandNode { override val subcommands = hashMapOf( "unit" to ConsoleUnitCommands(), "city" to ConsoleCityCommands(), - "tile" to ConsoleTileCommands() + "tile" to ConsoleTileCommands(), + "civ" to ConsoleCivCommands() ) } -class ConsoleUnitCommands:ConsoleCommandNode { +class ConsoleUnitCommands : ConsoleCommandNode { override val subcommands = hashMapOf( "add" to ConsoleAction { console, params -> if (params.size != 2) - return@ConsoleAction "Format: unit add " + return@ConsoleAction DevConsoleResponse.hint("Format: unit add ") val selectedTile = console.screen.mapHolder.selectedTile - ?: return@ConsoleAction "No tile selected" + ?: return@ConsoleAction DevConsoleResponse.error("No tile selected") val civ = console.getCivByName(params[0]) - ?: return@ConsoleAction "Unknown civ" + ?: return@ConsoleAction DevConsoleResponse.error("Unknown civ") val baseUnit = console.gameInfo.ruleset.units.values.firstOrNull { it.name.toCliInput() == params[1] } - ?: return@ConsoleAction "Unknown unit" + ?: return@ConsoleAction DevConsoleResponse.error("Unknown unit") civ.units.placeUnitNearTile(selectedTile.position, baseUnit) - return@ConsoleAction null + return@ConsoleAction DevConsoleResponse.OK }, "remove" to ConsoleAction { console, params -> + if (params.isNotEmpty()) + return@ConsoleAction DevConsoleResponse.hint("Format: unit remove") val unit = console.getSelectedUnit() - ?: return@ConsoleAction "Select tile with unit" + ?: return@ConsoleAction DevConsoleResponse.error("Select tile with unit") unit.destroy() - return@ConsoleAction null + return@ConsoleAction DevConsoleResponse.OK }, "addpromotion" to ConsoleAction { console, params -> if (params.size != 1) - return@ConsoleAction "Format: unit addpromotion " + return@ConsoleAction DevConsoleResponse.hint("Format: unit addpromotion ") val unit = console.getSelectedUnit() - ?: return@ConsoleAction "Select tile with unit" - val promotion = console.gameInfo.ruleset.unitPromotions.values.firstOrNull { it.name.toCliInput() == params[2] } - ?: return@ConsoleAction "Unknown promotion" + ?: return@ConsoleAction DevConsoleResponse.error("Select tile with unit") + val promotion = console.gameInfo.ruleset.unitPromotions.values.firstOrNull { it.name.toCliInput() == params[0] } + ?: return@ConsoleAction DevConsoleResponse.error("Unknown promotion") unit.promotions.addPromotion(promotion.name, true) - return@ConsoleAction null + return@ConsoleAction DevConsoleResponse.OK }, "removepromotion" to ConsoleAction { console, params -> if (params.size != 1) - return@ConsoleAction "Format: unit removepromotion " + return@ConsoleAction DevConsoleResponse.hint("Format: unit removepromotion ") val unit = console.getSelectedUnit() - ?: return@ConsoleAction "Select tile with unit" - val promotion = unit.promotions.getPromotions().firstOrNull { it.name.toCliInput() == params[2] } - ?: return@ConsoleAction "Promotion not found on unit" + ?: return@ConsoleAction DevConsoleResponse.error("Select tile with unit") + val promotion = unit.promotions.getPromotions().firstOrNull { it.name.toCliInput() == params[0] } + ?: return@ConsoleAction DevConsoleResponse.error("Promotion not found on unit") // No such action in-game so we need to manually update unit.promotions.promotions.remove(promotion.name) unit.updateUniques() unit.updateVisibleTiles() - return@ConsoleAction null + return@ConsoleAction DevConsoleResponse.OK } ) } -class ConsoleCityCommands:ConsoleCommandNode { +class ConsoleCityCommands : ConsoleCommandNode { override val subcommands = hashMapOf( "add" to ConsoleAction { console, params -> - if (params.size != 1) return@ConsoleAction "Format: city add " - val civ = console.getCivByName(params[0]) ?: return@ConsoleAction "Unknown civ" + if (params.size != 1) + return@ConsoleAction DevConsoleResponse.hint("Format: city add ") + val civ = console.getCivByName(params[0]) + ?: return@ConsoleAction DevConsoleResponse.error("Unknown civ") val selectedTile = console.screen.mapHolder.selectedTile - ?: return@ConsoleAction "No tile selected" - if (selectedTile.isCityCenter()) return@ConsoleAction "Tile already contains a city center" + ?: return@ConsoleAction DevConsoleResponse.error("No tile selected") + if (selectedTile.isCityCenter()) + return@ConsoleAction DevConsoleResponse.error("Tile already contains a city center") civ.addCity(selectedTile.position) - return@ConsoleAction null + return@ConsoleAction DevConsoleResponse.OK }, "remove" to ConsoleAction { console, params -> + if (params.isNotEmpty()) + return@ConsoleAction DevConsoleResponse.hint("Format: city remove") val selectedTile = console.screen.mapHolder.selectedTile - ?: return@ConsoleAction "No tile selected" - val city = selectedTile.getCity() ?: return@ConsoleAction "No city in selected tile" + ?: return@ConsoleAction DevConsoleResponse.error("No tile selected") + val city = selectedTile.getCity() + ?: return@ConsoleAction DevConsoleResponse.error("No city in selected tile") city.destroyCity(overrideSafeties = true) - return@ConsoleAction null + return@ConsoleAction DevConsoleResponse.OK }, "setpop" to ConsoleAction { console, params -> - if (params.size != 2) return@ConsoleAction "Format: city setpop " - val newPop = params[1].toIntOrNull() ?: return@ConsoleAction "Invalid amount " + params[1] - if (newPop < 1) return@ConsoleAction "Invalid amount $newPop" + if (params.size != 2) + return@ConsoleAction DevConsoleResponse.hint("Format: city setpop ") + val newPop = params[1].toIntOrNull() ?: return@ConsoleAction DevConsoleResponse.error("Invalid amount " + params[1]) + if (newPop < 1) return@ConsoleAction DevConsoleResponse.error("Invalid amount $newPop") val city = console.gameInfo.getCities().firstOrNull { it.name.toCliInput() == params[0] } - ?: return@ConsoleAction "Unknown city" + ?: return@ConsoleAction DevConsoleResponse.error("Unknown city") city.population.setPopulation(newPop) - return@ConsoleAction null + return@ConsoleAction DevConsoleResponse.OK }, "addtile" to ConsoleAction { console, params -> + if (params.size != 1) + return@ConsoleAction DevConsoleResponse.hint("Format: city addtile ") val selectedTile = console.screen.mapHolder.selectedTile - ?: return@ConsoleAction "No tile selected" + ?: return@ConsoleAction DevConsoleResponse.error("No tile selected") val city = console.gameInfo.getCities().firstOrNull { it.name.toCliInput() == params[0] } - ?: return@ConsoleAction "Unknown city" + ?: return@ConsoleAction DevConsoleResponse.error("Unknown city") if (selectedTile.neighbors.none { it.getCity() == city }) - return@ConsoleAction "Tile is not adjacent to city" + return@ConsoleAction DevConsoleResponse.error("Tile is not adjacent to any tile already owned by the city") city.expansion.takeOwnership(selectedTile) - return@ConsoleAction null + return@ConsoleAction DevConsoleResponse.OK }, "removetile" to ConsoleAction { console, params -> + if (params.isNotEmpty()) + return@ConsoleAction DevConsoleResponse.hint("Format: city removetile") val selectedTile = console.screen.mapHolder.selectedTile - ?: return@ConsoleAction "No tile selected" - val city = selectedTile.getCity() ?: return@ConsoleAction "No city for selected tile" + ?: return@ConsoleAction DevConsoleResponse.error("No tile selected") + val city = selectedTile.getCity() ?: return@ConsoleAction DevConsoleResponse.error("No city for selected tile") city.expansion.relinquishOwnership(selectedTile) - return@ConsoleAction null + return@ConsoleAction DevConsoleResponse.OK }) } @@ -156,47 +173,85 @@ class ConsoleTileCommands: ConsoleCommandNode { override val subcommands = hashMapOf( "setimprovement" to ConsoleAction { console, params -> - if (params.size != 1 && params.size != 2) return@ConsoleAction "Format: tile setimprovement []" + if (params.size != 1 && params.size != 2) + return@ConsoleAction DevConsoleResponse.hint("Format: tile setimprovement []") val selectedTile = console.screen.mapHolder.selectedTile - ?: return@ConsoleAction "No tile selected" + ?: return@ConsoleAction DevConsoleResponse.error("No tile selected") val improvement = console.gameInfo.ruleset.tileImprovements.values.firstOrNull { it.name.toCliInput() == params[0] - } ?: return@ConsoleAction "Unknown improvement" + } ?: return@ConsoleAction DevConsoleResponse.error("Unknown improvement") var civ:Civilization? = null if (params.size == 2){ - civ = console.getCivByName(params[1]) ?: return@ConsoleAction "Unknown civ" + civ = console.getCivByName(params[1]) + ?: return@ConsoleAction DevConsoleResponse.error("Unknown civ") } selectedTile.improvementFunctions.changeImprovement(improvement.name, civ) - return@ConsoleAction null + return@ConsoleAction DevConsoleResponse.OK }, "removeimprovement" to ConsoleAction { console, params -> + if (params.isNotEmpty()) + return@ConsoleAction DevConsoleResponse.hint("Format: tile removeimprovement") val selectedTile = console.screen.mapHolder.selectedTile - ?: return@ConsoleAction "No tile selected" + ?: return@ConsoleAction DevConsoleResponse.error("No tile selected") selectedTile.improvementFunctions.changeImprovement(null) - return@ConsoleAction null + return@ConsoleAction DevConsoleResponse.OK }, "addfeature" to ConsoleAction { console, params -> + if (params.size != 1) + return@ConsoleAction DevConsoleResponse.hint("Format: tile addfeature ") val selectedTile = console.screen.mapHolder.selectedTile - ?: return@ConsoleAction "No tile selected" - if (params.size != 1) return@ConsoleAction "Format: tile addfeature " + ?: return@ConsoleAction DevConsoleResponse.error("No tile selected") val feature = console.gameInfo.ruleset.terrains.values .firstOrNull { it.type == TerrainType.TerrainFeature && it.name.toCliInput() == params[0] } - ?: return@ConsoleAction "Unknown feature" + ?: return@ConsoleAction DevConsoleResponse.error("Unknown feature") selectedTile.addTerrainFeature(feature.name) - return@ConsoleAction null + return@ConsoleAction DevConsoleResponse.OK }, "removefeature" to ConsoleAction { console, params -> + if (params.size != 1) + return@ConsoleAction DevConsoleResponse.hint("Format: tile addfeature ") val selectedTile = console.screen.mapHolder.selectedTile - ?: return@ConsoleAction "No tile selected" - if (params.size != 1) return@ConsoleAction "Format: tile addfeature " + ?: return@ConsoleAction DevConsoleResponse.error("No tile selected") val feature = console.gameInfo.ruleset.terrains.values .firstOrNull { it.type == TerrainType.TerrainFeature && it.name.toCliInput() == params[0] } - ?: return@ConsoleAction "Unknown feature" + ?: return@ConsoleAction DevConsoleResponse.error("Unknown feature") selectedTile.removeTerrainFeature(feature.name) - return@ConsoleAction null + return@ConsoleAction DevConsoleResponse.OK } ) } + +class ConsoleCivCommands : ConsoleCommandNode { + override val subcommands = hashMapOf( + "add" to ConsoleAction { console, params -> + var statPos = 0 + if (params.size !in 2..3) + return@ConsoleAction DevConsoleResponse.hint("Format: civ add [civ] ") + val civ = if (params.size == 2) console.screen.selectedCiv + else { + statPos++ + console.getCivByName(params[0]) + ?: return@ConsoleAction DevConsoleResponse.error("Unknown civ") + } + val amount = params[statPos+1].toIntOrNull() + ?: return@ConsoleAction DevConsoleResponse.error("Whut? \"${params[statPos+1]}\" is not a number!") + val stat = Stat.safeValueOf(params[statPos].replaceFirstChar(Char::titlecase)) + ?: return@ConsoleAction DevConsoleResponse.error("Whut? \"${params[statPos]}\" is not a Stat!") + if (stat !in Stat.statsWithCivWideField) + return@ConsoleAction DevConsoleResponse.error("$stat is not civ-wide") + civ.addStat(stat, amount) + DevConsoleResponse.OK + } + ) + + override fun autocomplete(params: List): String? { + if (params.size == 2 && params[0] == "add") + return Stat.names() + .firstOrNull { it.lowercase().startsWith(params[1]) } + ?.drop(params[1].length) + return super.autocomplete(params) + } +} diff --git a/core/src/com/unciv/ui/screens/devconsole/DevConsolePopup.kt b/core/src/com/unciv/ui/screens/devconsole/DevConsolePopup.kt index 05e0f973d4..ba2e056971 100644 --- a/core/src/com/unciv/ui/screens/devconsole/DevConsolePopup.kt +++ b/core/src/com/unciv/ui/screens/devconsole/DevConsolePopup.kt @@ -17,26 +17,25 @@ class DevConsolePopup(val screen: WorldScreen) : Popup(screen) { companion object { val history = ArrayList() } + private var currentHistoryEntry = history.size - val textField = TextField("", BaseScreen.skin) + private val textField = TextField("", BaseScreen.skin) + private val responseLabel = "".toLabel(Color.RED) + + private val commandRoot = ConsoleCommandRoot() internal val gameInfo = screen.gameInfo init { add(textField).width(stageToShowOn.width / 2).row() - val label = "".toLabel(Color.RED) - add(label) - textField.keyShortcuts.add(Input.Keys.ENTER) { - val handleCommandResponse = handleCommand() - if (handleCommandResponse == null) { - screen.shouldUpdate = true - history.add(textField.text) - close() - } - else label.setText(handleCommandResponse) - } + textField.keyShortcuts.add(Input.Keys.ENTER, ::onEnter) + // Without this, console popup will always contain a ` textField.addAction(Actions.delay(0.05f, Actions.run { textField.text = "" })) + + add(responseLabel) + open(true) + keyShortcuts.add(KeyCharAndCode.ESC) { close() } keyShortcuts.add(KeyCharAndCode.TAB) { @@ -45,7 +44,6 @@ class DevConsolePopup(val screen: WorldScreen) : Popup(screen) { } if (history.isNotEmpty()) { - var currentHistoryEntry = history.size keyShortcuts.add(Input.Keys.UP) { if (currentHistoryEntry > 0) currentHistoryEntry-- textField.text = history[currentHistoryEntry] @@ -58,21 +56,35 @@ class DevConsolePopup(val screen: WorldScreen) : Popup(screen) { textField.cursorPosition = textField.text.length } } + + screen.stage.keyboardFocus = textField } - private fun getParams(text:String) = text.split(" ").filter { it.isNotEmpty() }.map { it.lowercase() } + private fun onEnter() { + val handleCommandResponse = handleCommand() + if (handleCommandResponse.isOK) { + screen.shouldUpdate = true + history.add(textField.text) + close() + return + } + responseLabel.setText(handleCommandResponse.message) + responseLabel.style.fontColor = handleCommandResponse.color + } - private fun handleCommand(): String? { + private fun getParams(text: String) = text.split(" ").filter { it.isNotEmpty() }.map { it.lowercase() } + + private fun handleCommand(): DevConsoleResponse { val params = getParams(textField.text) - return ConsoleCommandRoot().handle(this, params) + return commandRoot.handle(this, params) } - private fun getAutocomplete():String { + private fun getAutocomplete(): String { val params = getParams(textField.text) - return ConsoleCommandRoot().autocomplete(params) ?: "" + return commandRoot.autocomplete(params) ?: "" } - internal fun getCivByName(name:String) = gameInfo.civilizations.firstOrNull { it.civName.toCliInput() == name } + internal fun getCivByName(name: String) = gameInfo.civilizations.firstOrNull { it.civName.toCliInput() == name } internal fun getSelectedUnit(): MapUnit? { val selectedTile = screen.mapHolder.selectedTile ?: return null diff --git a/core/src/com/unciv/ui/screens/devconsole/DevConsoleResponse.kt b/core/src/com/unciv/ui/screens/devconsole/DevConsoleResponse.kt new file mode 100644 index 0000000000..efd9fb19df --- /dev/null +++ b/core/src/com/unciv/ui/screens/devconsole/DevConsoleResponse.kt @@ -0,0 +1,18 @@ + +package com.unciv.ui.screens.devconsole + +import com.badlogic.gdx.graphics.Color + +@Suppress("DataClassPrivateConstructor") // abuser need to find copy() first +data class DevConsoleResponse private constructor ( + val color: Color, + val message: String? = null, + val isOK: Boolean = false +) { + companion object { + val OK = DevConsoleResponse(Color.GREEN, isOK = true) + fun ok(message: String) = DevConsoleResponse(Color.GREEN, message, true) + fun error(message: String) = DevConsoleResponse(Color.RED, message) + fun hint(message: String) = DevConsoleResponse(Color.GOLD, message) + } +} diff --git a/core/src/com/unciv/ui/screens/worldscreen/WorldScreen.kt b/core/src/com/unciv/ui/screens/worldscreen/WorldScreen.kt index 0b9a3c0d66..0b5c38dfff 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/WorldScreen.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/WorldScreen.kt @@ -196,7 +196,6 @@ class WorldScreen( // No cheating unless you're by yourself if (gameInfo.civilizations.count { it.isHuman() } > 1) return@add val consolePopup = DevConsolePopup(this) - stage.keyboardFocus = consolePopup.textField } addKeyboardListener() // for map panning by W,S,A,D