Fix AnvilGUI for 1.21 not being mojmapped

This commit is contained in:
2026-06-12 14:52:03 +02:00
parent 27877938bc
commit e0aaacd66a
3 changed files with 1097 additions and 2 deletions
@@ -59,8 +59,6 @@ dependencies {
compileOnly(libs.netty) compileOnly(libs.netty)
compileOnly(libs.brigadier) compileOnly(libs.brigadier)
compileOnly(libs.fastutil) compileOnly(libs.fastutil)
implementation(libs.anvilgui)
} }
widener { widener {
@@ -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<Integer> interactableSlots;
/** An {@link Consumer} that is called when the anvil GUI is close */
private final Consumer<StateSnapshot> 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<Integer> interactableSlots,
Consumer<StateSnapshot> 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<List<ResponseAction>> actionsFuture =
clickHandler.apply(rawSlot, StateSnapshot.fromAnvilGUI(AnvilGUI.this));
final Consumer<List<ResponseAction>> 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<StateSnapshot> 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<Integer> 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<Integer> 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<StateSnapshot> closeListener) {
this.closeListener = closeListener;
return this;
}
/**
* Do an action when a slot is clicked in the inventory
* <p>
* 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.
* <p>
* 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<Integer, StateSnapshot, List<ResponseAction>> 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.
* <br><br>
* 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.
* <br>
* 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.
* <br>
* 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<Integer, StateSnapshot, CompletableFuture<List<ResponseAction>>> {}
/** 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<AnvilGUI, Player> {
/**
* 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 <code>null</code>
*/
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<ResponseAction> 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<ResponseAction> 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<ResponseAction> 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() : "";
}
}
}
@@ -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
* <br><br>
* 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);
}
}