Dev Console: Lint, crash fix, add Stat (#10656)

* Console: linting

* Console: Commands can control message Color

* Console: `civ add gold -1`

* Console: Typo
This commit is contained in:
SomeTroglodyte 2023-12-03 21:12:44 +01:00 committed by GitHub
parent 5061b29197
commit 96dcd584cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 168 additions and 84 deletions

View File

@ -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>): String?
fun handle(console: DevConsolePopup, params: List<String>): DevConsoleResponse
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? {
class ConsoleAction(val action: (console: DevConsolePopup, params: List<String>) -> DevConsoleResponse) : ConsoleCommand {
override fun handle(console: DevConsolePopup, params: List<String>): DevConsoleResponse {
return action(console, params)
}
}
interface ConsoleCommandNode:ConsoleCommand{
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.\nAvailable commands: " + subcommands.keys.joinToString("") { "\n- $it" }
override fun handle(console: DevConsolePopup, params: List<String>): 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<String, ConsoleCommand>(
"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<String, ConsoleCommand>(
"add" to ConsoleAction { console, params ->
if (params.size != 2)
return@ConsoleAction "Format: unit add <civName> <unitName>"
return@ConsoleAction DevConsoleResponse.hint("Format: unit add <civName> <unitName>")
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 <promotionName>"
return@ConsoleAction DevConsoleResponse.hint("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"
?: 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 <promotionName>"
return@ConsoleAction DevConsoleResponse.hint("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"
?: 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<String, ConsoleCommand>(
"add" to ConsoleAction { console, params ->
if (params.size != 1) return@ConsoleAction "Format: city add <civName>"
val civ = console.getCivByName(params[0]) ?: return@ConsoleAction "Unknown civ"
if (params.size != 1)
return@ConsoleAction DevConsoleResponse.hint("Format: city add <civName>")
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 <cityName> <amount>"
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 <cityName> <amount>")
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 <cityName>")
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<String, ConsoleCommand>(
"setimprovement" to ConsoleAction { console, params ->
if (params.size != 1 && params.size != 2) return@ConsoleAction "Format: tile setimprovement <improvementName> [<civName>]"
if (params.size != 1 && params.size != 2)
return@ConsoleAction DevConsoleResponse.hint("Format: tile setimprovement <improvementName> [<civName>]")
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 <featureName>")
val selectedTile = console.screen.mapHolder.selectedTile
?: return@ConsoleAction "No tile selected"
if (params.size != 1) return@ConsoleAction "Format: tile addfeature <featureName>"
?: 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 <featureName>")
val selectedTile = console.screen.mapHolder.selectedTile
?: return@ConsoleAction "No tile selected"
if (params.size != 1) return@ConsoleAction "Format: tile addfeature <featureName>"
?: 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<String, ConsoleCommand>(
"add" to ConsoleAction { console, params ->
var statPos = 0
if (params.size !in 2..3)
return@ConsoleAction DevConsoleResponse.hint("Format: civ add [civ] <stat> <amount>")
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>): 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)
}
}

View File

@ -17,26 +17,25 @@ class DevConsolePopup(val screen: WorldScreen) : Popup(screen) {
companion object {
val history = ArrayList<String>()
}
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

View File

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

View File

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