diff --git a/core/src/com/unciv/ui/screens/devconsole/DevConsoleCommand.kt b/core/src/com/unciv/ui/screens/devconsole/DevConsoleCommand.kt new file mode 100644 index 0000000000..59f62b575c --- /dev/null +++ b/core/src/com/unciv/ui/screens/devconsole/DevConsoleCommand.kt @@ -0,0 +1,130 @@ +package com.unciv.ui.screens.devconsole + +fun String.toCliInput() = this.lowercase().replace(" ","-") + +interface ConsoleCommand { + fun handle(console: DevConsolePopup, params: List): String? + fun autocomplete(params: List): String? = "" +} + +class ConsoleAction(val action: (console: DevConsolePopup, params: List)->String?):ConsoleCommand{ + override fun handle(console: DevConsolePopup, params: List): String? { + return action(console, params) + } +} + +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" + return handler.handle(console, params.drop(1)) + } + + override fun autocomplete(params: List): String? { + if (params.isEmpty()) return null + val firstParam = params[0] + if (firstParam in subcommands) return subcommands[firstParam]!!.autocomplete(params.drop(1)) + val possibleSubcommands = subcommands.keys.filter { it.startsWith(firstParam) } + if (possibleSubcommands.isEmpty()) return null + if (possibleSubcommands.size == 1) return possibleSubcommands.first().removePrefix(firstParam) + + val firstSubcommand = possibleSubcommands.first() + for ((index, char) in firstSubcommand.withIndex()){ + if (possibleSubcommands.any { it.lastIndex < index } || + possibleSubcommands.any { it[index] != char }) + return firstSubcommand.substring(0,index).removePrefix(firstParam) + } + return firstSubcommand.removePrefix(firstParam) + } +} + +class ConsoleCommandRoot:ConsoleCommandNode{ + override val subcommands = hashMapOf( + "unit" to ConsoleUnitCommands(), + "city" to ConsoleCityCommands() + ) +} + +class ConsoleUnitCommands:ConsoleCommandNode { + override val subcommands = hashMapOf( + "add" to ConsoleAction { console, params -> + if (params.size != 2) + return@ConsoleAction "Format: unit add " + val selectedTile = console.screen.mapHolder.selectedTile + ?: return@ConsoleAction "No tile selected" + val civ = console.getCivByName(params[0]) + ?: return@ConsoleAction "Unknown civ" + val baseUnit = console.gameInfo.ruleset.units.values.firstOrNull { it.name.toCliInput() == params[3] } + ?: return@ConsoleAction "Unknown unit" + civ.units.placeUnitNearTile(selectedTile.position, baseUnit) + return@ConsoleAction null + }, + + "remove" to ConsoleAction { console, params -> + val unit = console.getSelectedUnit() + ?: return@ConsoleAction "Select tile with unit" + unit.destroy() + return@ConsoleAction null + }, + + "addpromotion" to ConsoleAction { console, params -> + if (params.size != 1) + return@ConsoleAction "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" + unit.promotions.addPromotion(promotion.name, true) + return@ConsoleAction null + }, + + "removepromotion" to ConsoleAction { console, params -> + if (params.size != 1) + return@ConsoleAction "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" + // No such action in-game so we need to manually update + unit.promotions.promotions.remove(promotion.name) + unit.updateUniques() + unit.updateVisibleTiles() + return@ConsoleAction null + } + ) +} + +class ConsoleCityCommands:ConsoleCommandNode { + override val subcommands = hashMapOf( + "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" + val city = console.gameInfo.getCities().firstOrNull { it.name.toCliInput() == params[0] } + ?: return@ConsoleAction "Unknown city" + city.population.setPopulation(newPop) + return@ConsoleAction null + }, + + "addtile" to ConsoleAction { console, params -> + val selectedTile = console.screen.mapHolder.selectedTile + ?: return@ConsoleAction "No tile selected" + val city = console.gameInfo.getCities().firstOrNull { it.name.toCliInput() == params[0] } + ?: return@ConsoleAction "Unknown city" + if (selectedTile.neighbors.none { it.getCity() == city }) + return@ConsoleAction "Tile is not adjacent to city" + city.expansion.takeOwnership(selectedTile) + return@ConsoleAction null + }, + + "removetile" to ConsoleAction { console, params -> + val selectedTile = console.screen.mapHolder.selectedTile + ?: return@ConsoleAction "No tile selected" + val city = selectedTile.getCity() ?: return@ConsoleAction "No city for selected tile" + city.expansion.relinquishOwnership(selectedTile) + return@ConsoleAction null + }) +} diff --git a/core/src/com/unciv/ui/screens/devconsole/DevConsolePopup.kt b/core/src/com/unciv/ui/screens/devconsole/DevConsolePopup.kt new file mode 100644 index 0000000000..71021991a8 --- /dev/null +++ b/core/src/com/unciv/ui/screens/devconsole/DevConsolePopup.kt @@ -0,0 +1,87 @@ +package com.unciv.ui.screens.devconsole + +import com.badlogic.gdx.Input +import com.badlogic.gdx.graphics.Color +import com.badlogic.gdx.scenes.scene2d.actions.Actions +import com.badlogic.gdx.scenes.scene2d.ui.TextField +import com.unciv.logic.map.mapunit.MapUnit +import com.unciv.ui.components.extensions.toLabel +import com.unciv.ui.components.input.KeyCharAndCode +import com.unciv.ui.components.input.keyShortcuts +import com.unciv.ui.popups.Popup +import com.unciv.ui.screens.basescreen.BaseScreen +import com.unciv.ui.screens.worldscreen.WorldScreen + + +class DevConsolePopup(val screen: WorldScreen) : Popup(screen) { + companion object { + val history = ArrayList() + } + + val textField = TextField("", BaseScreen.skin) + 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) + } + // Without this, console popup will always contain a ` + textField.addAction(Actions.delay(0.05f, Actions.run { textField.text = "" })) + open(true) + keyShortcuts.add(KeyCharAndCode.ESC) { close() } + + keyShortcuts.add(KeyCharAndCode.TAB) { + val textToAdd = getAutocomplete() + textField.appendText(textToAdd) + } + + if (history.isNotEmpty()) { + var currentHistoryEntry = history.size + keyShortcuts.add(Input.Keys.UP) { + if (currentHistoryEntry > 0) currentHistoryEntry-- + textField.text = history[currentHistoryEntry] + textField.cursorPosition = textField.text.length + } + keyShortcuts.add(Input.Keys.DOWN) { + if (currentHistoryEntry == history.size) currentHistoryEntry-- + if (currentHistoryEntry < history.lastIndex) currentHistoryEntry++ + textField.text = history[currentHistoryEntry] + textField.cursorPosition = textField.text.length + } + } + } + + private fun getParams(text:String) = text.split(" ").filter { it.isNotEmpty() }.map { it.lowercase() } + + private fun handleCommand(): String? { + val params = getParams(textField.text) + if (params.isEmpty()) return "No command" + return ConsoleCommandRoot().handle(this, params) + } + + private fun getAutocomplete():String { + val params = getParams(textField.text) + return ConsoleCommandRoot().autocomplete(params) ?: "" + } + + internal fun getCivByName(name:String) = gameInfo.civilizations.firstOrNull { it.civName.toCliInput() == name } + + internal fun getSelectedUnit(): MapUnit? { + val selectedTile = screen.mapHolder.selectedTile ?: return null + if (selectedTile.getFirstUnit() == null) return null + val units = selectedTile.getUnits().toList() + val selectedUnit = screen.bottomUnitTable.selectedUnit + return if (selectedUnit != null && selectedUnit.getTile() == selectedTile) selectedUnit + else units.first() + } + +} diff --git a/core/src/com/unciv/ui/screens/worldscreen/WorldScreen.kt b/core/src/com/unciv/ui/screens/worldscreen/WorldScreen.kt index 9610da95b0..eadcf51d28 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/WorldScreen.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/WorldScreen.kt @@ -43,6 +43,7 @@ import com.unciv.ui.popups.hasOpenPopups import com.unciv.ui.screens.basescreen.BaseScreen import com.unciv.ui.screens.cityscreen.CityScreen import com.unciv.ui.screens.civilopediascreen.CivilopediaScreen +import com.unciv.ui.screens.devconsole.DevConsolePopup import com.unciv.ui.screens.mainmenuscreen.MainMenuScreen import com.unciv.ui.screens.newgamescreen.NewGameScreen import com.unciv.ui.screens.overviewscreen.EmpireOverviewCategories @@ -189,6 +190,14 @@ class WorldScreen( globalShortcuts.add(KeyCharAndCode.BACK) { backButtonAndESCHandler() } + + globalShortcuts.add('`'){ + // 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 addKeyboardPresses() // shortcut keys like F1