diff --git a/.gitignore b/.gitignore
index ab84b87..fb70c28 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,4 @@
/.gradle/
/run/
/.kotlin/
+/logs/
diff --git a/README.md b/README.md
index c0069ed..38c6335 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,5 @@
+
# Backbone
@@ -7,7 +8,7 @@

[](https://github.com/integr-dev/backbone/blob/master/LICENSE)
-Backbone is a powerful and flexible plugin for Spigot-based Minecraft servers, designed to supercharge server customization. Its core philosophy is to enable server administrators and developers to write, test, and update server logic on a live server without requiring restarts, dramatically accelerating the development lifecycle.
+Backbone is a powerful and flexible plugin for Spigot-based Minecraft servers, designed to supercharge server customization. Its core philosophy is to enable server administrators and developers to write, test, and update server logic on a live server without requiring restarts, dramatically speeding up the development lifecycle.
Whether you're a server administrator looking to add custom features with simple scripts or a developer prototyping new ideas, Backbone provides the tools you need to be more productive and creative.
@@ -17,7 +18,7 @@ Whether you're a server administrator looking to add custom features with simple
- **Advanced Scripting:** Go beyond simple scripts with support for inter-script imports, Maven dependencies, and custom compiler options.
- **Event System:** A custom event bus that complements Bukkit's event system, offering more control and flexibility within your scripts.
- **Command Framework:** A simple yet powerful command system to create custom commands directly from your scripts.
-- **Storage Abstraction:** Easily manage data with a flexible storage system that supports SQLite databases and typed configuration files.
+- **Storage Abstraction:** Manage data with a flexible storage system that supports SQLite databases and typed configuration files.
- **GUI Framework:** A declarative GUI framework for creating complex and interactive inventories from your scripts.
- **Text Formatting:** A flexible text formatting system with support for custom alphabets and color codes.
- **Entity Framework:** Custom entity utility for adding custom entities via the goals api.
@@ -26,11 +27,11 @@ Whether you're a server administrator looking to add custom features with simple
## Getting Started
-Getting started with Backbone is simple. The primary way to use Backbone is by installing it as a plugin and then creating your own custom features through its scripting engine.
+Getting started with Backbone is straightforward. The primary way to use Backbone is by installing it as a plugin and then creating your own custom features through its scripting engine.
### Requirements
- Minecraft Java Edition Server version 1.21 or higher.
-- [PlaceholderAPI](https://www.spigotmc.org/resources/placeholderapi.62/) (optional, for placeholder support).
+- [PlaceholderAPI](https://modrinth.com/plugin/placeholderapi) (optional, for placeholder support).
### Installation
1. **Download:** Download the latest release from the [official releases page](https://github.com/integr-dev/backbone/releases).
@@ -186,7 +187,7 @@ This will create directories at `storage/mystorage/` and `config/myconfig/` in y
#### Configuration
-You can easily manage typed configuration files. Backbone handles the serialization and deserialization of your data classes automatically.
+You can manage typed configuration files. Backbone handles the serialization and deserialization of your data classes automatically.
First, define a serializable data class for your configuration:
@@ -215,7 +216,6 @@ configHandler.writeState(currentConfig.copy(settingB = 20))
#### Databases
Backbone provides a simple and efficient way to work with SQLite databases from within your scripts.
-
```kotlin
// Get a connection to a database file named 'playerdata.db'
val dbConnection = myScriptStorage.database("playerdata.db")
@@ -239,7 +239,7 @@ dbConnection.useConnection {
### Custom Events
-Backbone's event system allows you to create and listen for custom events, giving you more control over your script's behavior.
+Backbone's event system allows you to create and listen to custom events, giving you more control over your script's behavior.
```kotlin
// Define a custom event
@@ -250,7 +250,7 @@ class MyCustomEvent(val message: String) : Event()
@BackboneEventHandler(EventPriority.THREE_BEFORE)
fun onMyCustomEvent(event: MyCustomEvent) {
println("Received custom event: ${event.message}")
- event.setCallback("yay!")
+ event.callback = "yay!"
}
// Fire the custom event from anywhere in your code
@@ -279,14 +279,14 @@ object MyCommand : Command("mycommand", "My first command") {
}
override suspend fun exec(ctx: Execution) {
- // Require a permission for this command
+ // Require permission for this command
ctx.requirePermission(perm.derive("mycommand")) // "myplugin.mycommand"
val text = ctx.get("text")
ctx.respond("Hello ${ctx.sender.name}: $text")
- // To affect server state, dispatch to the main thread for the next tick.
+ // To affect the server state, dispatch to the main thread for the next tick.
Backbone.dispatchMain {
val player = ctx.getPlayer() // Get the sender as a player (and require it to be one)
player.world.spawnEntity(player.location, EntityType.BEE)
@@ -382,7 +382,7 @@ Backbone allows you to create custom entities with unique AI goals.
// Define a custom entity that is a non-moving zombie
object GuardEntity : CustomEntity("guard", EntityType.ZOMBIE) {
override fun prepare(mob: Zombie) {
- // Set up for example armor
+ // Set up, for example, armor
}
override fun setupGoals(mob: Zombie) {
@@ -398,7 +398,7 @@ override fun onLoad() {
Backbone.Handlers.ENTITY.register(GuardEntity)
}
-// You can then spawn the entity for example using a command
+// You can then spawn the entity, for example, using a command
// In a command's exec method:
GuardEntity.spawn(ctx.getPlayer().location, ctx.getPlayer().world)
```
@@ -467,7 +467,7 @@ component {
#### Command Feedback Format
-You can create a custom `CommandFeedbackFormat` to change how command responses are displayed. Or simply inherit from it to unlock even more customisation via the component system.
+You can create a custom `CommandFeedbackFormat` to change how command responses are displayed. Or inherit from it to unlock even more customization via the component system.
```kotlin
val myFormat = CommandFeedbackFormat("MyPlugin", Color.RED)
@@ -486,12 +486,7 @@ You can create your own custom alphabets by implementing the `Alphabet` interfac
```kotlin
object MyAlphabet : Alphabet {
- const val ALPHABET = "..." // Your custom alphabet characters
-
- override fun encode(str: String): String {
- // Your encoding logic here
- return "encoded_string"
- }
+ override val alphabet = "..." // Your custom alphabet characters
}
```
diff --git a/build.gradle.kts b/build.gradle.kts
index 33d1581..590645b 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -7,7 +7,7 @@ plugins {
}
group = "net.integr"
-version = "1.2.0"
+version = "1.4.0"
repositories {
mavenCentral()
@@ -41,7 +41,7 @@ dependencies {
implementation("org.apache.ivy:ivy:2.5.2")
- implementation("tools.jackson.core:jackson-databind:3.0.4")
+ implementation("tools.jackson.core:jackson-databind:3.1.0")
implementation("tools.jackson.dataformat:jackson-dataformat-yaml:3.0.4")
implementation("tools.jackson.module:jackson-module-kotlin:3.0.4")
diff --git a/src/main/kotlin/net/integr/backbone/Backbone.kt b/src/main/kotlin/net/integr/backbone/Backbone.kt
index ff20086..5cf3dc0 100644
--- a/src/main/kotlin/net/integr/backbone/Backbone.kt
+++ b/src/main/kotlin/net/integr/backbone/Backbone.kt
@@ -36,7 +36,6 @@ import org.jetbrains.annotations.ApiStatus
* @since 1.0.0
*/
object Backbone {
- //TODO: dialogues, command help builder
/**
* Backbones internal storage pool. **Important:** Do not use this.
* Create a new pool instead:
@@ -109,12 +108,15 @@ object Backbone {
/**
* The internally checked plugin instance.
- * Null if we are in a testing environment.
+ * Null if we are not in a plugin environment.
+ *
+ * This is used to avoid null checks in the codebase.
+ * If you are not in a plugin environment, you can safely ignore this.
*
* @since 1.0.0
*/
private val pluginInternal: JavaPlugin? by lazy {
- Utils.tryOrNull { JavaPlugin.getPlugin(BackboneServer::class.java) } // For testing purposes
+ Utils.tryOrNull { JavaPlugin.getPlugin(BackboneServer::class.java) } // For testing purposes we allow null here
}
/**
@@ -144,28 +146,28 @@ object Backbone {
/**
* Registers a listener to the server's plugin manager and the internal event bus.
+ * 1. Registers the listener with the internal event bus.
+ * 2. Registers the listener with the plugin manager.
*
* @param listener The listener to register.
*
* @since 1.0.0
*/
fun registerListener(listener: Listener) {
- LOGGER.info("Registering listener: ${listener.javaClass.name}")
- SERVER.pluginManager.registerEvents(listener, PLUGIN)
EventBus.register(listener)
+ SERVER.pluginManager.registerEvents(listener, PLUGIN)
}
/**
* Removes a listener from the server's plugin manager and the internal event bus.
*
- * @param listener The listener to register.
+ * @param listener The listener to unregister.
*
* @since 1.0.0
*/
fun unregisterListener(listener: Listener) {
- LOGGER.info("Unregistering listener: ${listener.javaClass.name}")
- HandlerList.unregisterAll(listener)
EventBus.unregister(listener)
+ HandlerList.unregisterAll(listener)
}
/**
diff --git a/src/main/kotlin/net/integr/backbone/BackboneLogger.kt b/src/main/kotlin/net/integr/backbone/BackboneLogger.kt
index 8d25198..5010603 100644
--- a/src/main/kotlin/net/integr/backbone/BackboneLogger.kt
+++ b/src/main/kotlin/net/integr/backbone/BackboneLogger.kt
@@ -71,7 +71,26 @@ class BackboneLogger(name: String) : Logger(name, null) {
if (record.level == Level.SEVERE) logFile.appendText(fileMessage + "\n")
}
+ /**
+ * Flushes any buffered output.
+ *
+ * This implementation is intentionally empty as the handler writes directly to
+ * the console (via `println`) and file (via `appendText`), both of which handle
+ * their own flushing automatically.
+ *
+ * @since 1.0.0
+ */
override fun flush() {}
+
+ /**
+ * Closes the handler and releases any associated resources.
+ *
+ * This implementation is intentionally empty as the handler does not maintain
+ * any resources that require explicit cleanup. The log file is opened and closed
+ * on each write operation, and console output requires no cleanup.
+ *
+ * @since 1.0.0
+ */
override fun close() {}
}
diff --git a/src/main/kotlin/net/integr/backbone/Utils.kt b/src/main/kotlin/net/integr/backbone/Utils.kt
index 22e5f36..ad40503 100644
--- a/src/main/kotlin/net/integr/backbone/Utils.kt
+++ b/src/main/kotlin/net/integr/backbone/Utils.kt
@@ -13,6 +13,9 @@
package net.integr.backbone
+import net.kyori.adventure.builder.AbstractBuilder
+import kotlin.reflect.full.declaredMemberFunctions
+
/**
* Utility functions for various tasks.
* @since 1.0.0
@@ -54,4 +57,41 @@ object Utils {
fun isUid(string: String): Boolean {
return string.matches("^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$".toRegex())
}
+
+ /**
+ * Used to more easily get the result of a builder with applied block.
+ *
+ * Example:
+ * ```kotlin
+ * val builder = Something.builder()
+ * builder.block()
+ * val result = builder.build()
+ * ```
+ *
+ * is changed to
+ *
+ * ```kotlin
+ * val result = blockBuild(Something.builder(), block)
+ * ```
+ *
+ * Invokes a builders build method via reflection.
+ * Does not run any safety checks. It is your job to figure out
+ * if this will work or not.
+ *
+ * @param T the builder class
+ * @param U the builders result class
+ * @param builder the builder instance
+ * @param block the block to apply to the builder
+ * @since 1.4.0
+ */
+ inline fun blockBuild(builder: T, block: T.() -> Unit): U {
+ builder.block()
+ // Assume a build method is there
+ val method = builder::class.java.getDeclaredMethod("build")
+
+ // It is the users duty to only call this on builders with this signature
+ @Suppress("UNCHECKED_CAST")
+ val result = method.invoke(builder) as U
+ return result
+ }
}
\ No newline at end of file
diff --git a/src/main/kotlin/net/integr/backbone/commands/BackboneCommand.kt b/src/main/kotlin/net/integr/backbone/commands/BackboneCommand.kt
index 7ff4090..0714fa1 100644
--- a/src/main/kotlin/net/integr/backbone/commands/BackboneCommand.kt
+++ b/src/main/kotlin/net/integr/backbone/commands/BackboneCommand.kt
@@ -14,11 +14,14 @@
package net.integr.backbone.commands
import net.integr.backbone.Backbone
+import net.integr.backbone.commands.arguments.ValidatedArgument.ValidationResult.Companion.fail
+import net.integr.backbone.commands.arguments.commandArgument
import net.integr.backbone.commands.arguments.customEntityArgument
import net.integr.backbone.commands.arguments.customItemArgument
import net.integr.backbone.commands.arguments.scriptArgument
import net.integr.backbone.commands.arguments.stringArgument
import net.integr.backbone.systems.command.Command
+import net.integr.backbone.systems.command.CommandHandler
import net.integr.backbone.systems.command.Execution
import net.integr.backbone.systems.entity.EntityHandler
import net.integr.backbone.systems.hotloader.ScriptEngine
@@ -37,18 +40,42 @@ import java.awt.Color
*
* @since 1.0.0
*/
-object BackboneCommand : Command("backbone", "Base command for Backbone", listOf("bb")) {
+object BackboneCommand : Command("backbone", "Base command for backbone", listOf("bb")) {
val perm = Backbone.ROOT_PERMISSION.derive("command")
override fun onBuild() {
- subCommands(Scripting, Item, Entity)
+ subCommands(Scripting, Item, Entity, Help)
}
override suspend fun exec(ctx: Execution) {
ctx.respond("Backbone v${Backbone.VERSION}")
}
- object Scripting : Command("scripting", "Commands for Backbone scripting system") {
+ object Help : Command("help", "Shows help for a specific command") {
+ val helpPerm = perm.derive("help")
+
+ override fun onBuild() {
+ arguments(
+ commandArgument("command", "The command to get help for")
+ )
+ }
+
+ override suspend fun exec(ctx: Execution) {
+ ctx.requirePermission(helpPerm)
+
+ val command = ctx.get("command")
+ val help = CommandHandler.getHelp(command)
+
+ if (help == null) {
+ fail("No help found for command: $command")
+ } else {
+ ctx.respond("Displaying help for: $command")
+ ctx.respondComponent(help.buildComponent())
+ }
+ }
+ }
+
+ object Scripting : Command("scripting", "Commands for the backbone scripting system") {
val scriptingPerm = perm.derive("scripting")
override fun onBuild() {
@@ -84,7 +111,7 @@ object BackboneCommand : Command("backbone", "Base command for Backbone", listOf
}
}
- object Reload : Command("reload", "Reload all Backbone scripts") {
+ object Reload : Command("reload", "Reload all backbone scripts") {
val scriptingReloadPerm = scriptingPerm.derive("reload")
override suspend fun exec(ctx: Execution) {
@@ -100,7 +127,7 @@ object BackboneCommand : Command("backbone", "Base command for Backbone", listOf
}
}
- object Enable : Command("enable", "Enable a Backbone script") {
+ object Enable : Command("enable", "Enable a backbone script") {
val scriptingEnablePerm = scriptingPerm.derive("enable")
override fun onBuild() {
@@ -125,7 +152,7 @@ object BackboneCommand : Command("backbone", "Base command for Backbone", listOf
}
}
- object Disable : Command("disable", "Disable a Backbone script") {
+ object Disable : Command("disable", "Disable a backbone script") {
val scriptingDisablePerm = scriptingPerm.derive("disable")
override fun onBuild() {
@@ -180,7 +207,7 @@ object BackboneCommand : Command("backbone", "Base command for Backbone", listOf
}
}
- object Item : Command("item", "Commands for Backbone item system") {
+ object Item : Command("item", "Commands for the backbone item system") {
val itemPerm = perm.derive("item")
override fun onBuild() {
@@ -200,12 +227,12 @@ object BackboneCommand : Command("backbone", "Base command for Backbone", listOf
}
}
- object Give : Command("give", "Gives a custom item") {
+ object Give : Command("give", "Gives you a custom item") {
val itemGivePerm = itemPerm.derive("give")
override fun onBuild() {
arguments(
- customItemArgument("item", "The custom item to give")
+ customItemArgument("item", "The custom item to give you")
)
}
@@ -249,7 +276,7 @@ object BackboneCommand : Command("backbone", "Base command for Backbone", listOf
}
}
- object Read : Command("read", "Reads all meta tags from an item.") {
+ object Read : Command("read", "Reads all meta tags from the held item.") {
val itemReadPerm = itemPerm.derive("read")
override suspend fun exec(ctx: Execution) {
@@ -281,7 +308,7 @@ object BackboneCommand : Command("backbone", "Base command for Backbone", listOf
}
}
- object Entity : Command("entity", "Commands for Backbone entity system") {
+ object Entity : Command("entity", "Commands for the backbone entity system") {
val entityPerm = perm.derive("entity")
override fun onBuild() {
diff --git a/src/main/kotlin/net/integr/backbone/commands/arguments/CommandArgument.kt b/src/main/kotlin/net/integr/backbone/commands/arguments/CommandArgument.kt
new file mode 100644
index 0000000..6d18a8e
--- /dev/null
+++ b/src/main/kotlin/net/integr/backbone/commands/arguments/CommandArgument.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright © 2026 Integr
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.integr.backbone.commands.arguments
+
+import net.integr.backbone.systems.command.CommandArgumentException
+import net.integr.backbone.systems.command.CommandHandler
+import net.integr.backbone.systems.command.argument.Argument
+
+/**
+ * A command argument that parses a command.
+ *
+ * This argument accepts any valid custom item name as input.
+ * It provides completions for all registered custom items.
+ *
+ * @param name The name of the argument.
+ * @param description A brief description of the argument's purpose.
+ * @since 1.3.0
+ */
+fun commandArgument(name: String, description: String): Argument {
+ return CommandArgument(name, description)
+}
+
+/**
+ * A command argument that parses a command.
+ *
+ * This argument accepts any valid custom item name as input.
+ * It provides completions for all registered custom items.
+ *
+ * @param name The name of the argument.
+ * @param description A brief description of the argument's purpose.
+ * @since 1.0.0
+ */
+class CommandArgument(name: String, description: String) : Argument(name, description) {
+ override fun getCompletions(current: ArgumentInput): CompletionResult {
+ val commands = CommandHandler.commands.keys
+ val isQuoted = current.value.startsWith("\"")
+
+ val arg = if (isQuoted) current.getNextGreedyWithBoundChar('"') else current.getNextSingle()
+
+ val hasClosingQuote = isQuoted && arg.found
+
+ val itemMap = commands.map { it + "\"" }.toMutableList()
+
+ return if (isQuoted && !hasClosingQuote) {
+ CompletionResult(itemMap, arg.end)
+ } else {
+ CompletionResult(if (arg.text.isBlank()) mutableListOf("<$name:command>", *commands.toTypedArray()) else commands.toMutableList(), arg.end)
+ }
+ }
+
+ override fun parse(current: ArgumentInput): ParseResult {
+ val commands = CommandHandler.commands.keys
+ val isQuoted = current.value.startsWith("\"")
+ val arg = if (isQuoted) current.getNextGreedyWithBoundChar('"') else current.getNextSingle()
+
+ val text = if (isQuoted) {
+ if (!arg.found) throw CommandArgumentException("Argument '$name' is missing a closing quotation mark.")
+ arg.text.substring(1, arg.text.length - 1)
+ } else {
+ arg.text
+ }
+
+ if (!commands.contains(text)) throw CommandArgumentException("Argument '$name' is not a valid command.")
+
+ return ParseResult(text, arg.end)
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/net/integr/backbone/systems/command/Command.kt b/src/main/kotlin/net/integr/backbone/systems/command/Command.kt
index f4744bc..76c0df0 100644
--- a/src/main/kotlin/net/integr/backbone/systems/command/Command.kt
+++ b/src/main/kotlin/net/integr/backbone/systems/command/Command.kt
@@ -17,6 +17,7 @@ import kotlinx.coroutines.launch
import net.integr.backbone.Backbone
import net.integr.backbone.systems.command.argument.ArgumentChain
import net.integr.backbone.systems.command.argument.Argument
+import net.integr.backbone.systems.command.help.HelpNode
import net.integr.backbone.text.formats.CommandFeedbackFormat
import org.bukkit.command.CommandSender
import org.bukkit.command.defaults.BukkitCommand
@@ -35,11 +36,46 @@ import org.jetbrains.annotations.ApiStatus
abstract class Command(name: String, description: String, aliases: List = listOf(), val format: CommandFeedbackFormat = CommandHandler.defaultFeedbackFormat) : BukkitCommand(name, description, "See backbone help", aliases) {
private val logger = Backbone.LOGGER.derive("command")
- private val subCommands = mutableListOf()
+ /**
+ * The list of sub-commands registered for this command.
+ * @since 1.0.0
+ */
+ @ApiStatus.Internal
+ val subCommands = mutableListOf()
+
+ /**
+ * The list of arguments registered for this command.
+ * @since 1.0.0
+ */
+ @ApiStatus.Internal
private val arguments = mutableListOf>()
private var subCommandNames: List = listOf()
+ /**
+ * The parent command of this command, if any.
+ * @since 1.3.0
+ */
+ var parent: Command? = null
+ private set
+
+ /**
+ * The full name of this command, including any parent commands.
+ * @since 1.3.0
+ */
+ var fullName: String? = null
+ private set
+
+ /**
+ * The help node for this command.
+ *
+ * Help nodes are used to display the structure of the command tree.
+ *
+ * @since 1.3.0
+ */
+ var helpNode: HelpNode? = null
+ private set
+
/**
* Registers one or more sub-commands to this command.
*
@@ -48,6 +84,7 @@ abstract class Command(name: String, description: String, aliases: List
*/
fun subCommands(vararg commands: Command) {
commands.forEach {
+ it.parent = this
it.build()
}
@@ -64,13 +101,41 @@ abstract class Command(name: String, description: String, aliases: List
this.arguments.addAll(arguments)
}
+ /**
+ * Computes the help node for this command.
+ *
+ * This method recursively builds the help node for all sub-commands and their arguments.
+ * It is used by the help command to display the command structure.
+ *
+ * @since 1.3.0
+ */
+ private fun computeHelpNode() {
+ val subCommandNodes = subCommands.map { it.helpNode ?: throw IllegalStateException("Subcommand ${it.name} has not been built yet.") }
+
+ val contents = mutableListOf()
+
+ contents.add(HelpNode.Content(description, HelpNode.Content.Type.TEXT))
+
+ if (arguments.isNotEmpty()) contents.add(HelpNode.Content("Arguments", HelpNode.Content.Type.TITLE))
+ arguments.forEach { contents.add(it.getHelpText()) }
+
+ if (aliases.isNotEmpty()) contents.add(HelpNode.Content("Aliases", HelpNode.Content.Type.TITLE))
+ aliases.forEach { contents.add(HelpNode.Content(it, HelpNode.Content.Type.LIST)) }
+
+ helpNode = HelpNode(name, contents, subCommandNodes)
+ }
+
/**
* Builds the command and its sub-commands.
* @since 1.0.0
*/
@ApiStatus.Internal
fun build() {
+ fullName = if (parent == null) name else "${parent!!.fullName}.$name"
+
onBuild()
+ computeHelpNode()
+
subCommandNames = subCommands.map { it.name } + subCommands.flatMap { it.aliases }
}
diff --git a/src/main/kotlin/net/integr/backbone/systems/command/CommandHandler.kt b/src/main/kotlin/net/integr/backbone/systems/command/CommandHandler.kt
index e4f132d..e90e36a 100644
--- a/src/main/kotlin/net/integr/backbone/systems/command/CommandHandler.kt
+++ b/src/main/kotlin/net/integr/backbone/systems/command/CommandHandler.kt
@@ -16,11 +16,13 @@ package net.integr.backbone.systems.command
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import net.integr.backbone.Backbone
+import net.integr.backbone.systems.command.help.HelpNode
import net.integr.backbone.text.formats.CommandFeedbackFormat
import org.bukkit.command.CommandMap
import org.jetbrains.annotations.ApiStatus
import java.awt.Color
import java.lang.reflect.Field
+import java.util.concurrent.ConcurrentHashMap
/**
* Handles the registration and unregistration of commands.
@@ -44,7 +46,7 @@ object CommandHandler {
@ApiStatus.Internal
val coroutineScope = CoroutineScope(Dispatchers.IO)
- private val map: CommandMap by lazy {
+ private val bukkitMap: CommandMap by lazy {
val bukkitCommandMap: Field = Backbone.SERVER.javaClass.getDeclaredField("commandMap")
bukkitCommandMap.isAccessible = true
val map = bukkitCommandMap.get(Backbone.SERVER) as CommandMap
@@ -52,6 +54,26 @@ object CommandHandler {
map
}
+ /**
+ * The map of registered commands by their full name.
+ *
+ * @since 1.3.0
+ */
+ @ApiStatus.Internal
+ val commands: ConcurrentHashMap = ConcurrentHashMap()
+
+ /**
+ * Get the help node for a command.
+ *
+ * @param command The command to get the help node for.
+ * @return The help node for the command, or null if not found.
+ * @since 1.3.0
+ */
+ fun getHelp(command: String): HelpNode? {
+ val found = commands[command] ?: return null
+ return found.helpNode
+ }
+
/**
* Register a command to the server.
*
@@ -61,12 +83,31 @@ object CommandHandler {
*/
fun register(command: Command, prefix: String = "backbone") {
command.build()
- map.register(prefix, command)
+ push(command)
+
+ bukkitMap.register(prefix, command)
Backbone.SERVER.onlinePlayers.forEach {
it.updateCommands()
}
}
+ private fun push(command: Command) {
+ commands[command.fullName!!] = command
+ command.subCommands.forEach {
+ push(it)
+ }
+ }
+
+ private fun pop(command: Command) {
+ command.subCommands.forEach {
+ pop(it)
+ }
+ commands.remove(command.fullName)
+ command.aliases.forEach { alias ->
+ commands.remove(alias)
+ }
+ }
+
/**
* Unregister a command from the server.
*
@@ -75,8 +116,8 @@ object CommandHandler {
* @since 1.0.0
*/
fun unregister(command: Command, prefix: String = "backbone") {
+ commands.remove(command.fullName)
unregisterCommand(command.name, prefix)
-
Backbone.SERVER.onlinePlayers.forEach {
it.updateCommands()
}
@@ -84,9 +125,9 @@ object CommandHandler {
private fun unregisterCommand(commandName: String, prefix: String = "backbone") {
try {
- val knownCommandsField = map.javaClass.getSuperclass().getDeclaredField("knownCommands")
+ val knownCommandsField = bukkitMap.javaClass.getSuperclass().getDeclaredField("knownCommands")
knownCommandsField.setAccessible(true)
- val knownCommands = knownCommandsField.get(map) as MutableMap<*, *>
+ val knownCommands = knownCommandsField.get(bukkitMap) as MutableMap<*, *>
knownCommands.remove(commandName)
knownCommands.remove("$prefix:$commandName")
diff --git a/src/main/kotlin/net/integr/backbone/systems/command/argument/Argument.kt b/src/main/kotlin/net/integr/backbone/systems/command/argument/Argument.kt
index f78e118..43f3578 100644
--- a/src/main/kotlin/net/integr/backbone/systems/command/argument/Argument.kt
+++ b/src/main/kotlin/net/integr/backbone/systems/command/argument/Argument.kt
@@ -13,6 +13,8 @@
package net.integr.backbone.systems.command.argument
+import net.integr.backbone.systems.command.help.HelpNode
+
/**
* Represents a command argument.
*
@@ -42,6 +44,16 @@ abstract class Argument(val name: String, val description: String) {
*/
abstract fun parse(current: ArgumentInput): ParseResult
+ /**
+ * Returns a string representation of the argument's help text.
+ * @return A string containing the argument's name and description.
+ *
+ * @since 1.3.0
+ */
+ fun getHelpText(): HelpNode.Content {
+ return HelpNode.Content(" $name - $description", HelpNode.Content.Type.LIST)
+ }
+
/**
* Represents a finished completion.
*
diff --git a/src/main/kotlin/net/integr/backbone/systems/command/help/HelpNode.kt b/src/main/kotlin/net/integr/backbone/systems/command/help/HelpNode.kt
new file mode 100644
index 0000000..30d3c98
--- /dev/null
+++ b/src/main/kotlin/net/integr/backbone/systems/command/help/HelpNode.kt
@@ -0,0 +1,160 @@
+/*
+ * Copyright © 2026 Integr
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.integr.backbone.systems.command.help
+
+import net.integr.backbone.systems.text.component
+import net.kyori.adventure.text.Component
+import java.awt.Color
+
+/**
+ * Represents a hierarchical node in the command help system.
+ *
+ * This class builds a tree structure that displays command information in a formatted, visually organized manner.
+ * Each node can contain content (descriptions, arguments, aliases) and child nodes (subcommands).
+ * The tree structure is rendered with proper indentation, tree branches, and color coding for easy readability.
+ *
+ * @param title The title of this help node (e.g., command name).
+ * @param contents A list of content items to display under the title.
+ * @param children A list of child nodes representing subcommands.
+ * @since 1.3.0
+ */
+class HelpNode(val title: String, val contents: List, val children: List) {
+
+ /**
+ * Builds a formatted text component representation of this help node and its entire tree.
+ *
+ * This method recursively renders the help node as an Adventure text component with proper tree structure,
+ * indentation, color coding, and formatting. The tree is displayed with branch characters (├, └) and
+ * vertical lines (│) to show the hierarchy. Content sections are separated with visual separators (─).
+ *
+ * @param prefix The prefix string for indentation of nested nodes. Defaults to empty string for the root node.
+ * @param isLast Whether this node is the last child of its parent. Affects branch character selection.
+ * @param isRoot Whether this is the root node of the tree. Determines initial branch style.
+ * @return A formatted Component representing this help node and all its children.
+ * @since 1.3.0
+ */
+ fun buildComponent(prefix: String = "", isLast: Boolean = true, isRoot: Boolean = true): Component {
+ val titleColor = Color(141, 184, 130)
+ val barColor = Color(100, 120, 90) // Dark green for tree bars
+
+ // Tree structure branches
+ val branch = if (isRoot) "» " else if (isLast) "└ » " else "├ » "
+
+ // Continuation for nested children
+ val childPrefix = if (isRoot) "" else prefix + if (isLast) " " else "│ "
+
+ // Content prefix - vertical bar with separator/content
+ val contentPrefix = if (isRoot) "│ " else "$childPrefix│ "
+ val verticalBar = if (isRoot) "│" else "$childPrefix│"
+
+ return component {
+ // Title
+ append(prefix) { color(barColor) }
+ append(branch) { color(barColor) }
+ append("$title\n") { color(titleColor) }
+
+ // Separator line
+ append(contentPrefix + "─".repeat(10) + "\n") { color(barColor) }
+
+ // Contents
+ val hasChildren = children.isNotEmpty()
+ val lastContentIndex = contents.size - 1
+
+ contents.forEachIndexed { idx, content ->
+ if (content.type == Content.Type.TITLE) {
+ append("$verticalBar\n") { color(barColor) }
+ }
+
+ val isLastContent = idx == lastContentIndex && !hasChildren
+ val contentBranch = if (isLastContent) "└ " else "│ "
+ val contentLine = if (isRoot) contentBranch else childPrefix + contentBranch
+
+ append(contentLine) { color(barColor) }
+ append("$content\n") { color(content.getColor()) }
+ }
+
+ // Children with tree structure - parent manages ALL spacing
+ if (children.isNotEmpty()) {
+ // Blank line after content, before first child
+ append("$verticalBar\n") { color(barColor) }
+
+ children.forEachIndexed { index, child ->
+ val childIsLast = index == children.size - 1
+ append(child.buildComponent(childPrefix, childIsLast, isRoot = false))
+
+ // Add a blank line after each child (except the last)
+ if (!childIsLast) {
+ append("$verticalBar\n") { color(barColor) }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Represents a single content item within a help node.
+ *
+ * Content items can be text descriptions, section titles, or list items. Each content item
+ * is rendered with appropriate formatting and color based on its type.
+ *
+ * @param text The text content to display.
+ * @param type The type of content, determining how it's formatted and colored.
+ * @since 1.3.0
+ */
+ class Content(val text: String, val type: Type) {
+ /**
+ * Returns a string representation of this content item.
+ *
+ * Formatting depends on the content type:
+ * - TEXT and TITLE are returned as-is
+ * - LIST items are prefixed with a bullet point (•)
+ *
+ * @return The formatted string representation of this content.
+ * @since 1.3.0
+ */
+ override fun toString(): String {
+ return when (type) {
+ Type.TEXT, Type.TITLE -> text
+ Type.LIST -> " • $text"
+ }
+ }
+
+ /**
+ * Gets the display color for this content item based on its type.
+ *
+ * @return A Color object representing the appropriate color for this content type.
+ * @since 1.3.0
+ */
+ fun getColor(): Color {
+ return when (type) {
+ Type.TEXT, Type.LIST -> Color(169, 173, 168)
+ Type.TITLE -> Color(141, 184, 130)
+ }
+ }
+
+ /**
+ * Enumeration of possible content types.
+ *
+ * @since 1.3.0
+ */
+ enum class Type {
+ /** Regular text content such as descriptions. */
+ TEXT,
+ /** List item content, typically used for arguments or aliases. */
+ LIST,
+ /** Section title content that groups related items. */
+ TITLE
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/net/integr/backbone/systems/dialog/Dialog.kt b/src/main/kotlin/net/integr/backbone/systems/dialog/Dialog.kt
new file mode 100644
index 0000000..9d49813
--- /dev/null
+++ b/src/main/kotlin/net/integr/backbone/systems/dialog/Dialog.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright © 2026 Integr
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+@file:Suppress("UnstableApiUsage")
+
+package net.integr.backbone.systems.dialog
+
+import io.papermc.paper.dialog.Dialog
+import io.papermc.paper.dialog.DialogResponseView
+import io.papermc.paper.registry.RegistryBuilderFactory
+import io.papermc.paper.registry.data.dialog.DialogBase
+import io.papermc.paper.registry.data.dialog.DialogRegistryEntry
+import io.papermc.paper.registry.data.dialog.action.DialogAction
+import io.papermc.paper.registry.data.dialog.type.DialogType
+import net.integr.backbone.Utils
+import net.kyori.adventure.audience.Audience
+import net.kyori.adventure.text.Component
+import net.kyori.adventure.text.event.ClickCallback
+import java.util.function.Consumer
+
+
+/**
+ * Builds a Paper [Dialog] using a DSL.
+ *
+ * Example usage:
+ * ```kotlin
+ * dialog {
+ * base(component { append("My Dialog") }) {
+ * // configure base properties
+ * }
+ * type(myDialogType)
+ * }
+ * ```
+ *
+ * @param block The DSL block to construct the dialog.
+ * @return The built [Dialog].
+ * @since 1.4.0
+ */
+fun dialog(block: DialogBuilder.() -> Unit): Dialog {
+ val con = Consumer { builder: RegistryBuilderFactory