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
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.logic.civilization.Civilization
import com.unciv.models.ruleset.tile.TerrainType 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 { interface ConsoleCommand {
fun handle(console: DevConsolePopup, params: List<String>): String? fun handle(console: DevConsolePopup, params: List<String>): DevConsoleResponse
fun autocomplete(params: List<String>): String? = "" fun autocomplete(params: List<String>): String? = ""
} }
class ConsoleAction(val action: (console: DevConsolePopup, params: List<String>)->String?):ConsoleCommand{ class ConsoleAction(val action: (console: DevConsolePopup, params: List<String>) -> DevConsoleResponse) : ConsoleCommand {
override fun handle(console: DevConsolePopup, params: List<String>): String? { override fun handle(console: DevConsolePopup, params: List<String>): DevConsoleResponse {
return action(console, params) return action(console, params)
} }
} }
interface ConsoleCommandNode:ConsoleCommand{ interface ConsoleCommandNode : ConsoleCommand {
val subcommands: HashMap<String, ConsoleCommand> val subcommands: HashMap<String, ConsoleCommand>
override fun handle(console: DevConsolePopup, params: List<String>): String? { override fun handle(console: DevConsolePopup, params: List<String>): DevConsoleResponse {
if (params.isEmpty()) return "Available commands: " + subcommands.keys.joinToString() if (params.isEmpty())
val handler = subcommands[params[0]] ?: return "Invalid command.\nAvailable commands: " + subcommands.keys.joinToString("") { "\n- $it" } 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)) 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>( override val subcommands = hashMapOf<String, ConsoleCommand>(
"unit" to ConsoleUnitCommands(), "unit" to ConsoleUnitCommands(),
"city" to ConsoleCityCommands(), "city" to ConsoleCityCommands(),
"tile" to ConsoleTileCommands() "tile" to ConsoleTileCommands(),
"civ" to ConsoleCivCommands()
) )
} }
class ConsoleUnitCommands:ConsoleCommandNode { class ConsoleUnitCommands : ConsoleCommandNode {
override val subcommands = hashMapOf<String, ConsoleCommand>( override val subcommands = hashMapOf<String, ConsoleCommand>(
"add" to ConsoleAction { console, params -> "add" to ConsoleAction { console, params ->
if (params.size != 2) 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 val selectedTile = console.screen.mapHolder.selectedTile
?: return@ConsoleAction "No tile selected" ?: return@ConsoleAction DevConsoleResponse.error("No tile selected")
val civ = console.getCivByName(params[0]) 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] } 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) civ.units.placeUnitNearTile(selectedTile.position, baseUnit)
return@ConsoleAction null return@ConsoleAction DevConsoleResponse.OK
}, },
"remove" to ConsoleAction { console, params -> "remove" to ConsoleAction { console, params ->
if (params.isNotEmpty())
return@ConsoleAction DevConsoleResponse.hint("Format: unit remove")
val unit = console.getSelectedUnit() val unit = console.getSelectedUnit()
?: return@ConsoleAction "Select tile with unit" ?: return@ConsoleAction DevConsoleResponse.error("Select tile with unit")
unit.destroy() unit.destroy()
return@ConsoleAction null return@ConsoleAction DevConsoleResponse.OK
}, },
"addpromotion" to ConsoleAction { console, params -> "addpromotion" to ConsoleAction { console, params ->
if (params.size != 1) if (params.size != 1)
return@ConsoleAction "Format: unit addpromotion <promotionName>" return@ConsoleAction DevConsoleResponse.hint("Format: unit addpromotion <promotionName>")
val unit = console.getSelectedUnit() val unit = console.getSelectedUnit()
?: return@ConsoleAction "Select tile with unit" ?: return@ConsoleAction DevConsoleResponse.error("Select tile with unit")
val promotion = console.gameInfo.ruleset.unitPromotions.values.firstOrNull { it.name.toCliInput() == params[2] } val promotion = console.gameInfo.ruleset.unitPromotions.values.firstOrNull { it.name.toCliInput() == params[0] }
?: return@ConsoleAction "Unknown promotion" ?: return@ConsoleAction DevConsoleResponse.error("Unknown promotion")
unit.promotions.addPromotion(promotion.name, true) unit.promotions.addPromotion(promotion.name, true)
return@ConsoleAction null return@ConsoleAction DevConsoleResponse.OK
}, },
"removepromotion" to ConsoleAction { console, params -> "removepromotion" to ConsoleAction { console, params ->
if (params.size != 1) if (params.size != 1)
return@ConsoleAction "Format: unit removepromotion <promotionName>" return@ConsoleAction DevConsoleResponse.hint("Format: unit removepromotion <promotionName>")
val unit = console.getSelectedUnit() val unit = console.getSelectedUnit()
?: return@ConsoleAction "Select tile with unit" ?: return@ConsoleAction DevConsoleResponse.error("Select tile with unit")
val promotion = unit.promotions.getPromotions().firstOrNull { it.name.toCliInput() == params[2] } val promotion = unit.promotions.getPromotions().firstOrNull { it.name.toCliInput() == params[0] }
?: return@ConsoleAction "Promotion not found on unit" ?: return@ConsoleAction DevConsoleResponse.error("Promotion not found on unit")
// No such action in-game so we need to manually update // No such action in-game so we need to manually update
unit.promotions.promotions.remove(promotion.name) unit.promotions.promotions.remove(promotion.name)
unit.updateUniques() unit.updateUniques()
unit.updateVisibleTiles() unit.updateVisibleTiles()
return@ConsoleAction null return@ConsoleAction DevConsoleResponse.OK
} }
) )
} }
class ConsoleCityCommands:ConsoleCommandNode { class ConsoleCityCommands : ConsoleCommandNode {
override val subcommands = hashMapOf<String, ConsoleCommand>( override val subcommands = hashMapOf<String, ConsoleCommand>(
"add" to ConsoleAction { console, params -> "add" to ConsoleAction { console, params ->
if (params.size != 1) return@ConsoleAction "Format: city add <civName>" if (params.size != 1)
val civ = console.getCivByName(params[0]) ?: return@ConsoleAction "Unknown civ" 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 val selectedTile = console.screen.mapHolder.selectedTile
?: return@ConsoleAction "No tile selected" ?: return@ConsoleAction DevConsoleResponse.error("No tile selected")
if (selectedTile.isCityCenter()) return@ConsoleAction "Tile already contains a city center" if (selectedTile.isCityCenter())
return@ConsoleAction DevConsoleResponse.error("Tile already contains a city center")
civ.addCity(selectedTile.position) civ.addCity(selectedTile.position)
return@ConsoleAction null return@ConsoleAction DevConsoleResponse.OK
}, },
"remove" to ConsoleAction { console, params -> "remove" to ConsoleAction { console, params ->
if (params.isNotEmpty())
return@ConsoleAction DevConsoleResponse.hint("Format: city remove")
val selectedTile = console.screen.mapHolder.selectedTile val selectedTile = console.screen.mapHolder.selectedTile
?: return@ConsoleAction "No tile selected" ?: return@ConsoleAction DevConsoleResponse.error("No tile selected")
val city = selectedTile.getCity() ?: return@ConsoleAction "No city in selected tile" val city = selectedTile.getCity()
?: return@ConsoleAction DevConsoleResponse.error("No city in selected tile")
city.destroyCity(overrideSafeties = true) city.destroyCity(overrideSafeties = true)
return@ConsoleAction null return@ConsoleAction DevConsoleResponse.OK
}, },
"setpop" to ConsoleAction { console, params -> "setpop" to ConsoleAction { console, params ->
if (params.size != 2) return@ConsoleAction "Format: city setpop <cityName> <amount>" if (params.size != 2)
val newPop = params[1].toIntOrNull() ?: return@ConsoleAction "Invalid amount " + params[1] return@ConsoleAction DevConsoleResponse.hint("Format: city setpop <cityName> <amount>")
if (newPop < 1) return@ConsoleAction "Invalid amount $newPop" 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] } 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) city.population.setPopulation(newPop)
return@ConsoleAction null return@ConsoleAction DevConsoleResponse.OK
}, },
"addtile" to ConsoleAction { console, params -> "addtile" to ConsoleAction { console, params ->
if (params.size != 1)
return@ConsoleAction DevConsoleResponse.hint("Format: city addtile <cityName>")
val selectedTile = console.screen.mapHolder.selectedTile 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] } 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 }) 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) city.expansion.takeOwnership(selectedTile)
return@ConsoleAction null return@ConsoleAction DevConsoleResponse.OK
}, },
"removetile" to ConsoleAction { console, params -> "removetile" to ConsoleAction { console, params ->
if (params.isNotEmpty())
return@ConsoleAction DevConsoleResponse.hint("Format: city removetile")
val selectedTile = console.screen.mapHolder.selectedTile val selectedTile = console.screen.mapHolder.selectedTile
?: return@ConsoleAction "No tile selected" ?: return@ConsoleAction DevConsoleResponse.error("No tile selected")
val city = selectedTile.getCity() ?: return@ConsoleAction "No city for selected tile" val city = selectedTile.getCity() ?: return@ConsoleAction DevConsoleResponse.error("No city for selected tile")
city.expansion.relinquishOwnership(selectedTile) city.expansion.relinquishOwnership(selectedTile)
return@ConsoleAction null return@ConsoleAction DevConsoleResponse.OK
}) })
} }
@ -156,47 +173,85 @@ class ConsoleTileCommands: ConsoleCommandNode {
override val subcommands = hashMapOf<String, ConsoleCommand>( override val subcommands = hashMapOf<String, ConsoleCommand>(
"setimprovement" to ConsoleAction { console, params -> "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 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 { val improvement = console.gameInfo.ruleset.tileImprovements.values.firstOrNull {
it.name.toCliInput() == params[0] it.name.toCliInput() == params[0]
} ?: return@ConsoleAction "Unknown improvement" } ?: return@ConsoleAction DevConsoleResponse.error("Unknown improvement")
var civ:Civilization? = null var civ:Civilization? = null
if (params.size == 2){ 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) selectedTile.improvementFunctions.changeImprovement(improvement.name, civ)
return@ConsoleAction null return@ConsoleAction DevConsoleResponse.OK
}, },
"removeimprovement" to ConsoleAction { console, params -> "removeimprovement" to ConsoleAction { console, params ->
if (params.isNotEmpty())
return@ConsoleAction DevConsoleResponse.hint("Format: tile removeimprovement")
val selectedTile = console.screen.mapHolder.selectedTile val selectedTile = console.screen.mapHolder.selectedTile
?: return@ConsoleAction "No tile selected" ?: return@ConsoleAction DevConsoleResponse.error("No tile selected")
selectedTile.improvementFunctions.changeImprovement(null) selectedTile.improvementFunctions.changeImprovement(null)
return@ConsoleAction null return@ConsoleAction DevConsoleResponse.OK
}, },
"addfeature" to ConsoleAction { console, params -> "addfeature" to ConsoleAction { console, params ->
if (params.size != 1)
return@ConsoleAction DevConsoleResponse.hint("Format: tile addfeature <featureName>")
val selectedTile = console.screen.mapHolder.selectedTile val selectedTile = console.screen.mapHolder.selectedTile
?: return@ConsoleAction "No tile selected" ?: return@ConsoleAction DevConsoleResponse.error("No tile selected")
if (params.size != 1) return@ConsoleAction "Format: tile addfeature <featureName>"
val feature = console.gameInfo.ruleset.terrains.values val feature = console.gameInfo.ruleset.terrains.values
.firstOrNull { it.type == TerrainType.TerrainFeature && it.name.toCliInput() == params[0] } .firstOrNull { it.type == TerrainType.TerrainFeature && it.name.toCliInput() == params[0] }
?: return@ConsoleAction "Unknown feature" ?: return@ConsoleAction DevConsoleResponse.error("Unknown feature")
selectedTile.addTerrainFeature(feature.name) selectedTile.addTerrainFeature(feature.name)
return@ConsoleAction null return@ConsoleAction DevConsoleResponse.OK
}, },
"removefeature" to ConsoleAction { console, params -> "removefeature" to ConsoleAction { console, params ->
if (params.size != 1)
return@ConsoleAction DevConsoleResponse.hint("Format: tile addfeature <featureName>")
val selectedTile = console.screen.mapHolder.selectedTile val selectedTile = console.screen.mapHolder.selectedTile
?: return@ConsoleAction "No tile selected" ?: return@ConsoleAction DevConsoleResponse.error("No tile selected")
if (params.size != 1) return@ConsoleAction "Format: tile addfeature <featureName>"
val feature = console.gameInfo.ruleset.terrains.values val feature = console.gameInfo.ruleset.terrains.values
.firstOrNull { it.type == TerrainType.TerrainFeature && it.name.toCliInput() == params[0] } .firstOrNull { it.type == TerrainType.TerrainFeature && it.name.toCliInput() == params[0] }
?: return@ConsoleAction "Unknown feature" ?: return@ConsoleAction DevConsoleResponse.error("Unknown feature")
selectedTile.removeTerrainFeature(feature.name) 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 { companion object {
val history = ArrayList<String>() 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 internal val gameInfo = screen.gameInfo
init { init {
add(textField).width(stageToShowOn.width / 2).row() add(textField).width(stageToShowOn.width / 2).row()
val label = "".toLabel(Color.RED) textField.keyShortcuts.add(Input.Keys.ENTER, ::onEnter)
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 ` // Without this, console popup will always contain a `
textField.addAction(Actions.delay(0.05f, Actions.run { textField.text = "" })) textField.addAction(Actions.delay(0.05f, Actions.run { textField.text = "" }))
add(responseLabel)
open(true) open(true)
keyShortcuts.add(KeyCharAndCode.ESC) { close() } keyShortcuts.add(KeyCharAndCode.ESC) { close() }
keyShortcuts.add(KeyCharAndCode.TAB) { keyShortcuts.add(KeyCharAndCode.TAB) {
@ -45,7 +44,6 @@ class DevConsolePopup(val screen: WorldScreen) : Popup(screen) {
} }
if (history.isNotEmpty()) { if (history.isNotEmpty()) {
var currentHistoryEntry = history.size
keyShortcuts.add(Input.Keys.UP) { keyShortcuts.add(Input.Keys.UP) {
if (currentHistoryEntry > 0) currentHistoryEntry-- if (currentHistoryEntry > 0) currentHistoryEntry--
textField.text = history[currentHistoryEntry] textField.text = history[currentHistoryEntry]
@ -58,21 +56,35 @@ class DevConsolePopup(val screen: WorldScreen) : Popup(screen) {
textField.cursorPosition = textField.text.length 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) 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) 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? { internal fun getSelectedUnit(): MapUnit? {
val selectedTile = screen.mapHolder.selectedTile ?: return null 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 // No cheating unless you're by yourself
if (gameInfo.civilizations.count { it.isHuman() } > 1) return@add if (gameInfo.civilizations.count { it.isHuman() } > 1) return@add
val consolePopup = DevConsolePopup(this) val consolePopup = DevConsolePopup(this)
stage.keyboardFocus = consolePopup.textField
} }
addKeyboardListener() // for map panning by W,S,A,D addKeyboardListener() // for map panning by W,S,A,D