Initial scenario/dev console, with 1 command :D (#10540)

* Initial console, with 1 command :D

* add/remove city tiles

* add/remove units!

* Added command history, accessible with up and down keys

* 'add promotion' cli command

* 'remove promotion' cli command

* Enumified

* Converted enums into classes - enums are weirdly restricted in all kinds of uncomfortable ways

* Added autocomplete!
This commit is contained in:
Yair Morgenstern
2023-11-22 23:41:09 +02:00
committed by GitHub
parent 810392a55e
commit 4c8db02dfd
3 changed files with 226 additions and 0 deletions

View File

@ -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>): String?
fun autocomplete(params: List<String>): String? = ""
}
class ConsoleAction(val action: (console: DevConsolePopup, params: List<String>)->String?):ConsoleCommand{
override fun handle(console: DevConsolePopup, params: List<String>): String? {
return action(console, params)
}
}
interface ConsoleCommandNode:ConsoleCommand{
val subcommands: HashMap<String, ConsoleCommand>
override fun handle(console: DevConsolePopup, params: List<String>): 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>): 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<String, ConsoleCommand>(
"unit" to ConsoleUnitCommands(),
"city" to ConsoleCityCommands()
)
}
class ConsoleUnitCommands:ConsoleCommandNode {
override val subcommands = hashMapOf<String, ConsoleCommand>(
"add" to ConsoleAction { console, params ->
if (params.size != 2)
return@ConsoleAction "Format: unit add <civName> <unitName>"
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 <promotionName>"
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 <promotionName>"
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<String, ConsoleCommand>(
"setpop" to ConsoleAction { console, params ->
if (params.size != 2) return@ConsoleAction "Format: city setpop <cityName> <amount>"
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
})
}

View File

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

View File

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