init: 1.0

This commit is contained in:
2025-11-28 06:00:55 +09:00
commit dec60a1c46
22 changed files with 1433 additions and 0 deletions
@@ -0,0 +1,14 @@
package net.hareworks.kommand_lib;
import org.bukkit.plugin.java.JavaPlugin
public class App : JavaPlugin() {
companion object {
lateinit var instance: App
private set
}
override fun onEnable() {
instance = this
}
}
@@ -0,0 +1,129 @@
package net.hareworks.kommand_lib
import net.hareworks.kommand_lib.context.KommandContext
import net.hareworks.kommand_lib.execution.CommandTree
import net.hareworks.kommand_lib.execution.ParseMode
import net.hareworks.kommand_lib.dsl.KommandRegistry
import org.bukkit.Bukkit
import org.bukkit.command.CommandMap
import org.bukkit.command.CommandSender
import org.bukkit.command.PluginCommand
import org.bukkit.command.TabCompleter
import org.bukkit.plugin.java.JavaPlugin
/**
* Entry-point for registering commands via the Kommand DSL.
*
* Example:
* ```kotlin
* kommand(plugin) {
* command("eco", "economy") {
* description = "Economy management"
* permission = "example.eco"
*
* literal("give") {
* string("player")
* integer("amount", min = 0) {
* executes {
* val targetName = string("player")
* val amount = int("amount")
* sender.sendMessage("Giving $amount to $targetName")
* }
* }
* }
* }
* }
* ```
*/
fun kommand(plugin: JavaPlugin, block: KommandRegistry.() -> Unit): KommandLib {
val registry = KommandRegistry(plugin)
registry.block()
return registry.build()
}
/**
* Manages the lifecycle of the commands registered through the DSL.
*/
class KommandLib internal constructor(
private val plugin: JavaPlugin,
private val definitions: List<CommandDefinition>
) {
private val commandMap: CommandMap by lazy {
val field = Bukkit.getServer().javaClass.getDeclaredField("commandMap")
field.isAccessible = true
field.get(Bukkit.getServer()) as CommandMap
}
private val registered = mutableListOf<PluginCommand>()
init {
registerAll()
}
private fun registerAll() {
for (definition in definitions) {
commandMap.getCommand(definition.name)?.unregister(commandMap)
val command = newPluginCommand(definition.name)
if (definition.aliases.isNotEmpty()) command.aliases = definition.aliases
definition.description?.let { command.description = it }
definition.usage?.let { command.usage = it }
definition.permission?.let { command.permission = it }
command.setExecutor { sender, _, alias, args ->
definition.execute(plugin, sender, alias, args)
}
command.tabCompleter = TabCompleter { sender, _, alias, args ->
definition.tabComplete(plugin, sender, alias, args)
}
commandMap.register(plugin.name.lowercase(), command)
registered += command
}
}
fun unregister() {
registered.forEach { it.unregister(commandMap) }
registered.clear()
}
private fun newPluginCommand(name: String): PluginCommand {
val constructor = PluginCommand::class.java.getDeclaredConstructor(String::class.java, JavaPlugin::class.java)
constructor.isAccessible = true
return constructor.newInstance(name, plugin)
}
}
internal data class CommandDefinition(
val name: String,
val aliases: List<String>,
val description: String?,
val usage: String?,
val permission: String?,
val rootCondition: (CommandSender) -> Boolean,
val rootExecutor: (KommandContext.() -> Unit)?,
val nodes: List<net.hareworks.kommand_lib.nodes.KommandNode>
) {
private val tree = CommandTree(nodes)
fun execute(plugin: JavaPlugin, sender: CommandSender, alias: String, args: Array<String>): Boolean {
if (!rootCondition(sender)) return false
val context = KommandContext(plugin, sender, alias, args, ParseMode.EXECUTE)
if (args.isEmpty()) {
val executor = rootExecutor ?: return false
executor.invoke(context)
return true
}
return tree.execute(context)
}
fun tabComplete(plugin: JavaPlugin, sender: CommandSender, alias: String, args: Array<String>): List<String> {
if (!rootCondition(sender)) return emptyList()
if (nodes.isEmpty()) return emptyList()
val context = KommandContext(plugin, sender, alias, args, ParseMode.SUGGEST)
if (args.isEmpty()) {
return nodes
.filter { it.isVisible(sender) }
.flatMap { it.suggestions("", context) }
.distinct()
}
return tree.tabComplete(context)
}
}
@@ -0,0 +1,197 @@
package net.hareworks.kommand_lib.arguments
import net.hareworks.kommand_lib.context.KommandContext
import org.bukkit.Bukkit
import org.bukkit.Location
import org.bukkit.entity.Entity
import org.bukkit.entity.Player
sealed class ArgumentParseResult<out T> {
data class Success<T>(val value: T) : ArgumentParseResult<T>()
data class Failure(val reason: String) : ArgumentParseResult<Nothing>()
}
interface KommandArgumentType<T> {
fun parse(input: String, context: KommandContext): ArgumentParseResult<T>
fun suggestions(context: KommandContext, prefix: String): List<String> = emptyList()
}
object WordArgumentType : KommandArgumentType<String> {
override fun parse(input: String, context: KommandContext): ArgumentParseResult<String> {
if (input.isBlank()) return ArgumentParseResult.Failure("Value cannot be blank")
return ArgumentParseResult.Success(input)
}
}
class IntegerArgumentType(
private val min: Int? = null,
private val max: Int? = null
) : KommandArgumentType<Int> {
override fun parse(input: String, context: KommandContext): ArgumentParseResult<Int> {
val value = input.toIntOrNull()
?: return ArgumentParseResult.Failure("Expected integer but got '$input'")
if (min != null && value < min) {
return ArgumentParseResult.Failure("Value must be >= $min")
}
if (max != null && value > max) {
return ArgumentParseResult.Failure("Value must be <= $max")
}
return ArgumentParseResult.Success(value)
}
}
class FloatArgumentType(
private val min: Double? = null,
private val max: Double? = null
) : KommandArgumentType<Double> {
override fun parse(input: String, context: KommandContext): ArgumentParseResult<Double> {
val value = input.toDoubleOrNull()
?: return ArgumentParseResult.Failure("Expected decimal number but got '$input'")
if (min != null && value < min) {
return ArgumentParseResult.Failure("Value must be >= $min")
}
if (max != null && value > max) {
return ArgumentParseResult.Failure("Value must be <= $max")
}
return ArgumentParseResult.Success(value)
}
}
class PlayerArgumentType(
private val allowSelectors: Boolean
) : KommandArgumentType<Player> {
override fun parse(input: String, context: KommandContext): ArgumentParseResult<Player> {
val trimmed = input.trim()
if (allowSelectors && trimmed.startsWith("@")) {
val entities = try {
Bukkit.selectEntities(context.sender, trimmed)
} catch (ex: IllegalArgumentException) {
return ArgumentParseResult.Failure(ex.message ?: "Invalid selector '$trimmed'")
}
val player = entities.firstOrNull { it is Player } as? Player
?: return ArgumentParseResult.Failure("Selector '$trimmed' did not match a player")
return ArgumentParseResult.Success(player)
}
val player = Bukkit.getPlayerExact(trimmed)
?: return ArgumentParseResult.Failure("Player '$trimmed' is not online")
return ArgumentParseResult.Success(player)
}
override fun suggestions(context: KommandContext, prefix: String): List<String> {
val names = Bukkit.getOnlinePlayers()
.map { it.name }
.filter { it.startsWith(prefix, ignoreCase = true) }
if (!allowSelectors) return names
val selectors = DEFAULT_SELECTOR_SUGGESTIONS.filter { it.startsWith(prefix) }
return (names + selectors).distinct()
}
}
class PlayerSelectorArgumentType(
private val allowDirectNames: Boolean
) : KommandArgumentType<List<Player>> {
override fun parse(input: String, context: KommandContext): ArgumentParseResult<List<Player>> {
val trimmed = input.trim()
if (!trimmed.startsWith("@")) {
if (!allowDirectNames) {
return ArgumentParseResult.Failure("Selector expected but got '$trimmed'")
}
val player = Bukkit.getPlayerExact(trimmed)
?: return ArgumentParseResult.Failure("Player '$trimmed' is not online")
return ArgumentParseResult.Success(listOf(player))
}
val entities = try {
Bukkit.selectEntities(context.sender, trimmed)
} catch (ex: IllegalArgumentException) {
return ArgumentParseResult.Failure(ex.message ?: "Invalid selector '$trimmed'")
}
val players = entities.filterIsInstance<Player>()
if (players.isEmpty()) {
return ArgumentParseResult.Failure("Selector '$trimmed' did not match any players")
}
return ArgumentParseResult.Success(players)
}
override fun suggestions(context: KommandContext, prefix: String): List<String> {
val candidates = linkedSetOf<String>()
candidates += Bukkit.getOnlinePlayers()
.map { it.name }
.filter { it.startsWith(prefix, ignoreCase = true) }
candidates += DEFAULT_SELECTOR_SUGGESTIONS.filter { it.startsWith(prefix) }
return candidates.toList()
}
}
class EntitySelectorArgumentType(
private val requireMatch: Boolean
) : KommandArgumentType<List<Entity>> {
override fun parse(input: String, context: KommandContext): ArgumentParseResult<List<Entity>> {
val trimmed = input.trim()
if (!trimmed.startsWith("@")) {
val player = Bukkit.getPlayerExact(trimmed)
if (player != null) return ArgumentParseResult.Success(listOf(player))
return if (requireMatch) {
ArgumentParseResult.Failure("No entity matched '$trimmed'")
} else {
ArgumentParseResult.Success(emptyList())
}
}
val entities = try {
Bukkit.selectEntities(context.sender, trimmed)
} catch (ex: IllegalArgumentException) {
return ArgumentParseResult.Failure(ex.message ?: "Invalid selector '$trimmed'")
}
if (requireMatch && entities.isEmpty()) {
return ArgumentParseResult.Failure("Selector '$trimmed' did not match any entities")
}
return ArgumentParseResult.Success(entities)
}
override fun suggestions(context: KommandContext, prefix: String): List<String> {
return DEFAULT_SELECTOR_SUGGESTIONS.filter { it.startsWith(prefix) }
}
}
class CoordinateComponentArgumentType(
private val allowRelative: Boolean
) : KommandArgumentType<CoordinateComponent> {
override fun parse(input: String, context: KommandContext): ArgumentParseResult<CoordinateComponent> {
val trimmed = input.trim()
if (allowRelative && trimmed.startsWith("~")) {
val remainder = trimmed.removePrefix("~")
val offset = if (remainder.isEmpty()) 0.0 else remainder.toDoubleOrNull()
?: return ArgumentParseResult.Failure("Invalid relative coordinate '$trimmed'")
return ArgumentParseResult.Success(CoordinateComponent(relative = true, value = offset))
}
val absolute = trimmed.toDoubleOrNull()
?: return ArgumentParseResult.Failure("Expected coordinate but got '$trimmed'")
return ArgumentParseResult.Success(CoordinateComponent(relative = false, value = absolute))
}
override fun suggestions(context: KommandContext, prefix: String): List<String> {
if (!allowRelative) return emptyList()
return if ("~".startsWith(prefix)) listOf("~") else emptyList()
}
}
private val DEFAULT_SELECTOR_SUGGESTIONS = listOf("@p", "@a", "@s", "@r", "@e")
data class CoordinateComponent(val relative: Boolean, val value: Double) {
fun resolve(origin: Double): Double = if (relative) origin + value else value
}
data class Coordinates3(
val x: CoordinateComponent,
val y: CoordinateComponent,
val z: CoordinateComponent
) {
fun resolve(origin: Location): Location {
val clone = origin.clone()
clone.x = x.resolve(clone.x)
clone.y = y.resolve(clone.y)
clone.z = z.resolve(clone.z)
return clone
}
}
@@ -0,0 +1,55 @@
package net.hareworks.kommand_lib.context
import net.hareworks.kommand_lib.arguments.Coordinates3
import org.bukkit.Location
import org.bukkit.command.CommandSender
import org.bukkit.entity.Entity
import org.bukkit.entity.Player
import org.bukkit.plugin.java.JavaPlugin
import net.hareworks.kommand_lib.execution.ParseMode
open class KommandContext internal constructor(
val plugin: JavaPlugin,
val sender: CommandSender,
val alias: String,
val args: Array<String>,
val mode: ParseMode
) {
private val parsedArguments = linkedMapOf<String, Any?>()
internal fun remember(name: String, value: Any?) {
parsedArguments[name] = value
}
internal fun drop(name: String) {
parsedArguments.remove(name)
}
@Suppress("UNCHECKED_CAST")
fun <T> argument(name: String): T =
parsedArguments[name] as? T
?: error("Argument '$name' is not present in this context.")
@Suppress("UNCHECKED_CAST")
fun <T> argumentOrNull(name: String): T? = parsedArguments[name] as? T
fun arguments(): Map<String, Any?> = parsedArguments.toMap()
}
fun KommandContext.string(name: String): String = argument(name)
fun KommandContext.int(name: String): Int = argument(name)
fun KommandContext.double(name: String): Double = argument(name)
fun KommandContext.float(name: String): Double = argument(name)
fun KommandContext.player(name: String): Player = argument(name)
fun KommandContext.players(name: String): List<Player> = argument(name)
fun KommandContext.selector(name: String): List<Entity> = argument(name)
fun KommandContext.coordinates(name: String): Coordinates3 = argument(name)
fun KommandContext.location(name: String, origin: Location): Location = coordinates(name).resolve(origin)
@@ -0,0 +1,198 @@
package net.hareworks.kommand_lib.dsl
import net.hareworks.kommand_lib.CommandDefinition
import net.hareworks.kommand_lib.arguments.*
import net.hareworks.kommand_lib.nodes.Axis
import net.hareworks.kommand_lib.nodes.CoordinateAxisNode
import net.hareworks.kommand_lib.nodes.KommandNode
import net.hareworks.kommand_lib.nodes.LiteralNode
import net.hareworks.kommand_lib.nodes.ValueNode
import org.bukkit.command.CommandSender
import org.bukkit.entity.Entity
import org.bukkit.entity.Player
import org.bukkit.plugin.java.JavaPlugin
@KommandDsl
class KommandRegistry internal constructor(private val plugin: JavaPlugin) {
private val definitions = mutableListOf<CommandDefinition>()
/**
* Declares a new command root.
*/
fun command(name: String, vararg aliases: String, block: CommandBuilder.() -> Unit) {
val builder = CommandBuilder(name, aliases.toList())
builder.block()
definitions += builder.build()
}
fun command(name: String, aliases: Iterable<String>, block: CommandBuilder.() -> Unit) {
val builder = CommandBuilder(name, aliases.toList())
builder.block()
definitions += builder.build()
}
internal fun build(): net.hareworks.kommand_lib.KommandLib =
net.hareworks.kommand_lib.KommandLib(plugin, definitions.toList())
}
@KommandDsl
class CommandBuilder internal constructor(
val name: String,
val aliases: List<String>
) : BranchScope(mutableListOf()) {
var description: String? = null
var usage: String? = null
var permission: String? = null
private var condition: (CommandSender) -> Boolean = { true }
private var rootExecutor: (net.hareworks.kommand_lib.context.KommandContext.() -> Unit)? = null
fun condition(predicate: (CommandSender) -> Boolean) {
condition = predicate
}
fun executes(block: net.hareworks.kommand_lib.context.KommandContext.() -> Unit) {
rootExecutor = block
}
override val inheritedPermission: String?
get() = permission
override val inheritedCondition: (CommandSender) -> Boolean
get() = condition
internal fun build(): CommandDefinition =
CommandDefinition(
name = name,
aliases = aliases,
description = description,
usage = usage,
permission = permission,
rootCondition = condition,
rootExecutor = rootExecutor,
nodes = children.toList()
)
}
@KommandDsl
abstract class BranchScope internal constructor(
protected val children: MutableList<KommandNode>
) {
protected abstract val inheritedPermission: String?
protected abstract val inheritedCondition: (CommandSender) -> Boolean
fun literal(name: String, block: LiteralBuilder.() -> Unit = {}) {
val node = LiteralNode(name)
node.permission = inheritedPermission
node.condition = inheritedCondition
children += node
LiteralBuilder(node).apply(block)
}
fun <T> argument(name: String, type: KommandArgumentType<T>, block: ValueBuilder<T>.() -> Unit = {}) {
val node = ValueNode(name, type)
node.permission = inheritedPermission
node.condition = inheritedCondition
children += node
ValueBuilder(node).apply(block)
}
fun string(name: String, block: ValueBuilder<String>.() -> Unit = {}) = argument(name, WordArgumentType, block)
fun integer(
name: String,
min: Int? = null,
max: Int? = null,
block: ValueBuilder<Int>.() -> Unit = {}
) = argument(name, IntegerArgumentType(min, max), block)
fun float(
name: String,
min: Double? = null,
max: Double? = null,
block: ValueBuilder<Double>.() -> Unit = {}
) = argument(name, FloatArgumentType(min, max), block)
fun player(
name: String,
allowSelectors: Boolean = true,
block: ValueBuilder<Player>.() -> Unit = {}
) = argument(name, PlayerArgumentType(allowSelectors), block)
fun players(
name: String,
allowDirectNames: Boolean = true,
block: ValueBuilder<List<Player>>.() -> Unit = {}
) = argument(name, PlayerSelectorArgumentType(allowDirectNames), block)
fun selector(
name: String,
requireMatch: Boolean = true,
block: ValueBuilder<List<Entity>>.() -> Unit = {}
) = argument(name, EntitySelectorArgumentType(requireMatch), block)
fun coordinates(
name: String,
allowRelative: Boolean = true,
block: CoordinateNodeScope.() -> Unit = {}
) {
val xNode = CoordinateAxisNode(name, Axis.X, allowRelative)
val yNode = CoordinateAxisNode(name, Axis.Y, allowRelative)
val zNode = CoordinateAxisNode(name, Axis.Z, allowRelative)
val nodes = listOf(xNode, yNode, zNode)
nodes.forEach { node ->
node.permission = inheritedPermission
node.condition = inheritedCondition
}
xNode.children += yNode
yNode.children += zNode
children += xNode
CoordinateNodeScope(zNode).apply(block)
}
}
@KommandDsl
abstract class NodeScope internal constructor(
protected val node: KommandNode
) : BranchScope(node.children) {
override val inheritedPermission: String?
get() = node.permission
override val inheritedCondition: (CommandSender) -> Boolean
get() = node.condition
fun requires(permission: String) {
node.permission = permission
}
fun condition(predicate: (CommandSender) -> Boolean) {
node.condition = predicate
}
fun executes(block: net.hareworks.kommand_lib.context.KommandContext.() -> Unit) {
node.executor = block
}
}
@KommandDsl
class LiteralBuilder internal constructor(
node: LiteralNode
) : NodeScope(node)
@KommandDsl
class ValueBuilder<T> internal constructor(
private val valueNode: ValueNode<T>
) : NodeScope(valueNode) {
/**
* Overrides the default suggestion provider for this argument.
*/
fun suggests(block: net.hareworks.kommand_lib.context.KommandContext.(prefix: String) -> List<String>) {
valueNode.suggestionProvider = { ctx, prefix -> block(ctx, prefix) }
}
}
@KommandDsl
class CoordinateNodeScope internal constructor(node: KommandNode) : NodeScope(node)
@DslMarker
annotation class KommandDsl
@@ -0,0 +1,59 @@
package net.hareworks.kommand_lib.execution
import net.hareworks.kommand_lib.context.KommandContext
import net.hareworks.kommand_lib.nodes.KommandNode
internal class CommandTree(private val roots: List<KommandNode>) {
fun execute(context: KommandContext): Boolean {
if (context.args.isEmpty()) return false
val node = match(roots, context, 0) ?: return false
val executor = node.executor ?: return false
executor.invoke(context)
return true
}
fun tabComplete(context: KommandContext): List<String> {
if (context.args.isEmpty()) return emptyList()
return collect(roots, context, 0)
}
private fun match(nodes: List<KommandNode>, context: KommandContext, index: Int): KommandNode? {
if (index >= context.args.size) return null
val token = context.args[index]
for (node in nodes) {
if (!node.isVisible(context.sender)) continue
if (!node.consume(token, context, ParseMode.EXECUTE)) continue
if (index == context.args.lastIndex && node.executor != null) {
return node
}
val result = match(node.children, context, index + 1)
if (result != null) return result
node.undo(context)
}
return null
}
private fun collect(nodes: List<KommandNode>, context: KommandContext, index: Int): List<String> {
val token = context.args[index]
val last = index == context.args.lastIndex
val suggestions = linkedSetOf<String>()
for (node in nodes) {
if (!node.isVisible(context.sender)) continue
if (last) {
suggestions += node.suggestions(token, context)
}
if (node.consume(token, context, ParseMode.SUGGEST)) {
if (!last && node.children.isNotEmpty()) {
suggestions += collect(node.children, context, index + 1)
}
node.undo(context)
}
}
return suggestions.toList()
}
}
enum class ParseMode {
EXECUTE,
SUGGEST
}
@@ -0,0 +1,98 @@
package net.hareworks.kommand_lib.nodes
import net.hareworks.kommand_lib.arguments.CoordinateComponent
import net.hareworks.kommand_lib.arguments.Coordinates3
import net.hareworks.kommand_lib.arguments.KommandArgumentType
import net.hareworks.kommand_lib.context.KommandContext
import net.hareworks.kommand_lib.execution.ParseMode
import org.bukkit.command.CommandSender
abstract class KommandNode internal constructor() {
val children: MutableList<KommandNode> = mutableListOf()
var executor: (KommandContext.() -> Unit)? = null
var permission: String? = null
var condition: (CommandSender) -> Boolean = { true }
fun isVisible(sender: CommandSender): Boolean {
val perm = permission
if (!perm.isNullOrBlank() && !sender.hasPermission(perm)) return false
return condition(sender)
}
abstract fun consume(token: String, context: KommandContext, mode: ParseMode): Boolean
open fun undo(context: KommandContext) {}
abstract fun suggestions(prefix: String, context: KommandContext): List<String>
}
class LiteralNode internal constructor(private val literal: String) : KommandNode() {
override fun consume(token: String, context: KommandContext, mode: ParseMode): Boolean {
return literal.equals(token, ignoreCase = true)
}
override fun suggestions(prefix: String, context: KommandContext): List<String> {
return if (literal.startsWith(prefix, ignoreCase = true)) listOf(literal) else emptyList()
}
}
open class ValueNode<T> internal constructor(
private val name: String,
private val type: KommandArgumentType<T>
) : KommandNode() {
var suggestionProvider: ((KommandContext, String) -> List<String>)? = null
override fun consume(token: String, context: KommandContext, mode: ParseMode): Boolean {
return when (val result = type.parse(token, context)) {
is net.hareworks.kommand_lib.arguments.ArgumentParseResult.Success -> {
context.remember(name, result.value)
true
}
is net.hareworks.kommand_lib.arguments.ArgumentParseResult.Failure -> {
if (mode == ParseMode.EXECUTE) {
context.sender.sendMessage(result.reason)
}
false
}
}
}
override fun undo(context: KommandContext) {
context.drop(name)
}
override fun suggestions(prefix: String, context: KommandContext): List<String> {
val custom = suggestionProvider?.invoke(context, prefix)
if (custom != null) return custom
return type.suggestions(context, prefix)
}
}
private fun coordinateAxisKey(base: String, axis: Axis): String = "$base::__${axis.name.lowercase()}"
class CoordinateAxisNode(
private val aggregateName: String,
private val axis: Axis,
allowRelative: Boolean
) : ValueNode<CoordinateComponent>(coordinateAxisKey(aggregateName, axis),
net.hareworks.kommand_lib.arguments.CoordinateComponentArgumentType(allowRelative)) {
override fun consume(token: String, context: KommandContext, mode: ParseMode): Boolean {
val success = super.consume(token, context, mode)
if (success && axis == Axis.Z) {
val x = context.argumentOrNull<CoordinateComponent>(coordinateAxisKey(aggregateName, Axis.X))
val y = context.argumentOrNull<CoordinateComponent>(coordinateAxisKey(aggregateName, Axis.Y))
val z = context.argumentOrNull<CoordinateComponent>(coordinateAxisKey(aggregateName, Axis.Z))
if (x != null && y != null && z != null) {
context.remember(aggregateName, Coordinates3(x, y, z))
}
}
return success
}
override fun undo(context: KommandContext) {
if (axis == Axis.Z) {
context.drop(aggregateName)
}
super.undo(context)
}
}
enum class Axis { X, Y, Z }