diff --git a/SpigotCore/SpigotCore_Main/build.gradle.kts b/SpigotCore/SpigotCore_Main/build.gradle.kts index 322b1046..13fe0e40 100644 --- a/SpigotCore/SpigotCore_Main/build.gradle.kts +++ b/SpigotCore/SpigotCore_Main/build.gradle.kts @@ -59,8 +59,6 @@ dependencies { compileOnly(libs.netty) compileOnly(libs.brigadier) compileOnly(libs.fastutil) - - implementation(libs.anvilgui) } widener { diff --git a/SpigotCore/SpigotCore_Main/src/net/wesjd/anvilgui/AnvilGUI.java b/SpigotCore/SpigotCore_Main/src/net/wesjd/anvilgui/AnvilGUI.java new file mode 100644 index 00000000..d2d91852 --- /dev/null +++ b/SpigotCore/SpigotCore_Main/src/net/wesjd/anvilgui/AnvilGUI.java @@ -0,0 +1,922 @@ +package net.wesjd.anvilgui; + +import lombok.NonNull; +import net.md_5.bungee.api.chat.BaseComponent; +import net.wesjd.anvilgui.version.VersionWrapper; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.HandlerList; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.*; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.plugin.Plugin; + +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.logging.Level; + +/** + * An anvil gui, used for gathering a user's input + * + * @author Wesley Smith + * @since 1.0 + */ +public class AnvilGUI { + + /** + * The local {@link VersionWrapper} object for the server's version + */ + private static final VersionWrapper WRAPPER = VersionWrapper.INSTANCE; + + /** + * The variable containing an item with air. Used when the item would be null. + * To keep the heap clean, this object only gets iniziaised once + */ + private static final ItemStack AIR = new ItemStack(Material.AIR); + /** + * If the given ItemStack is null, return an air ItemStack, otherwise return the given ItemStack + * + * @param stack The ItemStack to check + * @return air or the given ItemStack + */ + private static ItemStack itemNotNull(ItemStack stack) { + return stack == null ? AIR : stack; + } + + /** + * The {@link Plugin} that this anvil GUI is associated with + */ + private final Plugin plugin; + /** + * The player who has the GUI open + */ + private final Player player; + /** + * An {@link Executor} that executes tasks on the main server thread + */ + private final Executor mainThreadExecutor; + /** + * The title of the anvil inventory + */ + private final Object titleComponent; + /** + * The initial contents of the inventory + */ + private final ItemStack[] initialContents; + /** + * A state that decides where the anvil GUI is able to get closed by the user + */ + private final boolean preventClose; + /** + * A state that decides whether compatibility with Geyser software is enabled + */ + private final boolean geyserCompatibility; + /** + * A set of slot numbers that are permitted to be interacted with by the user. An interactable + * slot is one that is able to be minipulated by the player, i.e. clicking and picking up an item, + * placing in a new one, etc. + */ + private final Set interactableSlots; + + /** An {@link Consumer} that is called when the anvil GUI is close */ + private final Consumer closeListener; + /** A flag that decides whether the async click handler can be run concurrently */ + private final boolean concurrentClickHandlerExecution; + /** An {@link BiFunction} that is called when a slot is clicked */ + private final ClickHandler clickHandler; + + /** + * The container id of the inventory, used for NMS methods + */ + private int containerId; + + /** + * The inventory that is used on the Bukkit side of things + */ + private Inventory inventory; + /** + * The listener holder class + */ + private final ListenUp listener = new ListenUp(); + + /** + * Represents the state of the inventory being open + */ + private boolean open; + + /** + * The actual container backing the Anvil GUI + */ + private VersionWrapper.AnvilContainerWrapper container; + + /** + * Create an AnvilGUI + * + * @param plugin A {@link org.bukkit.plugin.java.JavaPlugin} instance + * @param player The {@link Player} to open the inventory for + * @param mainThreadExecutor An {@link Executor} that executes on the main server thread + * @param titleComponent What to have the text already set to + * @param initialContents The initial contents of the inventory + * @param preventClose Whether to prevent the inventory from closing + * @param geyserCompatibility Whether to enable compatibility with Geyser software + * @param closeListener A {@link Consumer} when the inventory closes + * @param concurrentClickHandlerExecution Flag to allow concurrent execution of the click handler + * @param clickHandler A {@link ClickHandler} that is called when the player clicks a slot + */ + private AnvilGUI( + Plugin plugin, + Player player, + Executor mainThreadExecutor, + Object titleComponent, + ItemStack[] initialContents, + boolean preventClose, + boolean geyserCompatibility, + Set interactableSlots, + Consumer closeListener, + boolean concurrentClickHandlerExecution, + ClickHandler clickHandler) { + this.plugin = plugin; + this.player = player; + this.mainThreadExecutor = mainThreadExecutor; + this.titleComponent = titleComponent; + this.initialContents = initialContents; + this.preventClose = preventClose; + this.geyserCompatibility = geyserCompatibility; + this.interactableSlots = Collections.unmodifiableSet(interactableSlots); + this.closeListener = closeListener; + this.concurrentClickHandlerExecution = concurrentClickHandlerExecution; + this.clickHandler = clickHandler; + } + + /** + * Opens the anvil GUI + */ + private void openInventory() { + Bukkit.getPluginManager().registerEvents(listener, plugin); + + container = WRAPPER.newContainerAnvil(player, titleComponent); + + inventory = container.getBukkitInventory(); + // We need to use setItem instead of setContents because a Minecraft ContainerAnvil + // contains two separate inventories: the result inventory and the ingredients inventory. + // The setContents method only updates the ingredients inventory unfortunately, + // but setItem handles the index going into the result inventory. + for (int i = 0; i < initialContents.length; i++) { + inventory.setItem(i, initialContents[i]); + } + + containerId = WRAPPER.getNextContainerId(player, container); + WRAPPER.handleInventoryCloseEvent(player); + WRAPPER.sendPacketOpenWindow(player, containerId, titleComponent); + WRAPPER.setActiveContainer(player, container); + WRAPPER.setActiveContainerId(container, containerId); + WRAPPER.addActiveContainerSlotListener(container, player); + + // Primitive Geyser Check! + if (player.getName().startsWith(".")) { + WRAPPER.sendPacketExperienceChange(player, 20); + } + + open = true; + } + + /** + * Closes the inventory if it's open. + */ + public void closeInventory() { + closeInventory(true); + } + + /** + * Closes the inventory if it's open, only sending the close inventory packets if the arg is true + * + * @param sendClosePacket Whether to send the close inventory event, packet, etc + */ + private void closeInventory(boolean sendClosePacket) { + if (!open) { + return; + } + + open = false; + + HandlerList.unregisterAll(listener); + + if (sendClosePacket) { + WRAPPER.handleInventoryCloseEvent(player); + WRAPPER.setActiveContainerDefault(player); + WRAPPER.sendPacketCloseWindow(player, containerId); + } + // Primitive Geyser Check! + if (player.getName().startsWith(".")) { + WRAPPER.sendPacketExperienceChange(player, player.getLevel()); + } + + if (closeListener != null) { + closeListener.accept(StateSnapshot.fromAnvilGUI(this)); + } + } + + /** + * Updates the title of the AnvilGUI to the new one. + * + * @param literalTitle The title to use as literal text + * @param preserveRenameText Whether to preserve the entered rename text + * @throws IllegalArgumentException when literalTitle is null + * @see Builder#title(String) + */ + public void setTitle(@NonNull String literalTitle, boolean preserveRenameText) { + setTitle(WRAPPER.literalChatComponent(literalTitle), preserveRenameText); + } + + /** + * Updates the title of the AnvilGUI to the new one. + * + * @param json The json used to parse into a rich chat component + * @param preserveRenameText Whether to preserve the entered rename text + * @throws IllegalArgumentException when json is null + * @see Builder#jsonTitle(String) + */ + public void setJsonTitle(@NonNull String json, boolean preserveRenameText) { + setTitle(WRAPPER.jsonChatComponent(json), preserveRenameText); + } + + /** + * Updates the title of the AnvilGUI to the new one. + * + * @param title The title as a NMS ChatComponent + * @param preserveRenameText Whether to preserve the entered rename text + */ + private void setTitle(Object title, boolean preserveRenameText) { + if (!true) { + return; + } + String renameText = container.getRenameText(); + WRAPPER.sendPacketOpenWindow(player, containerId, title); + if (preserveRenameText) { + // The renameText field is marked as @Nullable in newer versions + container.setRenameText(renameText == null ? "" : renameText); + } + } + + /** + * Returns the Bukkit inventory for this anvil gui + * + * @return the {@link Inventory} for this anvil gui + */ + public Inventory getInventory() { + return inventory; + } + + /** + * Simply holds the listeners for the GUI + */ + private class ListenUp implements Listener { + + /** + * Boolean storing the running status of the latest click handler to prevent double execution. + * All accesses to this boolean will be from the main server thread, except for the rare event + * that the plugin is disabled and the mainThreadExecutor throws an exception + */ + private boolean clickHandlerRunning = false; + + @EventHandler + public void onInventoryClick(InventoryClickEvent event) { + if (!event.getInventory().equals(inventory)) { + return; + } + + final int rawSlot = event.getRawSlot(); + // ignore items dropped outside the window + if (rawSlot == -999) return; + + final Player clicker = (Player) event.getWhoClicked(); + final Inventory clickedInventory = event.getClickedInventory(); + + if (clickedInventory != null) { + if (clickedInventory.equals(clicker.getInventory())) { + // prevent players from merging items from the anvil inventory + if (event.getClick().equals(ClickType.DOUBLE_CLICK)) { + event.setCancelled(true); + return; + } + // prevent shift moving items from players inv to the anvil inventory + if (event.isShiftClick()) { + event.setCancelled(true); + return; + } + } + // prevent players from swapping items in the anvil gui + if ((event.getCursor() != null && event.getCursor().getType() != Material.AIR) + && !interactableSlots.contains(rawSlot) + && event.getClickedInventory().equals(inventory)) { + event.setCancelled(true); + return; + } + } + + if (rawSlot < 3 && rawSlot >= 0 || event.getAction().equals(InventoryAction.MOVE_TO_OTHER_INVENTORY)) { + event.setCancelled(!interactableSlots.contains(rawSlot)); + if (clickHandlerRunning && !concurrentClickHandlerExecution) { + // A click handler is running, don't launch another one + return; + } + + final CompletableFuture> actionsFuture = + clickHandler.apply(rawSlot, StateSnapshot.fromAnvilGUI(AnvilGUI.this)); + + final Consumer> actionsConsumer = actions -> { + for (final ResponseAction action : actions) { + action.accept(AnvilGUI.this, clicker); + } + }; + + if (actionsFuture.isDone()) { + // Fast-path without scheduling if clickHandler is performed in sync + // Because the future is already completed, .join() will not block the server thread + actionsFuture.thenAccept(actionsConsumer).join(); + } else { + clickHandlerRunning = true; + // If the plugin is disabled and the Executor throws an exception, the exception will be passed to + // the .handle method + actionsFuture + .thenAcceptAsync(actionsConsumer, mainThreadExecutor) + .handle((results, exception) -> { + if (exception != null) { + plugin.getLogger() + .log( + Level.SEVERE, + "An exception occurred in the AnvilGUI clickHandler", + exception); + } + // Whether an exception occurred or not, set running to false + clickHandlerRunning = false; + return null; + }); + } + } + } + + @EventHandler + public void onInventoryDrag(InventoryDragEvent event) { + if (event.getInventory().equals(inventory)) { + for (int slot : Slot.values()) { + if (event.getRawSlots().contains(slot)) { + event.setCancelled(!interactableSlots.contains(slot)); + break; + } + } + } + } + + @EventHandler + public void onInventoryClose(InventoryCloseEvent event) { + if (open && event.getInventory().equals(inventory)) { + closeInventory(false); + if (preventClose) { + mainThreadExecutor.execute(AnvilGUI.this::openInventory); + } + } + } + } + + /** A builder class for an {@link AnvilGUI} object */ + public static class Builder { + + /** An {@link Executor} that executes tasks on the main server thread */ + private Executor mainThreadExecutor; + /** An {@link Consumer} that is called when the anvil GUI is close */ + private Consumer closeListener; + /** A flag that decides whether the async click handler can be run concurrently */ + private boolean concurrentClickHandlerExecution = false; + /** An {@link Function} that is called when a slot in the inventory has been clicked */ + private ClickHandler clickHandler; + /** A state that decides where the anvil GUI is able to be closed by the user */ + private boolean preventClose = false; + /** A state that determines whether support for Geyser software is enabled */ + private boolean geyserCompatibility = true; + /** A set of integers containing the slot numbers that should be modifiable by the user. */ + private Set interactableSlots = Collections.emptySet(); + /** The {@link Plugin} that this anvil GUI is associated with */ + private Plugin plugin; + /** The text that will be displayed to the user */ + private Object titleComponent = WRAPPER.literalChatComponent("Repair & Name"); + /** The starting text on the item */ + private String itemText; + /** An {@link ItemStack} to be put in the left input slot */ + private ItemStack itemLeft; + /** An {@link ItemStack} to be put in the right input slot */ + private ItemStack itemRight; + /** An {@link ItemStack} to be placed in the output slot */ + private ItemStack itemOutput; + + /** + * Set a custom main server thread executor. Useful for plugins targeting Folia. + * + * @param executor The executor to run tasks on + * @return The {@link Builder} instance + * @throws IllegalArgumentException when the executor is null + */ + public Builder mainThreadExecutor(@NonNull Executor executor) { + this.mainThreadExecutor = executor; + return this; + } + + /** + * Prevents the closing of the anvil GUI by the user + * + * @return The {@link Builder} instance + */ + public Builder preventClose() { + preventClose = true; + return this; + } + + /** + * Disables compatibility with Geyser software + */ + public Builder disableGeyserCompat() { + geyserCompatibility = false; + return this; + } + + /** + * Permit the user to modify (take items in and out) the slot numbers provided. + * + * @param slots A varags param for the slot numbers. You can avoid relying on magic constants by using + * the {@link AnvilGUI.Slot} class. + * @return The {@link Builder} instance + */ + public Builder interactableSlots(int... slots) { + final Set newValue = new HashSet<>(); + for (int slot : slots) { + newValue.add(slot); + } + interactableSlots = newValue; + return this; + } + + /** + * Listens for when the inventory is closed + * + * @param closeListener An {@link Consumer} that is called when the anvil GUI is closed + * @return The {@link Builder} instance + * @throws IllegalArgumentException when the closeListener is null + */ + public Builder onClose(@NonNull Consumer closeListener) { + this.closeListener = closeListener; + return this; + } + + /** + * Do an action when a slot is clicked in the inventory + *

+ * The ClickHandler is only called when the previous execution of the ClickHandler has finished. + * To alter this behaviour use {@link #allowConcurrentClickHandlerExecution()} + * + * @param clickHandler A {@link ClickHandler} that is called when the user clicks a slot. The + * {@link Integer} is the slot number corresponding to {@link Slot}, the + * {@link StateSnapshot} contains information about the current state of the anvil, + * and the response is a {@link CompletableFuture} that will eventually return a + * list of {@link ResponseAction} to execute in the order that they are supplied. + * @return The {@link Builder} instance + * @throws IllegalArgumentException when the function supplied is null + */ + public Builder onClickAsync(@NonNull ClickHandler clickHandler) { + this.clickHandler = clickHandler; + return this; + } + + /** + * By default, the {@link #onClickAsync(ClickHandler) async click handler} will not run concurrently + * and instead wait for the previous {@link CompletableFuture} to finish before executing it again. + *

+ * If this trait is desired, it can be enabled by calling this method but may lead to inconsistent + * behaviour if not handled properly. + * + * @return The {@link Builder} instance + */ + public Builder allowConcurrentClickHandlerExecution() { + this.concurrentClickHandlerExecution = true; + return this; + } + + /** + * Do an action when a slot is clicked in the inventory + * + * @param clickHandler A {@link BiFunction} that is called when the user clicks a slot. The + * {@link Integer} is the slot number corresponding to {@link Slot}, the + * {@link StateSnapshot} contains information about the current state of the anvil, + * and the response is a list of {@link ResponseAction} to execute in the order + * that they are supplied. + * @return The {@link Builder} instance + * @throws IllegalArgumentException when the function supplied is null + */ + public Builder onClick(@NonNull BiFunction> clickHandler) { + this.clickHandler = + (slot, stateSnapshot) -> CompletableFuture.completedFuture(clickHandler.apply(slot, stateSnapshot)); + return this; + } + + /** + * Sets the plugin for the {@link AnvilGUI} + * + * @param plugin The {@link Plugin} this anvil GUI is associated with + * @return The {@link Builder} instance + * @throws IllegalArgumentException if the plugin is null + */ + public Builder plugin(@NonNull Plugin plugin) { + this.plugin = plugin; + return this; + } + + /** + * Sets the initial item-text that is displayed to the user. + *

+ * If the usage of Adventure Components is desired, you must create an item, set the displayname of it + * and put it into the AnvilGUI via {@link #itemLeft(ItemStack)} manually. + * + * @param text The initial name of the item in the anvil + * @return The {@link Builder} instance + * @throws IllegalArgumentException if the text is null + */ + public Builder text(@NonNull String text) { + this.itemText = text; + return this; + } + + /** + * Sets the AnvilGUI title that is to be displayed to the user. + *
+ * The provided title will be treated as literal text. + * + * @param title The title that is to be displayed to the user + * @return The {@link Builder} instance + * @throws IllegalArgumentException if the title is null + */ + public Builder title(@NonNull String title) { + this.titleComponent = WRAPPER.literalChatComponent(title); + return this; + } + + /** + * Sets the AnvilGUI title that is to be displayed to the user. + *
+ * The provided json will be parsed into rich chat components. + * + * @param json The title that is to be displayed to the user + * @return The {@link Builder} instance + * @throws IllegalArgumentException if the title is null + * @see net.md_5.bungee.chat.ComponentSerializer#toString(BaseComponent) + */ + public Builder jsonTitle(@NonNull String json) { + this.titleComponent = WRAPPER.jsonChatComponent(json); + return this; + } + + /** + * Sets the {@link ItemStack} to be put in the first slot + * + * @param item The {@link ItemStack} to be put in the first slot + * @return The {@link Builder} instance + * @throws IllegalArgumentException if the {@link ItemStack} is null + */ + public Builder itemLeft(@NonNull ItemStack item) { + this.itemLeft = item; + return this; + } + + /** + * Sets the {@link ItemStack} to be put in the second slot + * + * @param item The {@link ItemStack} to be put in the second slot + * @return The {@link Builder} instance + */ + public Builder itemRight(ItemStack item) { + this.itemRight = item; + return this; + } + + /** + * Sets the {@link ItemStack} to be put in the output slot + * + * @param item The {@link ItemStack} to be put in the output slot + * @return The {@link Builder} instance + */ + public Builder itemOutput(ItemStack item) { + this.itemOutput = item; + return this; + } + + /** + * Creates the anvil GUI and opens it for the player + * + * @param player The {@link Player} the anvil GUI should open for + * @return The {@link AnvilGUI} instance from this builder + * @throws IllegalArgumentException when the onClick function, plugin, or player is null + */ + public AnvilGUI open(@NonNull Player player) { + if (plugin == null) { + throw new IllegalStateException("An AnvilGUI plugin has not been set!"); + } + if (clickHandler == null) { + throw new IllegalStateException("An AnvilGUI clickHandler has not been set!"); + } + + if (itemText != null) { + if (itemLeft == null) { + itemLeft = new ItemStack(Material.PAPER); + } + + ItemMeta paperMeta = itemLeft.getItemMeta(); + paperMeta.setDisplayName(itemText); + itemLeft.setItemMeta(paperMeta); + } + + // If no executor is specified, execute all tasks with the BukkitScheduler + if (mainThreadExecutor == null) { + mainThreadExecutor = task -> Bukkit.getScheduler().runTask(plugin, task); + } + + final AnvilGUI anvilGUI = new AnvilGUI( + plugin, + player, + mainThreadExecutor, + titleComponent, + new ItemStack[] {itemLeft, itemRight, itemOutput}, + preventClose, + geyserCompatibility, + interactableSlots, + closeListener, + concurrentClickHandlerExecution, + clickHandler); + anvilGUI.openInventory(); + return anvilGUI; + } + } + + /** + * A handler that is called when the user clicks a slot. The + * {@link Integer} is the slot number corresponding to {@link Slot}, the + * {@link StateSnapshot} contains information about the current state of the anvil, + * and the response is a {@link CompletableFuture} that will eventually return a + * list of {@link ResponseAction} to execute in the order that they are supplied. + */ + @FunctionalInterface + public interface ClickHandler extends BiFunction>> {} + + /** An action to run in response to a player clicking the output slot in the GUI. This interface is public + * and permits you, the developer, to add additional response features easily to your custom AnvilGUIs. */ + @FunctionalInterface + public interface ResponseAction extends BiConsumer { + + /** + * Replace the input text box value with the provided text value. + * + * Before using this method, it must be verified by the caller that items are either in + * {@link Slot#INPUT_LEFT} or {@link Slot#OUTPUT} present. + * + * @param text The text to write in the input box + * @return The {@link ResponseAction} to achieve the text replacement + * @throws IllegalArgumentException when the text is null + * @throws IllegalStateException when the slots {@link Slot#INPUT_LEFT} and {@link Slot#OUTPUT} are null + */ + static ResponseAction replaceInputText(@NonNull String text) { + return (anvilgui, player) -> { + ItemStack item = anvilgui.getInventory().getItem(Slot.OUTPUT); + if (item == null) { + // Fallback on left input slot if player hasn't typed anything yet + item = anvilgui.getInventory().getItem(Slot.INPUT_LEFT); + } + if (item == null) { + throw new IllegalStateException( + "replaceInputText can only be used if slots OUTPUT or INPUT_LEFT are not empty"); + } + + final ItemStack cloned = item.clone(); + final ItemMeta meta = cloned.getItemMeta(); + meta.setDisplayName(text); + cloned.setItemMeta(meta); + anvilgui.getInventory().setItem(Slot.INPUT_LEFT, cloned); + }; + } + + /** + * Updates the title of the AnvilGUI to the new one. + * + * @param literalTitle The title to use as literal text + * @param preserveRenameText Whether to preserve the entered rename text + * @throws IllegalArgumentException when literalTitle is null + * @see Builder#title(String) + */ + static ResponseAction updateTitle(@NonNull String literalTitle, boolean preserveRenameText) { + return (anvilGUI, player) -> anvilGUI.setTitle(literalTitle, preserveRenameText); + } + + /** + * Updates the title of the AnvilGUI to the new one. + * + * @param json The json used to parse into a rich chat component + * @param preserveRenameText Whether to preserve the entered rename text + * @throws IllegalArgumentException when json is null + * @see Builder#jsonTitle(String) + */ + static ResponseAction updateJsonTitle(@NonNull String json, boolean preserveRenameText) { + return (anvilGUI, player) -> anvilGUI.setJsonTitle(json, preserveRenameText); + } + + /** + * Open another inventory + * @param otherInventory The inventory to open + * @return The {@link ResponseAction} to achieve the inventory open + * @throws IllegalArgumentException when the otherInventory is null + */ + static ResponseAction openInventory(@NonNull Inventory otherInventory) { + return (anvilgui, player) -> player.openInventory(otherInventory); + } + + /** + * Close the AnvilGUI + * @return The {@link ResponseAction} to achieve closing the AnvilGUI + */ + static ResponseAction close() { + return (anvilgui, player) -> anvilgui.closeInventory(); + } + + /** + * Run the provided runnable + * @param runnable The runnable to run + * @return The {@link ResponseAction} to achieve running the runnable + * @throws IllegalArgumentException when the runnable is null + */ + static ResponseAction run(@NonNull Runnable runnable) { + return (anvilgui, player) -> runnable.run(); + } + } + + /** + * Represents a response when the player clicks the output item in the anvil GUI + * @deprecated Since 1.6.2, use {@link ResponseAction} + */ + @Deprecated + public static class Response { + /** + * Returns an {@link Response} object for when the anvil GUI is to close + * @return An {@link Response} object for when the anvil GUI is to display text to the user + * @deprecated Since 1.6.2, use {@link ResponseAction#close()} + */ + public static List close() { + return Arrays.asList(ResponseAction.close()); + } + + /** + * Returns an {@link Response} object for when the anvil GUI is to display text to the user + * + * @param text The text that is to be displayed to the user + * @return A list containing the {@link ResponseAction} for legacy compat + * @deprecated Since 1.6.2, use {@link ResponseAction#replaceInputText(String)} + */ + public static List text(String text) { + return Arrays.asList(ResponseAction.replaceInputText(text)); + } + + /** + * Returns an {@link Response} object for when the GUI should open the provided inventory + * + * @param inventory The inventory to open + * @return A list containing the {@link ResponseAction} for legacy compat + * @deprecated Since 1.6.2, use {@link ResponseAction#openInventory(Inventory)} + */ + public static List openInventory(Inventory inventory) { + return Arrays.asList(ResponseAction.openInventory(inventory)); + } + } + + /** + * Class wrapping the magic constants of slot numbers in an anvil GUI + */ + public static class Slot { + + private static final int[] values = new int[] {Slot.INPUT_LEFT, Slot.INPUT_RIGHT, Slot.OUTPUT}; + + /** + * The slot on the far left, where the first input is inserted. An {@link ItemStack} is always inserted + * here to be renamed + */ + public static final int INPUT_LEFT = 0; + /** + * Not used, but in a real anvil you are able to put the second item you want to combine here + */ + public static final int INPUT_RIGHT = 1; + /** + * The output slot, where an item is put when two items are combined from {@link #INPUT_LEFT} and + * {@link #INPUT_RIGHT} or {@link #INPUT_LEFT} is renamed + */ + public static final int OUTPUT = 2; + + /** + * Get all anvil slot values + * + * @return The array containing all possible anvil slots + */ + public static int[] values() { + return values; + } + } + + /** Represents a snapshot of the state of an AnvilGUI */ + public static final class StateSnapshot { + + /** + * Create an {@link StateSnapshot} from the current state of an {@link AnvilGUI} + * @param anvilGUI The instance to take the snapshot of + * @return The snapshot + */ + private static StateSnapshot fromAnvilGUI(AnvilGUI anvilGUI) { + final Inventory inventory = anvilGUI.getInventory(); + return new StateSnapshot( + itemNotNull(inventory.getItem(Slot.INPUT_LEFT)).clone(), + itemNotNull(inventory.getItem(Slot.INPUT_RIGHT)).clone(), + itemNotNull(inventory.getItem(Slot.OUTPUT)).clone(), + anvilGUI.player); + } + + /** + * The {@link ItemStack} in the anvilGui slots + */ + private final ItemStack leftItem, rightItem, outputItem; + + /** + * The {@link Player} that clicked the output slot + */ + private final Player player; + + /** + * The event parameter constructor + * @param leftItem The left item in the combine slot of the anvilGUI + * @param rightItem The right item in the combine slot of the anvilGUI + * @param outputItem The item that would have been outputted, when the items would have been combined + * @param player The player that clicked the output slot + */ + public StateSnapshot(ItemStack leftItem, ItemStack rightItem, ItemStack outputItem, Player player) { + this.leftItem = leftItem; + this.rightItem = rightItem; + this.outputItem = outputItem; + this.player = player; + } + + /** + * It returns the item in the left combine slot of the gui + * + * @return The leftItem + */ + public ItemStack getLeftItem() { + return leftItem; + } + + /** + * It returns the item in the right combine slot of the gui + * + * @return The rightItem + */ + public ItemStack getRightItem() { + return rightItem; + } + + /** + * It returns the output item that would have been the result + * by combining the left and right one + * + * @return The outputItem + */ + public ItemStack getOutputItem() { + return outputItem; + } + + /** + * It returns the player that clicked onto the output slot + * + * @return The player + */ + public Player getPlayer() { + return player; + } + + /** + * It returns the text the player typed into the rename field + * + * @return The text of the rename field + */ + public String getText() { + return outputItem.hasItemMeta() ? outputItem.getItemMeta().getDisplayName() : ""; + } + } +} \ No newline at end of file diff --git a/SpigotCore/SpigotCore_Main/src/net/wesjd/anvilgui/version/VersionWrapper.java b/SpigotCore/SpigotCore_Main/src/net/wesjd/anvilgui/version/VersionWrapper.java new file mode 100644 index 00000000..6422fb48 --- /dev/null +++ b/SpigotCore/SpigotCore_Main/src/net/wesjd/anvilgui/version/VersionWrapper.java @@ -0,0 +1,175 @@ +package net.wesjd.anvilgui.version; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.component.DataComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.game.ClientboundContainerClosePacket; +import net.minecraft.network.protocol.game.ClientboundOpenScreenPacket; +import net.minecraft.network.protocol.game.ClientboundSetExperiencePacket; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.inventory.AnvilMenu; +import net.minecraft.world.inventory.ContainerLevelAccess; +import net.minecraft.world.inventory.MenuType; +import net.minecraft.world.inventory.Slot; +import org.bukkit.craftbukkit.CraftWorld; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.bukkit.craftbukkit.event.CraftEventFactory; +import org.bukkit.craftbukkit.util.CraftChatMessage; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryCloseEvent; +import org.bukkit.inventory.Inventory; + +import java.lang.reflect.Method; + +public final class VersionWrapper { + public static final VersionWrapper INSTANCE = new VersionWrapper(); + + /** + * Interface implemented by the custom NMS AnvilContainer used to interact with it directly + */ + public interface AnvilContainerWrapper { + + /** + * Retrieves the raw text that has been entered into the Anvil at the moment + *

+ * This field is marked as public in the Minecraft AnvilContainer only from Minecraft 1.11 and upwards + * + * @return The raw text in the rename field + */ + default String getRenameText() { + return null; + } + + /** + * Sets the provided text as the literal hovername of the item in the left input slot + * + * @param text The text to set + */ + default void setRenameText(String text) { + } + + /** + * Gets the {@link Inventory} wrapper of the NMS container + * + * @return The inventory of the NMS container + */ + Inventory getBukkitInventory(); + } + + private static class AnvilContainer extends AnvilMenu implements AnvilContainerWrapper { + public AnvilContainer(Player player, int containerId, Component guiTitle) { + super( + containerId, + ((CraftPlayer) player).getHandle().getInventory(), + ContainerLevelAccess.create(((CraftWorld) player.getWorld()).getHandle(), new BlockPos(0, 0, 0)) + ); + this.checkReachable = false; + setTitle(guiTitle); + } + + public void l() { + // If the output is empty copy the left input into the output + Slot output = this.getSlot(2); + if (!output.hasItem()) { + output.set(this.getSlot(0).getItem().copy()); + } + + this.cost.set(0); + + // Sync to the client + this.sendAllDataToRemote(); + this.broadcastChanges(); + } + + public void a(LivingEntity player) { + } + + protected void a(LivingEntity player, Container container) { + } + + public int getContainerId() { + return this.containerId; + } + + public String getRenameText() { + return this.itemName; + } + + public void setRenameText(String text) { + // If an item is present in the left input slot change its hover name to the literal text. + Slot inputLeft = getSlot(0); + if (inputLeft.hasItem()) { + inputLeft.getItem() + .set(DataComponents.CUSTOM_NAME, Component.literal(text)); + } + } + + public Inventory getBukkitInventory() { + return this.getBukkitView().getTopInventory(); + } + } + + private int getRealNextContainerId(Player player) { + return toNMS(player).nextContainerCounter(); + } + + /** + * Turns a {@link Player} into an NMS one + * + * @param player The player to be converted + * @return the NMS EntityPlayer + */ + private ServerPlayer toNMS(Player player) { + return ((CraftPlayer) player).getHandle(); + } + + public int getNextContainerId(Player player, AnvilContainerWrapper container) { + return ((AnvilContainer) container).getContainerId(); + } + + public void handleInventoryCloseEvent(Player player) { + CraftEventFactory.handleInventoryCloseEvent(toNMS(player), InventoryCloseEvent.Reason.UNKNOWN); + toNMS(player).doCloseContainer(); + } + + public void sendPacketOpenWindow(Player player, int containerId, Object inventoryTitle) { + toNMS(player).connection.send(new ClientboundOpenScreenPacket(containerId, MenuType.ANVIL, (Component) inventoryTitle)); + } + + public void sendPacketCloseWindow(Player player, int containerId) { + toNMS(player).connection.send(new ClientboundContainerClosePacket(containerId)); + } + + public void sendPacketExperienceChange(Player player, int experienceLevel) { + toNMS(player).connection.send(new ClientboundSetExperiencePacket(0f, 0, experienceLevel)); + } + + public void setActiveContainerDefault(Player player) { + toNMS(player).containerMenu = toNMS(player).inventoryMenu; + } + + public void setActiveContainer(Player player, AnvilContainerWrapper container) { + toNMS(player).containerMenu = (AnvilMenu) container; + } + + public void setActiveContainerId(AnvilContainerWrapper container, int containerId) { + } + + public void addActiveContainerSlotListener(AnvilContainerWrapper container, Player player) { + toNMS(player).initMenu((AnvilMenu) container); + } + + public AnvilContainerWrapper newContainerAnvil(Player player, Object title) { + return new AnvilContainer(player, getRealNextContainerId(player), (Component) title); + } + + public Object literalChatComponent(String content) { + return Component.literal(content); + } + + public Object jsonChatComponent(String json) { + return CraftChatMessage.fromJSON(json); + } +} \ No newline at end of file