diff --git a/README.md b/README.md index 6923375..e3fea0b 100644 --- a/README.md +++ b/README.md @@ -16,15 +16,14 @@ - [ Features](#-features) - [ How to Use](#-how-to-use) - [ Adding the API](#-adding-the-api) - - [ API Initialization](#-api-initialization) + - [ Listening for Dialog Interactions](#-listening-for-dialog-interactions) - [ Building a Simple Dialog](#-building-a-simple-dialog) - [ DialogData](#-dialog-data) - [ Adding Buttons with Actions](#-adding-buttons-with-actions) - [ Creating the Dialog](#-creating-the-dialog) - [ Opening a Dialog](#-opening-a-dialog) - [ Creating Custom Actions](#creating-custom-actions) - - [ Registering an Action](#registering-an-action) - - [ Action Implementation](#-action-implementation) + - [ Action and Input Handling](#-action-and-input-handling) - [ Input Readers](#-input-readers) - [ Creating Input Fields](#-creating-input-fields) - [ Looking for More Examples?](#-looking-for-more-examples) @@ -37,12 +36,31 @@ ## ✨ Features + - Kotlin-first builder pattern for intuitive dialog creation. - Supports all Vanilla dialog types: `MultiAction`, `List`, `Links`, `Notice`. - Custom actions via Mojang’s native `ServerboundCustomClickActionPacket` system. - Robust input reading: text, numbers (including ranges), multiline text, boolean toggles, and single-choice options. - Flexible dialog bodies: plain text messages and item displays. - Easy integration with Paper events and plugin lifecycle. +- > 💡 **New** ViaDialog + +--- + + +## 🤔 What is ViaDialog? + +`ViaDialog` is a compatibility layer within DialogAPI designed to deliver a functional, albeit simplified, dialog experience to players on older Minecraft clients (pre-1.21.6). + +When DialogAPI detects that a player cannot receive the native dialog, it seamlessly switches to using `ViaDialog`. This module translates the dialog's structure—buttons, text, and inputs—into a familiar inventory interface. + +### How It Works: + +* **Inventory as UI**: Buttons are represented as items in a chest. +* **Anvil for Input**: For text input, players are prompted with an Anvil GUI. +* **Packet Handling**: It uses packet listeners to capture player interactions and translate them back into the standard `DialogAPI` events. + +While this fallback doesn't replicate the native look and feel, it ensures that your dialog-based features remain accessible to a wider range of players. --- @@ -58,27 +76,40 @@ repositories { } dependencies { - implementation("com.github.AlepandoCR:DialogAPI:v1.2.0") + implementation("com.github.AlepandoCR:DialogAPI:Tag") } ``` > 💡 **Tip:** Always check [JitPack](https://jitpack.io/#AlepandoCR/DialogAPI) or the [GitHub Releases page](https://github.com/AlepandoCR/DialogAPI/releases) for the latest version number. -### 🔧 API Initialization - -Call this function in your plugin's `onEnable` method to initialize the API internals and register the required packet listeners. This enables features like `CustomActions` and `InputReaders`, which rely on packet-level data. - -> ⚠️ **Crucial Step:** If you don't call `DialogApi.initialize(this)`, dialogs will still open and render correctly, but essential features like button actions (`CustomAction`) and input field processing (`InputReader`) **will not function**. +# 👂 Listening for Dialog Interactions +To respond to user interactions with your dialogs, you need to listen for PlayerDialogInteractionEvent. This is how you hook into the DialogAPI event system: ```kotlin // In your main plugin class override fun onEnable() { - // Initialize DialogAPI, passing your plugin instance - DialogApi.initialize(this) - - // Your other onEnable logic... - logger.info("MyPlugin has been enabled and DialogAPI is initialized!") +DialogApi.initialize() // ✅ Required: Initializes DialogAPI +server.pluginManager.registerEvents(DialogListener(), this) // 🔄 Register your listener +logger.info("MyPlugin has been enabled and the DialogAPI listener is registered!") +} +Next, implement the listener to handle dialog interactions: +``` +```kotlin +// Your listener class +class DialogListener : Listener { + + @EventHandler + fun onDialogInteraction(event: PlayerDialogInteractionEvent) { + val player = event.player + + // 🧠 Your logic here! + // Example: give player a reward or open another GUI + player.sendMessage("You interacted with: ${event.id}") + + // 📌 See the "Creating Custom Actions" section to define your own behavior + } } ``` +This listener is essential if you want to create dynamic responses, chain dialogs, or implement custom logic when players interact with GUI buttons created using DialogAPI. --- @@ -182,7 +213,7 @@ val testButton = Button( Optional.of(KeyedAction(resourceLocation)) // Associates the button with the custom action ) ``` -> ℹ️ **Note:** For this button to work, you'll need to register a `CustomAction` with the `resourceLocation` defined above. See the [ Registering an Action](#️-registering-an-action) section for details. +> ℹ️ **Note:** The `resourceLocation` is used to identify the button in the `PlayerDialogInteractionEvent` listener. ### 🧱 Creating the Dialog @@ -301,77 +332,51 @@ DialogAPI also supports more specialized dialog types for advanced use cases: ## Creating Custom Actions -Custom actions are the heart of interactive dialogs. They allow you to execute specific server-side logic when a player interacts with a dialog element (e.g., clicks a button). +Custom actions are the heart of interactive dialogs. They allow you to execute specific server-side logic when a player interacts with a dialog element. This is now handled within the `PlayerDialogInteractionEvent` listener. -### Registering an Action +### Action and Input Handling -First, you need to register your custom action with a unique `ResourceLocation` key. This key is crucial as it links the client-side dialog interaction (like a button click) to your server-side `CustomAction` implementation. +In your `PlayerDialogInteractionEvent` listener, you can check the `id` of the interaction and then execute a `CustomAction` or read input using an `InputReader`. ```kotlin -val killPlayerNamespace = "dialog" // Example namespace -val killPlayerPath = "damage_player" // Example path -val killPlayerKey = ResourceLocation(killPlayerNamespace, killPlayerPath) - -try { - CustomKeyRegistry.register( - killPlayerKey, // The unique key for this action - KillPlayerAction, // Your CustomAction implementation (see below) - PlayerReturnValueReader // Your InputReader implementation, you can also register an action without the Reader, although you might not be able to register the reader to the same key later on - ) -} catch (e: IllegalStateException) { - // Handle cases where the key might already be registered - // This message is good for debugging during development - player.sendMessage("Note: Kill player key was already registered, perhaps by another part of your plugin or a different plugin: ${e.message}") -} -``` -> 💡 **Best Practice:** Register all your custom keys during your plugin's `onEnable` phase to ensure they are available when needed and to handle any registration conflicts early. +class DialogListener : Listener { -### ⚙️ Action Implementation - -Create a class (or object for singletons) that extends `CustomAction` and implement the `task` method. This method contains the server-side logic that will be executed when the action is triggered. - -```kotlin -object KillPlayerAction : CustomAction() { - override fun task(player: Player, plugin: Plugin) { - // Optional: Start a dynamic listener if needed for this action - dynamicListener?.start() - // Optional: Stop the dynamic listener after a delay or when the action is complete - dynamicListener?.stopListenerAfter(20L) // Time is based ticks + @EventHandler + fun onDialogInteraction(event: PlayerDialogInteractionEvent) { + val player = event.player + val plugin = event.plugin + val resourceLocation = ResourceLocation("path","namespace") - player.damage(5.0) // Example action: damage the player - } - - // Optional: Define a Bukkit event listener specific to this action - // Custom listeners are not required for CustomActions, but it's an option. - // Listener also includes the player that triggered the action - override fun listener(dialogPlayer: Player): Listener { - return object : Listener { - @EventHandler - fun onPlayerDeath(event: PlayerDeathEvent) { - if (event.player == dialogPlayer) { - dialogPlayer.sendMessage("you died during your dialog") + if(event.id == resourceLocation){ + event.action(object : CustomAction() { + override fun task(player: Player, plugin: Plugin) { + player.sendMessage("Custom action executed!") } - } + }) } + + // also use PlayerDialogInteractionEvent#read with an InputReader } } ``` -> ℹ️ The `plugin: Plugin` parameter in `task` refers to your main plugin instance, allowing you to access plugin-specific resources or schedulers, this plugin instance is the same as the one you use to initialize DialogAPI +> ℹ️ The `plugin: Plugin` parameter in `task` refers to your main plugin instance, allowing you to access plugin-specific resources or schedulers. ### 📥 Input Readers -Input readers (`InputReader`) are essential when your dialog includes input fields (like text boxes, number inputs, etc.). They are responsible for processing the data submitted by the player from these fields. Each `CustomAction` that handles a dialog submission with inputs can have an associated `InputReader`. +Input readers (`InputReader`) are essential when your dialog includes input fields. They are responsible for processing the data submitted by the player. You use them within the `read()` method of the `PlayerDialogInteractionEvent`. ```kotlin -object PlayerReturnValueReader : InputReader { - // InputValueList offers a getter based on keys. - // Useful for getting specific values with the key set on Input creation (see below). +event.read(object : InputReader { override fun task(player: Player, values: InputValueList) { - for (input in values.list) { - player.sendMessage("Received input - Key: ${input.key}, Value: ${input.value}") // Corrected to show input.value - } + // InputValueList offers a getter based on keys. + val feedback = values.get("key") + val quantity = values.get("key") // Key as the same key set on InputBuilders (See bellow) + + if(feedback == null || quantity == null) return + + player.sendMessage("Feedback: $feedback, Quantity: $quantity") } -} +}) ``` ### ⌨️ Creating Input Fields @@ -498,10 +503,10 @@ The core builder and registry patterns (e.g., `CustomKeyRegistry`) are your main ## 🤝 Contributing - Contributors are welcome! If you'd like to help improve DialogAPI, please follow these steps: +Contributors are welcome! If you'd like to help improve DialogAPI, please follow these steps: 1. **🍴 Fork the Repository**: Click the 'Fork' button at the top right of the GitHub page. -2. **💻 Clone Your Fork**: `git clone https://github.com/YourUsername/DialogAPI.git` (Replace `YourUsername`) +2. **💻 Clone Your Fork**: `git clone https://github.com/AlepandoCR/DialogAPI.git` (Replace `AlepandoCR`) 3. **🌿 Create a Branch**: `git checkout -b feature/YourAmazingFeature` or `fix/IssueBeingFixed`. Descriptive branch names are helpful! 4. **✍️ Make Your Changes**: Implement your feature, bug fix, or documentation improvement. * Adhere to the existing code style (primarily Kotlin conventions). @@ -529,14 +534,7 @@ Your contributions, big or small, are highly appreciated and help make DialogAPI ## ❓ Troubleshooting / FAQ **Q: My dialog opens, but buttons with `CustomAction` or input fields don't work. What's wrong?** -A: ⚠️ The most common reason is forgetting to call `DialogApi.initialize(this)` in your plugin's `onEnable()` method. This step is **crucial** for registering the necessary packet listeners that handle custom actions and input data. Without it, the API won't process clicks or input submissions from the client. - -**Q: I'm getting an `IllegalStateException: Key [your_key] is already registered` when trying to register a `CustomKey`.** -A: This error means the `ResourceLocation` (the key, composed of a namespace and path, e.g., `yourplugin:some_action`) you're trying to use for your `CustomAction` is already in use. This could be by another part of your plugin, or rarely, a different plugin (if namespaces aren't unique). -* **Solution:** Ensure your `ResourceLocation` is unique. -* **Namespace:** Use your plugin's unique ID (e.g., `myplugin`). -* **Path:** Use a descriptive name for the action (e.g., `open_main_menu`, `submit_player_report`). -* You can defensively check if a key is registered before attempting to register it, or wrap the registration call in a try-catch block to handle this specific exception gracefully (perhaps logging a warning). +A: ⚠️ The most common reason is forgetting to initialize your `DialogApi.initialize()` in your plugin's `onEnable()` method. This step is **crucial** for handling custom actions and input data. Without it, the API won't process clicks or input submissions from the client. **Q: How do I choose a good `ResourceLocation` for my custom actions?** A: A `ResourceLocation` has two string parts: `namespace` and `path`. @@ -556,7 +554,7 @@ A: 🔗 The best places to check for the latest version are: **Q: My input fields (text, number, etc.) are not returning the values I expect in my `InputReader`. What should I check?** A: 🕵️‍♀️ Double-check these common points: 1. **Unique Input Keys:** Ensure that each input field within your dialog was created with a **unique `key` string** (e.g., `TextInputBuilder().key("player_name_input")`, `NumberRangeInputBuilder().key("item_quantity")`). This `key` is how you identify the input's value. -2. **Correct `InputReader` Registration:** Verify that the `CustomAction` responsible for handling the dialog submission is correctly registered with the appropriate `InputReader` instance in the `CustomKeyRegistry.register()` call. +2. **Correct `InputReader` Usage:** Verify that you are calling `event.read()` with the correct `InputReader` implementation inside your `PlayerDialogInteractionEvent` listener, and that you are checking for the correct `event.id`. 3. **Accessing Values in Reader:** In your `InputReader`'s `task(player: Player, values: InputValueList)` method, make sure you are using the correct key to retrieve the value: `values.getValue("your_exact_input_key")`. You can also iterate through `values.list` and check `input.key` and `input.value` for debugging. 4. **Data Types:** Ensure the type of value you're expecting matches what the input field provides (e.g., a `NumberRangeInput` will provide a number, a `BooleanInput` a boolean). @@ -571,8 +569,6 @@ You can find the full license text in the `LICENSE` file in the repository, but ``` MIT License -Copyright (c) [Year] [Your Name/Organization - AlepandoCR for DialogAPI] - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights diff --git a/src/main/kotlin/alepando/dev/dialogapi/DialogAPI.kt b/src/main/kotlin/alepando/dev/dialogapi/DialogAPI.kt index 8c75072..f149b16 100644 --- a/src/main/kotlin/alepando/dev/dialogapi/DialogAPI.kt +++ b/src/main/kotlin/alepando/dev/dialogapi/DialogAPI.kt @@ -2,6 +2,7 @@ package alepando.dev.dialogapi import alepando.dev.dialogapi.listeners.PlayerConnectionStatus import alepando.dev.dialogapi.listeners.ServerStatusListener +import alepando.dev.versionSupplier.packet.ClientVersionSniffer import org.bukkit.Bukkit import org.bukkit.plugin.Plugin import org.bukkit.plugin.PluginManager @@ -13,6 +14,8 @@ object DialogAPI { private var initialized = false + var plugin: Plugin? = null + /** * Initializes the Dialog API by registering necessary listeners and hooks. * @@ -22,10 +25,20 @@ object DialogAPI { if (initialized) return initialized = true + this.plugin = plugin + val pm: PluginManager = Bukkit.getPluginManager() + pm.registerEvents(ClientVersionSniffer,plugin) pm.registerEvents(PlayerConnectionStatus(plugin), plugin) pm.registerEvents(ServerStatusListener(plugin),plugin) } + fun log(vararg string: String){ + plugin?.let { + string.forEach { + plugin!!.logger.info(it) + } + } + } } diff --git a/src/main/kotlin/alepando/dev/dialogapi/body/types/PlainMessageDialogBody.kt b/src/main/kotlin/alepando/dev/dialogapi/body/types/PlainMessageDialogBody.kt index da0ce90..2b1f350 100644 --- a/src/main/kotlin/alepando/dev/dialogapi/body/types/PlainMessageDialogBody.kt +++ b/src/main/kotlin/alepando/dev/dialogapi/body/types/PlainMessageDialogBody.kt @@ -10,7 +10,7 @@ import net.minecraft.server.dialog.body.PlainMessage * * @property contents The contents of the dialog body. */ -class PlainMessageDialogBody(width: Int, private val contents: Component) : DialogBody(width) { +class PlainMessageDialogBody(width: Int, val contents: Component) : DialogBody(width) { /** * Converts this dialog body to its NMS equivalent. * @return The NMS equivalent of this dialog body. diff --git a/src/main/kotlin/alepando/dev/dialogapi/executor/CustomKeyRegistry.kt b/src/main/kotlin/alepando/dev/dialogapi/executor/CustomKeyRegistry.kt deleted file mode 100644 index 837b893..0000000 --- a/src/main/kotlin/alepando/dev/dialogapi/executor/CustomKeyRegistry.kt +++ /dev/null @@ -1,65 +0,0 @@ -package alepando.dev.dialogapi.executor - -import alepando.dev.dialogapi.factory.actions.CustomAction -import alepando.dev.dialogapi.packets.reader.InputReader -import alepando.dev.dialogapi.factory.data.ResourceLocation -import java.util.* - -typealias NMSResourceLocation = net.minecraft.resources.ResourceLocation -/** - * Internal data class to hold a registered custom action and its corresponding input reader. - */ -internal data class KeyBinding(val action: CustomAction, val reader: Optional) - - - -/** - * A registry for developers to register custom actions and input readers, - * associating them with a unique ResourceLocation (namespace and path). - */ -object CustomKeyRegistry { - - private val registeredKeys = mutableMapOf() - - /** - * Registers a custom key with an associated action and input reader. - * - * @param resourceLocation location to be stored - * @param action The [CustomAction] to be executed when this key is triggered. - * @param reader The [InputReader] to process any input associated with this key. - * @throws IllegalArgumentException if the namespace or path contains invalid characters for ResourceLocation. - * @throws IllegalStateException if the key (namespace and path combination) is already registered. - */ - fun register(resourceLocation: ResourceLocation, action: CustomAction, reader: Optional = Optional.empty()) { - val location = try { - resourceLocation.toNMS() - } catch (e: Exception) { - throw IllegalArgumentException("Invalid namespace or path: '${'$'}namespace:${'$'}path'. Ensure they contain valid characters.", e) - } - - if (registeredKeys.containsKey(location)) { - throw IllegalStateException("Custom key '${'$'}location' is already registered.") - } - registeredKeys[location] = KeyBinding(action, reader) - } - - /** - * Retrieves the [KeyBinding] (action and reader) for a given ResourceLocation. - * Intended for internal use by the dialog system (e.g., ReaderManager). - * - * @param location The [ResourceLocation] of the key. - * @return The [KeyBinding] if found, otherwise null. - */ - internal fun getBinding(location: NMSResourceLocation): KeyBinding? { - return registeredKeys[location] - } - - /** - * Clears all registered custom keys. - * Mainly intended for testing or server shutdown/plugin disable scenarios. - */ - fun clearAll() { - registeredKeys.clear() - - } -} diff --git a/src/main/kotlin/alepando/dev/dialogapi/executor/PlayerOpener.kt b/src/main/kotlin/alepando/dev/dialogapi/executor/PlayerOpener.kt index a411f02..9128361 100644 --- a/src/main/kotlin/alepando/dev/dialogapi/executor/PlayerOpener.kt +++ b/src/main/kotlin/alepando/dev/dialogapi/executor/PlayerOpener.kt @@ -1,6 +1,10 @@ package alepando.dev.dialogapi.executor +import alepando.dev.dialogapi.executor.events.PlayerOpenDialogEvent import alepando.dev.dialogapi.factory.Dialog +import alepando.dev.versionSupplier.VersionSupplier.getVersion +import alepando.dev.versionSupplier.packet.ClientVersionSniffer +import alepando.dev.viaDialog.inventory.DialogInventory import net.minecraft.core.Holder.Direct import org.bukkit.craftbukkit.entity.CraftPlayer import org.bukkit.entity.Player @@ -15,11 +19,19 @@ object PlayerOpener{ * @param dialog The dialog to open. */ fun Player.openDialog(dialog: Dialog) { + val protocolVersion = this.getVersion() +// if (protocolVersion < 765) { +// DialogInventory().parse(this, dialog) +// return +// } + val craftPlayer = player as CraftPlayer val nmsPlayer = craftPlayer.handle val holder = Direct(dialog.toNMS()) + PlayerOpenDialogEvent(this,dialog).callEvent() + nmsPlayer.openDialog(holder) } } diff --git a/src/main/kotlin/alepando/dev/dialogapi/executor/events/PlayerDialogInteractionEvent.kt b/src/main/kotlin/alepando/dev/dialogapi/executor/events/PlayerDialogInteractionEvent.kt new file mode 100644 index 0000000..756dc5a --- /dev/null +++ b/src/main/kotlin/alepando/dev/dialogapi/executor/events/PlayerDialogInteractionEvent.kt @@ -0,0 +1,35 @@ +package alepando.dev.dialogapi.executor.events + +import alepando.dev.dialogapi.DialogAPI +import alepando.dev.dialogapi.factory.actions.CustomAction +import alepando.dev.dialogapi.packets.parser.PayloadParser +import alepando.dev.dialogapi.packets.reader.InputReader +import net.minecraft.network.protocol.common.ServerboundCustomClickActionPacket +import org.bukkit.entity.Player +import org.bukkit.event.HandlerList +import org.bukkit.event.player.PlayerEvent +import org.bukkit.plugin.Plugin + +class PlayerDialogInteractionEvent(player: Player, packet: ServerboundCustomClickActionPacket, internal val plugin: Plugin = DialogAPI.plugin!!): PlayerEvent(player) { + + private val payload = PayloadParser.getValues(packet) + val id = packet.id + + override fun getHandlers(): HandlerList = handlerList + + fun read(reader:InputReader){ + reader.task(player,payload) + } + + fun action(action:CustomAction){ + action.execute(player,plugin) + } + + companion object { + @JvmStatic + private val handlerList = HandlerList() + + @JvmStatic + fun getHandlerList(): HandlerList = handlerList + } +} \ No newline at end of file diff --git a/src/main/kotlin/alepando/dev/dialogapi/executor/events/PlayerOpenDialogEvent.kt b/src/main/kotlin/alepando/dev/dialogapi/executor/events/PlayerOpenDialogEvent.kt new file mode 100644 index 0000000..2e34657 --- /dev/null +++ b/src/main/kotlin/alepando/dev/dialogapi/executor/events/PlayerOpenDialogEvent.kt @@ -0,0 +1,24 @@ +package alepando.dev.dialogapi.executor.events + +import alepando.dev.dialogapi.factory.Dialog +import alepando.dev.dialogapi.factory.actions.CustomAction +import alepando.dev.dialogapi.packets.parser.PayloadParser +import alepando.dev.dialogapi.packets.reader.InputReader +import net.minecraft.network.protocol.common.ServerboundCustomClickActionPacket +import org.bukkit.entity.Player +import org.bukkit.event.HandlerList +import org.bukkit.event.player.PlayerEvent +import org.bukkit.plugin.Plugin + +class PlayerOpenDialogEvent(player: Player, val dialog: Dialog): PlayerEvent(player) { + + override fun getHandlers(): HandlerList = handlerList + + companion object { + @JvmStatic + private val handlerList = HandlerList() + + @JvmStatic + fun getHandlerList(): HandlerList = handlerList + } +} \ No newline at end of file diff --git a/src/main/kotlin/alepando/dev/dialogapi/factory/button/Button.kt b/src/main/kotlin/alepando/dev/dialogapi/factory/button/Button.kt index 491cb2e..423e200 100644 --- a/src/main/kotlin/alepando/dev/dialogapi/factory/button/Button.kt +++ b/src/main/kotlin/alepando/dev/dialogapi/factory/button/Button.kt @@ -1,11 +1,12 @@ package alepando.dev.dialogapi.factory.button import alepando.dev.dialogapi.factory.Wrapper -import alepando.dev.dialogapi.factory.button.data.KeyedAction import alepando.dev.dialogapi.factory.button.data.ButtonData +import alepando.dev.dialogapi.factory.button.data.KeyedAction import net.minecraft.server.dialog.ActionButton import java.util.* + /** * Represents a button in a dialog. * @@ -13,8 +14,8 @@ import java.util.* * @property action The action to perform when this button is clicked. */ class Button( - private val data: ButtonData, - private val action: Optional = Optional.empty() + val data: ButtonData, + val action: Optional = Optional.empty() ): Wrapper { /** * Converts this button to its NMS equivalent. @@ -24,4 +25,12 @@ class Button( if(action.isEmpty) return ActionButton(data.toNMS(), Optional.empty()) return ActionButton(data.toNMS(),action.get().toNMS()) } + + companion object { + fun fromNMS(button: ActionButton): Button { + val buttonData = ButtonData.fromNMS(button.button) + val keyedAction = if (button.action.isPresent) Optional.of(KeyedAction.fromNMS(button.action.get())) else Optional.empty() + return Button(buttonData, keyedAction) + } + } } \ No newline at end of file diff --git a/src/main/kotlin/alepando/dev/dialogapi/factory/button/data/ButtonData.kt b/src/main/kotlin/alepando/dev/dialogapi/factory/button/data/ButtonData.kt index 0674647..d9800ee 100644 --- a/src/main/kotlin/alepando/dev/dialogapi/factory/button/data/ButtonData.kt +++ b/src/main/kotlin/alepando/dev/dialogapi/factory/button/data/ButtonData.kt @@ -1,6 +1,7 @@ package alepando.dev.dialogapi.factory.button.data import alepando.dev.dialogapi.factory.Wrapper +import alepando.dev.dialogapi.util.Translator import net.minecraft.network.chat.Component import net.minecraft.server.dialog.CommonButtonData import java.util.* @@ -13,7 +14,7 @@ import java.util.* * @property tooltip The tooltip of the button. */ class ButtonData( - private val label: Component, + val label: Component, private val width: Int, private val tooltip: Optional = Optional.empty() ): Wrapper { @@ -24,4 +25,13 @@ class ButtonData( override fun toNMS(): CommonButtonData{ return CommonButtonData(label,tooltip,width) } + + companion object { + fun fromNMS(buttonData: CommonButtonData): ButtonData { + val label = buttonData.label + val width = buttonData.width + val tooltip = buttonData.tooltip + return ButtonData(label, width, tooltip) + } + } } \ No newline at end of file diff --git a/src/main/kotlin/alepando/dev/dialogapi/factory/button/data/DataContainer.kt b/src/main/kotlin/alepando/dev/dialogapi/factory/button/data/DataContainer.kt index 316d32c..a2d150a 100644 --- a/src/main/kotlin/alepando/dev/dialogapi/factory/button/data/DataContainer.kt +++ b/src/main/kotlin/alepando/dev/dialogapi/factory/button/data/DataContainer.kt @@ -1,5 +1,8 @@ package alepando.dev.dialogapi.factory.button.data +import alepando.dev.dialogapi.util.Translator.toPersistentDataContainer +import net.minecraft.nbt.CompoundTag +import org.bukkit.Bukkit import org.bukkit.NamespacedKey import org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer import org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry @@ -20,7 +23,7 @@ class DataContainer { /** * The underlying [PersistentDataContainer] where all key-value pairs are stored. */ - internal val container: PersistentDataContainer = create() + internal var container: PersistentDataContainer = create() /** * Adds a new key-value pair to the container. @@ -41,4 +44,12 @@ class DataContainer { private fun create(): PersistentDataContainer { return CraftPersistentDataContainer(CraftPersistentDataTypeRegistry()) } + + companion object { + fun fromNMS(tag: CompoundTag): DataContainer { + val dataContainer = DataContainer() + dataContainer.container = tag.toPersistentDataContainer(Bukkit.getPluginManager().plugins[0]) + return dataContainer + } + } } diff --git a/src/main/kotlin/alepando/dev/dialogapi/factory/button/data/KeyedAction.kt b/src/main/kotlin/alepando/dev/dialogapi/factory/button/data/KeyedAction.kt index 95d6953..162c8ee 100644 --- a/src/main/kotlin/alepando/dev/dialogapi/factory/button/data/KeyedAction.kt +++ b/src/main/kotlin/alepando/dev/dialogapi/factory/button/data/KeyedAction.kt @@ -14,7 +14,7 @@ import java.util.* * @property resourceLocation The resource location identifying this action. */ class KeyedAction( - private val resourceLocation: ResourceLocation, + val resourceLocation: ResourceLocation, private val additions: Optional ):Wrapper> { @@ -25,7 +25,17 @@ class KeyedAction( * @return An [Optional] containing the NMS equivalent of this action. */ override fun toNMS(): Optional { - return Optional.of(CustomAll(resourceLocation.toNMS(), Optional.of(additions.get().container.toCompoundTag()))) + if(additions.isPresent) return Optional.of(CustomAll(resourceLocation.toNMS(), Optional.of(additions.get().container.toCompoundTag()))) + return Optional.of(CustomAll(resourceLocation.toNMS(), Optional.empty())) + } + + companion object { + fun fromNMS(action: Action): KeyedAction { + val customAction = action as CustomAll + val resourceLocation = ResourceLocation.fromNMS(customAction.id) + val additions = if (customAction.additions.isPresent) Optional.of(DataContainer.fromNMS(customAction.additions.get())) else Optional.empty() + return KeyedAction(resourceLocation, additions) + } } } \ No newline at end of file diff --git a/src/main/kotlin/alepando/dev/dialogapi/factory/data/DialogData.kt b/src/main/kotlin/alepando/dev/dialogapi/factory/data/DialogData.kt index ac4582e..9a3fc9b 100644 --- a/src/main/kotlin/alepando/dev/dialogapi/factory/data/DialogData.kt +++ b/src/main/kotlin/alepando/dev/dialogapi/factory/data/DialogData.kt @@ -31,13 +31,13 @@ typealias NMSInput = net.minecraft.server.dialog.Input * @property inputs A list of [Input] elements allowing user interaction. */ class DialogData internal constructor( - private val title: Component, + val title: Component, private val externalTitle: Optional, private val canCloseWithEscape: Boolean, private val pause: Boolean, private val afterAction: DialogAction, - private val dialogBody: List>, - private val inputs: List>, + val dialogBody: MutableList>, + val inputs: MutableList>, ): Wrapper { /** diff --git a/src/main/kotlin/alepando/dev/dialogapi/factory/data/ResourceLocation.kt b/src/main/kotlin/alepando/dev/dialogapi/factory/data/ResourceLocation.kt index c400d2f..de60211 100644 --- a/src/main/kotlin/alepando/dev/dialogapi/factory/data/ResourceLocation.kt +++ b/src/main/kotlin/alepando/dev/dialogapi/factory/data/ResourceLocation.kt @@ -1,11 +1,11 @@ package alepando.dev.dialogapi.factory.data import alepando.dev.dialogapi.factory.Wrapper -import net.minecraft.resources.ResourceLocation +import net.minecraft.resources.ResourceLocation as NMSResourceLocation /** * Represents a resource location, consisting of a namespace and a path. - * This is a wrapper around Minecraft's [ResourceLocation]. + * This is a wrapper around Minecraft's [NMSResourceLocation]. * * @property namespace The namespace of the resource location. * @property path The path of the resource location. @@ -13,10 +13,16 @@ import net.minecraft.resources.ResourceLocation class ResourceLocation( private val namespace: String, private val path: String -): Wrapper { +): Wrapper { /** * Converts this resource location to its NMS equivalent. - * @return The NMS [ResourceLocation]. + * @return The NMS [NMSResourceLocation]. */ - override fun toNMS(): ResourceLocation { return ResourceLocation.fromNamespaceAndPath(namespace,path) } + override fun toNMS(): NMSResourceLocation { return NMSResourceLocation.fromNamespaceAndPath(namespace,path) } + + companion object { + fun fromNMS(location: NMSResourceLocation): ResourceLocation { + return ResourceLocation(location.namespace, location.path) + } + } } \ No newline at end of file diff --git a/src/main/kotlin/alepando/dev/dialogapi/packets/PacketBuilder.kt b/src/main/kotlin/alepando/dev/dialogapi/packets/PacketBuilder.kt new file mode 100644 index 0000000..2964532 --- /dev/null +++ b/src/main/kotlin/alepando/dev/dialogapi/packets/PacketBuilder.kt @@ -0,0 +1,25 @@ +package alepando.dev.dialogapi.packets + +import alepando.dev.dialogapi.DialogAPI +import alepando.dev.dialogapi.factory.data.ResourceLocation +import alepando.dev.dialogapi.packets.parser.PayloadParser.toCompoundTag +import alepando.dev.dialogapi.util.InputValueList +import net.minecraft.network.protocol.common.ServerboundCustomClickActionPacket +import java.util.* + +object PacketBuilder { + + fun build(resourceLocation: ResourceLocation, inputValues: InputValueList): ServerboundCustomClickActionPacket { + inputValues.list.forEach { + val key = it.key + val text = it.value + + DialogAPI.log("building Packet value : key=$key : text=$text") + } + val compoundTag = inputValues.toCompoundTag() + return ServerboundCustomClickActionPacket( + resourceLocation.toNMS(), + Optional.of(compoundTag) + ) + } +} diff --git a/src/main/kotlin/alepando/dev/dialogapi/packets/PacketSniffer.kt b/src/main/kotlin/alepando/dev/dialogapi/packets/PacketSniffer.kt index ba666e8..459564c 100644 --- a/src/main/kotlin/alepando/dev/dialogapi/packets/PacketSniffer.kt +++ b/src/main/kotlin/alepando/dev/dialogapi/packets/PacketSniffer.kt @@ -1,6 +1,6 @@ package alepando.dev.dialogapi.packets -import alepando.dev.dialogapi.packets.reader.ReaderManager +import alepando.dev.dialogapi.executor.events.PlayerDialogInteractionEvent import io.netty.channel.ChannelDuplexHandler import io.netty.channel.ChannelHandlerContext import net.minecraft.network.Connection @@ -42,8 +42,7 @@ internal object PacketSniffer { val handler = object : ChannelDuplexHandler() { override fun channelRead(ctx: ChannelHandlerContext, msg: Any) { if (msg is ServerboundCustomClickActionPacket) { - ReaderManager.peekActions(player, msg, plugin) - ReaderManager.peekInputs(player, msg) + PlayerDialogInteractionEvent(player,msg,plugin).callEvent() } super.channelRead(ctx, msg) } diff --git a/src/main/kotlin/alepando/dev/dialogapi/packets/parser/PayloadParser.kt b/src/main/kotlin/alepando/dev/dialogapi/packets/parser/PayloadParser.kt index 02d88be..94d2cfe 100644 --- a/src/main/kotlin/alepando/dev/dialogapi/packets/parser/PayloadParser.kt +++ b/src/main/kotlin/alepando/dev/dialogapi/packets/parser/PayloadParser.kt @@ -1,5 +1,6 @@ package alepando.dev.dialogapi.packets.parser +import alepando.dev.dialogapi.DialogAPI import alepando.dev.dialogapi.util.InputValue import alepando.dev.dialogapi.util.InputValueList import net.minecraft.nbt.* @@ -27,24 +28,23 @@ internal object PayloadParser { val compound = payloadHolder.get() as? CompoundTag ?: return list - for(key in compound.keySet()){ + for (key in compound.keySet()) { val tag = compound.get(key) ?: continue - val type = getTypedValue(tag) ?: continue - val data = type.get() ?: continue - - list.add(InputValue(data,key)) + var value = fromTag(tag) + if (value != null) { + if(value is Optional<*>){ + if(value.isPresent) value = value.get()!! + } + list.add(InputValue(value, key)) + } else { + DialogAPI.log("Unknown NBT tag type: ${tag.id} for key $key") + } } return list } - /** - * Converts an NBT [Tag] to its corresponding Java type, wrapped in an [Optional]. - * - * @param tag The NBT [Tag] to convert. - * @return An [Optional] containing the converted value, or null if the tag type is unknown. - */ - private fun getTypedValue(tag: Tag): Optional<*>? { + private fun fromTag(tag: Tag): Any? { return when (tag.id.toInt()) { 1 -> (tag as ByteTag).asByte() 2 -> (tag as ShortTag).asShort() @@ -52,16 +52,74 @@ internal object PayloadParser { 4 -> (tag as LongTag).asLong() 5 -> (tag as FloatTag).asFloat() 6 -> (tag as DoubleTag).asDouble() - 7 -> (tag as ByteArrayTag).asByteArray() + 7 -> (tag as ByteArrayTag).asByteArray 8 -> (tag as StringTag).asString() - 9 -> (tag as ListTag).asList() - 10 -> (tag as CompoundTag).asCompound() - 11 -> (tag as IntArrayTag).asIntArray() - 12 -> (tag as LongArrayTag).asLongArray() - else -> { - Bukkit.getLogger().warning("Unknown NBT tag type: ${tag.id}") - null + 9 -> { + val listTag = tag as ListTag + val list = mutableListOf() + for (element in listTag) { + list.add(fromTag(element)) + } + list + } + 10 -> { + val compoundTag = tag as CompoundTag + val map = mutableMapOf() + for (key in compoundTag.keySet()) { + compoundTag.get(key)?.let { map[key] = fromTag(it) } + } + map + } + 11 -> (tag as IntArrayTag).asIntArray + 12 -> (tag as LongArrayTag).asLongArray + else -> null + } + } + + fun InputValueList.toCompoundTag(): CompoundTag { + val compound = CompoundTag() + for (value in this.list) { + val tag = toTag(value.value) + if (tag != null) { + compound.put(value.key, tag) + } else { + Bukkit.getLogger().warning("Unsupported input value type: ${value.value.javaClass.simpleName} for key ${value.key}") + } + } + return compound + } + + private fun toTag(value: Any): Tag? { + return when (value) { + is Byte -> ByteTag.valueOf(value) + is Short -> ShortTag.valueOf(value) + is Int -> IntTag.valueOf(value) + is Long -> LongTag.valueOf(value) + is Float -> FloatTag.valueOf(value) + is Double -> DoubleTag.valueOf(value) + is String -> StringTag.valueOf(value) + is ByteArray -> ByteArrayTag(value) + is IntArray -> IntArrayTag(value) + is LongArray -> LongArrayTag(value) + is List<*> -> { + val listTag = ListTag() + value.forEach { item -> + if (item != null) { + toTag(item)?.let { listTag.add(it) } + } + } + listTag + } + is Map<*, *> -> { + val compoundTag = CompoundTag() + value.forEach { (key, value) -> + if (key is String && value != null) { + toTag(value)?.let { compoundTag.put(key, it) } + } + } + compoundTag } + else -> null } } diff --git a/src/main/kotlin/alepando/dev/dialogapi/packets/reader/ReaderManager.kt b/src/main/kotlin/alepando/dev/dialogapi/packets/reader/ReaderManager.kt deleted file mode 100644 index 83ae37c..0000000 --- a/src/main/kotlin/alepando/dev/dialogapi/packets/reader/ReaderManager.kt +++ /dev/null @@ -1,48 +0,0 @@ -package alepando.dev.dialogapi.packets.reader - -import alepando.dev.dialogapi.executor.CustomKeyRegistry // Import new registry -import alepando.dev.dialogapi.packets.parser.PayloadParser -import net.minecraft.network.protocol.common.ServerboundCustomClickActionPacket -import org.bukkit.entity.Player -import org.bukkit.plugin.Plugin - -/** - * Internal object responsible for managing the processing of incoming dialog packets. - * It uses the [CustomKeyRegistry] to find the appropriate action or input reader - * based on the packet's ID. - */ -internal object ReaderManager { - - /** - * Processes a [ServerboundCustomClickActionPacket] for input data. - * It retrieves the corresponding [InputReader] from the [CustomKeyRegistry] - * and executes its task with the parsed payload. - * - * @param player The player who sent the packet. - * @param packet The packet to process. - */ - fun peekInputs(player: Player, packet: ServerboundCustomClickActionPacket) { - val binding = CustomKeyRegistry.getBinding(packet.id) - val inputReader = binding?.reader - if (inputReader != null) { - if(inputReader.isPresent){ - binding.reader.get().task(player, PayloadParser.getValues(packet)) - } - } - } - - /** - * Processes a [ServerboundCustomClickActionPacket] for actions. - * It retrieves the corresponding [CustomAction] from the [CustomKeyRegistry] - * and executes it. - * - * @param player The player who sent the packet. - * @param packet The packet to process. - * @param plugin The plugin instance, required for executing the action. - */ - fun peekActions(player: Player, packet: ServerboundCustomClickActionPacket, plugin: Plugin) { - val binding = CustomKeyRegistry.getBinding(packet.id) - binding?.action?.execute(player, plugin) - } - -} diff --git a/src/main/kotlin/alepando/dev/dialogapi/packets/reader/types/PlayerReturnValueReader.kt b/src/main/kotlin/alepando/dev/dialogapi/packets/reader/types/PlayerReturnValueReader.kt index 063c574..0c9d94f 100644 --- a/src/main/kotlin/alepando/dev/dialogapi/packets/reader/types/PlayerReturnValueReader.kt +++ b/src/main/kotlin/alepando/dev/dialogapi/packets/reader/types/PlayerReturnValueReader.kt @@ -22,7 +22,7 @@ object PlayerReturnValueReader: InputReader { */ override fun task(player: Player, values: InputValueList) { for (input in values.list) { - player.sendMessage("${input.key}: ${input.key}") + player.sendMessage("${input.key}: ${input.value}") } } } \ No newline at end of file diff --git a/src/main/kotlin/alepando/dev/dialogapi/types/ConfirmationDialog.kt b/src/main/kotlin/alepando/dev/dialogapi/types/ConfirmationDialog.kt index f777ebf..49d0d56 100644 --- a/src/main/kotlin/alepando/dev/dialogapi/types/ConfirmationDialog.kt +++ b/src/main/kotlin/alepando/dev/dialogapi/types/ConfirmationDialog.kt @@ -22,8 +22,8 @@ typealias NMSConfirmationDialog = net.minecraft.server.dialog.ConfirmationDialog * @param dynamicListener An optional [DynamicListener] for handling Bukkit events related to this dialog. */ class ConfirmationDialog( - private val yesButton: ActionButton, - private val noButton: ActionButton, data: DialogData, + val yesButton: ActionButton, + val noButton: ActionButton, data: DialogData, dynamicListener: Optional = Optional.empty() ): Dialog(data, dynamicListener){ diff --git a/src/main/kotlin/alepando/dev/dialogapi/types/LinksDialog.kt b/src/main/kotlin/alepando/dev/dialogapi/types/LinksDialog.kt index d3bc689..4b5e9c3 100644 --- a/src/main/kotlin/alepando/dev/dialogapi/types/LinksDialog.kt +++ b/src/main/kotlin/alepando/dev/dialogapi/types/LinksDialog.kt @@ -20,7 +20,7 @@ typealias NMSServerLinksDialog = ServerLinksDialog */ class LinksDialog( data: DialogData, - private val exitButton: Optional