feat: make nicko work again partially

This commit is contained in:
ineanto 2025-03-20 20:35:24 +01:00
parent a996858ba9
commit 4f9e334544
Signed by: ineanto
GPG key ID: E511F9CAA2F9CE84
14 changed files with 1939 additions and 77 deletions

1
.gitignore vendored
View file

@ -30,3 +30,4 @@ run/
### Private ###
TODO
src/main/java/net/

View file

@ -36,7 +36,7 @@ dependencies {
compileOnly("me.clip:placeholderapi:2.11.5")
compileOnly("net.kyori:adventure-api:4.17.0")
compileOnly("xyz.xenondevs.invui:invui-core:$invuiVersion")
compileOnly("net.wesjd:anvilgui:1.10.4-SNAPSHOT")
//compileOnly("net.wesjd:anvilgui:1.10.4-SNAPSHOT")
//implementation("xyz.xenondevs.invui:inventory-access-r22:$invuiVersion:remapped-mojang")

View file

@ -0,0 +1,13 @@
Copyright (C) 2012 Kristian S. Stangeland
This program is free software; you can redistribute it and/or modify it under the terms of the
GNU General Public License as published by the Free Software Foundation; either version 2 of
the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program;
if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
02111-1307 USA

View file

@ -0,0 +1,468 @@
/*
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
* Copyright (C) 2012 Kristian S. Stangeland
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program;
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
*/
package com.comphenix.protocol.utility;
import com.comphenix.protocol.ProtocolLibrary;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.Ordering;
import org.bukkit.Bukkit;
import org.bukkit.Server;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Locale;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Determine the current Minecraft version.
*
* @author Kristian
*/
public final class MinecraftVersion implements Comparable<MinecraftVersion>, Serializable {
/**
* Version 1.21.4 - the garden awakens drop
*/
public static final MinecraftVersion v1_21_4 = new MinecraftVersion("1.21.4");
/**
* Version 1.21.2 - the bundles of bravery drop
*/
public static final MinecraftVersion v1_21_2 = new MinecraftVersion("1.21.2");
/**
* Version 1.21.0 - the tricky trials update
*/
public static final MinecraftVersion v1_21_0 = new MinecraftVersion("1.21.0");
/**
* Version 1.20.5 - the cookie and transfer packet update
*/
public static final MinecraftVersion v1_20_5 = new MinecraftVersion("1.20.5");
/**
* Version 1.20.4 - the decorated pot update
*/
public static final MinecraftVersion v1_20_4 = new MinecraftVersion("1.20.4");
/**
* Version 1.20.2 - the update that added the configuration protocol phase.
*/
public static final MinecraftVersion CONFIG_PHASE_PROTOCOL_UPDATE = new MinecraftVersion("1.20.2");
/**
* Version 1.20 - the trails and tails update
*/
public static final MinecraftVersion TRAILS_AND_TAILS = new MinecraftVersion("1.20");
/**
* Version 1.19.4 - the rest of the feature preview
*/
public static final MinecraftVersion FEATURE_PREVIEW_2 = new MinecraftVersion("1.19.4");
/**
* Version 1.19.3 - introducing feature preview
*/
public static final MinecraftVersion FEATURE_PREVIEW_UPDATE = new MinecraftVersion("1.19.3");
/**
* Version 1.19 - the wild update
*/
public static final MinecraftVersion WILD_UPDATE = new MinecraftVersion("1.19");
/**
* Version 1.18 - caves and cliffs part 2
*/
public static final MinecraftVersion CAVES_CLIFFS_2 = new MinecraftVersion("1.18");
/**
* Version 1.17 - caves and cliffs part 1
*/
public static final MinecraftVersion CAVES_CLIFFS_1 = new MinecraftVersion("1.17");
/**
* Version 1.16.4
*/
public static final MinecraftVersion NETHER_UPDATE_4 = new MinecraftVersion("1.16.4");
/**
* Version 1.16.2 - breaking change to the nether update
*/
public static final MinecraftVersion NETHER_UPDATE_2 = new MinecraftVersion("1.16.2");
/**
* Version 1.16.0 - the nether update
*/
public static final MinecraftVersion NETHER_UPDATE = new MinecraftVersion("1.16");
/**
* Version 1.15 - the bee update
*/
public static final MinecraftVersion BEE_UPDATE = new MinecraftVersion("1.15");
/**
* Version 1.14 - village and pillage update.
*/
public static final MinecraftVersion VILLAGE_UPDATE = new MinecraftVersion("1.14");
/**
* Version 1.13 - update aquatic.
*/
public static final MinecraftVersion AQUATIC_UPDATE = new MinecraftVersion("1.13");
/**
* Version 1.12 - the world of color update.
*/
public static final MinecraftVersion COLOR_UPDATE = new MinecraftVersion("1.12");
/**
* Version 1.11 - the exploration update.
*/
public static final MinecraftVersion EXPLORATION_UPDATE = new MinecraftVersion("1.11");
/**
* Version 1.10 - the frostburn update.
*/
public static final MinecraftVersion FROSTBURN_UPDATE = new MinecraftVersion("1.10");
/**
* Version 1.9 - the combat update.
*/
public static final MinecraftVersion COMBAT_UPDATE = new MinecraftVersion("1.9");
/**
* Version 1.8 - the "bountiful" update.
*/
public static final MinecraftVersion BOUNTIFUL_UPDATE = new MinecraftVersion("1.8");
/**
* Version 1.7.8 - the update that changed the skin format (and distribution - R.I.P. player disguise)
*/
public static final MinecraftVersion SKIN_UPDATE = new MinecraftVersion("1.7.8");
/**
* Version 1.7.2 - the update that changed the world.
*/
public static final MinecraftVersion WORLD_UPDATE = new MinecraftVersion("1.7.2");
/**
* Version 1.6.1 - the horse update.
*/
public static final MinecraftVersion HORSE_UPDATE = new MinecraftVersion("1.6.1");
/**
* Version 1.5.0 - the redstone update.
*/
public static final MinecraftVersion REDSTONE_UPDATE = new MinecraftVersion("1.5.0");
/**
* Version 1.4.2 - the scary update (Wither Boss).
*/
public static final MinecraftVersion SCARY_UPDATE = new MinecraftVersion("1.4.2");
/**
* The latest release version of minecraft.
*/
public static final MinecraftVersion LATEST = v1_21_4;
// used when serializing
private static final long serialVersionUID = -8695133558996459770L;
/**
* Regular expression used to parse version strings.
*/
private static final Pattern VERSION_PATTERN = Pattern.compile(".*\\(.*MC.\\s*([a-zA-z0-9\\-.]+).*");
/**
* The current version of minecraft, lazy initialized by MinecraftVersion.currentVersion()
*/
private static MinecraftVersion currentVersion;
private final int major;
private final int minor;
private final int build;
// The development stage
private final String development;
// Snapshot?
private final SnapshotVersion snapshot;
private volatile Boolean atCurrentOrAbove;
/**
* Determine the current Minecraft version.
*
* @param server - the Bukkit server that will be used to examine the MC version.
*/
public MinecraftVersion(Server server) {
this(extractVersion(server.getVersion()));
}
/**
* Construct a version object from the format major.minor.build, or the snapshot format.
*
* @param versionOnly - the version in text form.
*/
public MinecraftVersion(String versionOnly) {
this(versionOnly, true);
}
/**
* Construct a version format from the standard release version or the snapshot verison.
*
* @param versionOnly - the version.
* @param parseSnapshot - TRUE to parse the snapshot, FALSE otherwise.
*/
private MinecraftVersion(String versionOnly, boolean parseSnapshot) {
String[] section = versionOnly.split("-");
SnapshotVersion snapshot = null;
int[] numbers = new int[3];
try {
numbers = this.parseVersion(section[0]);
} catch (NumberFormatException cause) {
// Skip snapshot parsing
if (!parseSnapshot) {
throw cause;
}
try {
// Determine if the snapshot is newer than the current release version
snapshot = new SnapshotVersion(section[0]);
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
MinecraftVersion latest = new MinecraftVersion(ProtocolLibrary.MAXIMUM_MINECRAFT_VERSION, false);
boolean newer = snapshot.getSnapshotDate().compareTo(
format.parse(ProtocolLibrary.MINECRAFT_LAST_RELEASE_DATE)) > 0;
numbers[0] = latest.getMajor();
numbers[1] = latest.getMinor() + (newer ? 1 : -1);
} catch (Exception e) {
throw new IllegalStateException("Cannot parse " + section[0], e);
}
}
this.major = numbers[0];
this.minor = numbers[1];
this.build = numbers[2];
this.development = section.length > 1 ? section[1] : (snapshot != null ? "snapshot" : null);
this.snapshot = snapshot;
}
/**
* Construct a version object directly.
*
* @param major - major version number.
* @param minor - minor version number.
* @param build - build version number.
*/
public MinecraftVersion(int major, int minor, int build) {
this(major, minor, build, null);
}
/**
* Construct a version object directly.
*
* @param major - major version number.
* @param minor - minor version number.
* @param build - build version number.
* @param development - development stage.
*/
public MinecraftVersion(int major, int minor, int build, String development) {
this.major = major;
this.minor = minor;
this.build = build;
this.development = development;
this.snapshot = null;
}
/**
* Extract the Minecraft version from CraftBukkit itself.
*
* @param text - the server version in text form.
* @return The underlying MC version.
* @throws IllegalStateException If we could not parse the version string.
*/
public static String extractVersion(String text) {
Matcher version = VERSION_PATTERN.matcher(text);
if (version.matches() && version.group(1) != null) {
return version.group(1);
} else {
throw new IllegalStateException("Cannot parse version String '" + text + "'");
}
}
/**
* Parse the given server version into a Minecraft version.
*
* @param serverVersion - the server version.
* @return The resulting Minecraft version.
*/
public static MinecraftVersion fromServerVersion(String serverVersion) {
return new MinecraftVersion(extractVersion(serverVersion));
}
public static MinecraftVersion getCurrentVersion() {
if (currentVersion == null) {
currentVersion = fromServerVersion(Bukkit.getVersion());
}
return currentVersion;
}
public static void setCurrentVersion(MinecraftVersion version) {
currentVersion = version;
}
private static boolean atOrAbove(MinecraftVersion version) {
return getCurrentVersion().isAtLeast(version);
}
private int[] parseVersion(String version) {
String[] elements = version.split("\\.");
int[] numbers = new int[3];
// Make sure it's even a valid version
if (elements.length < 1) {
throw new IllegalStateException("Corrupt MC version: " + version);
}
// The String 1 or 1.2 is interpreted as 1.0.0 and 1.2.0 respectively.
for (int i = 0; i < Math.min(numbers.length, elements.length); i++) {
numbers[i] = Integer.parseInt(elements[i].trim());
}
return numbers;
}
/**
* Major version number
*
* @return Current major version number.
*/
public int getMajor() {
return this.major;
}
/**
* Minor version number
*
* @return Current minor version number.
*/
public int getMinor() {
return this.minor;
}
/**
* Build version number
*
* @return Current build version number.
*/
public int getBuild() {
return this.build;
}
/**
* Retrieve the development stage.
*
* @return Development stage, or NULL if this is a release.
*/
public String getDevelopmentStage() {
return this.development;
}
/**
* Retrieve the snapshot version, or NULL if this is a release.
*
* @return The snapshot version.
*/
public SnapshotVersion getSnapshot() {
return this.snapshot;
}
/**
* Determine if this version is a snapshot.
*
* @return The snapshot version.
*/
public boolean isSnapshot() {
return this.snapshot != null;
}
/**
* Checks if this version is at or above the current version the server is running.
*
* @return true if this version is equal or newer than the server version, false otherwise.
*/
public boolean atOrAbove() {
if (this.atCurrentOrAbove == null) {
this.atCurrentOrAbove = atOrAbove(this);
}
return this.atCurrentOrAbove;
}
/**
* Retrieve the version String (major.minor.build) only.
*
* @return A normal version string.
*/
public String getVersion() {
if (this.getDevelopmentStage() == null) {
return String.format("%s.%s.%s", this.getMajor(), this.getMinor(), this.getBuild());
} else {
return String.format("%s.%s.%s-%s%s", this.getMajor(), this.getMinor(), this.getBuild(),
this.getDevelopmentStage(), this.isSnapshot() ? this.snapshot : "");
}
}
@Override
public int compareTo(MinecraftVersion o) {
if (o == null) {
return 1;
}
return ComparisonChain.start()
.compare(this.getMajor(), o.getMajor())
.compare(this.getMinor(), o.getMinor())
.compare(this.getBuild(), o.getBuild())
.compare(this.getDevelopmentStage(), o.getDevelopmentStage(), Ordering.natural().nullsLast())
.compare(this.getSnapshot(), o.getSnapshot(), Ordering.natural().nullsFirst())
.result();
}
public boolean isAtLeast(MinecraftVersion other) {
if (other == null) {
return false;
}
return this.compareTo(other) >= 0;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (obj == this) {
return true;
}
if (obj instanceof MinecraftVersion) {
MinecraftVersion other = (MinecraftVersion) obj;
return this.getMajor() == other.getMajor() &&
this.getMinor() == other.getMinor() &&
this.getBuild() == other.getBuild() &&
Objects.equals(this.getDevelopmentStage(), other.getDevelopmentStage());
}
return false;
}
@Override
public int hashCode() {
return Objects.hash(this.getMajor(), this.getMinor(), this.getBuild());
}
@Override
public String toString() {
// Convert to a String that we can parse back again
return String.format("(MC: %s)", this.getVersion());
}
}

View file

@ -0,0 +1,150 @@
/*
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
* Copyright (C) 2012 Kristian S. Stangeland
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program;
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
*/
package com.comphenix.protocol.utility;
import com.google.common.collect.ComparisonChain;
import java.io.Serial;
import java.io.Serializable;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Used to parse a snapshot version.
*
* @author Kristian
*/
public class SnapshotVersion implements Comparable<SnapshotVersion>, Serializable {
@Serial
private static final long serialVersionUID = 2778655372579322310L;
private static final Pattern SNAPSHOT_PATTERN = Pattern.compile("(\\d{2}w\\d{2})([a-z])");
private final Date snapshotDate;
private final int snapshotWeekVersion;
private transient String rawString;
public SnapshotVersion(String version) {
Matcher matcher = SNAPSHOT_PATTERN.matcher(version.trim());
if (matcher.matches()) {
try {
this.snapshotDate = getDateFormat().parse(matcher.group(1));
this.snapshotWeekVersion = matcher.group(2).charAt(0) - 'a';
this.rawString = version;
} catch (ParseException e) {
throw new IllegalArgumentException("Date implied by snapshot version is invalid.", e);
}
} else {
throw new IllegalArgumentException("Cannot parse " + version + " as a snapshot version.");
}
}
/**
* Retrieve the snapshot date parser.
* <p>
* We have to create a new instance of SimpleDateFormat every time as it is not thread safe.
*
* @return The date formatter.
*/
private static SimpleDateFormat getDateFormat() {
SimpleDateFormat format = new SimpleDateFormat("yy'w'ww", Locale.US);
format.setLenient(false);
return format;
}
/**
* Retrieve the snapshot version within a week, starting at zero.
*
* @return The weekly version
*/
public int getSnapshotWeekVersion() {
return this.snapshotWeekVersion;
}
/**
* Retrieve the week this snapshot was released.
*
* @return The week.
*/
public Date getSnapshotDate() {
return this.snapshotDate;
}
/**
* Retrieve the raw snapshot string (yy'w'ww[a-z]).
*
* @return The snapshot string.
*/
public String getSnapshotString() {
if (this.rawString == null) {
// It's essential that we use the same locale
Calendar current = Calendar.getInstance(Locale.US);
current.setTime(this.snapshotDate);
this.rawString = String.format("%02dw%02d%s",
current.get(Calendar.YEAR) % 100,
current.get(Calendar.WEEK_OF_YEAR),
(char) ('a' + this.snapshotWeekVersion));
}
return this.rawString;
}
@Override
public int compareTo(SnapshotVersion o) {
if (o == null) {
return 1;
}
return ComparisonChain.start()
.compare(this.snapshotDate, o.getSnapshotDate())
.compare(this.snapshotWeekVersion, o.getSnapshotWeekVersion())
.result();
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj instanceof SnapshotVersion) {
SnapshotVersion other = (SnapshotVersion) obj;
return Objects.equals(this.snapshotDate, other.getSnapshotDate())
&& this.snapshotWeekVersion == other.getSnapshotWeekVersion();
}
return false;
}
@Override
public int hashCode() {
return Objects.hash(this.snapshotDate, this.snapshotWeekVersion);
}
@Override
public String toString() {
return this.getSnapshotString();
}
}

View file

@ -0,0 +1,889 @@
package net.wesjd.anvilgui;
import net.md_5.bungee.api.chat.BaseComponent;
import net.wesjd.anvilgui.version.VersionWrapper;
import net.wesjd.anvilgui.version.VersionMatcher;
import org.apache.commons.lang.Validate;
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 = new VersionMatcher().match();
/**
* 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);
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);
}
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(String literalTitle, boolean preserveRenameText) {
Validate.notNull(literalTitle, "literalTitle cannot be null");
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(String json, boolean preserveRenameText) {
Validate.notNull(json, "json cannot be null");
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 (!WRAPPER.isCustomTitleSupported()) {
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(Executor executor) {
Validate.notNull(executor, "Executor cannot be null");
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(Consumer<StateSnapshot> closeListener) {
Validate.notNull(closeListener, "closeListener cannot be null");
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(ClickHandler clickHandler) {
Validate.notNull(clickHandler, "click function cannot be null");
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(BiFunction<Integer, StateSnapshot, List<ResponseAction>> clickHandler) {
Validate.notNull(clickHandler, "click function cannot be null");
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(Plugin plugin) {
Validate.notNull(plugin, "Plugin cannot be null");
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(String text) {
Validate.notNull(text, "Text cannot be null");
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(String title) {
Validate.notNull(title, "title cannot be null");
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(String json) {
Validate.notNull(json, "json cannot be null");
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(ItemStack item) {
Validate.notNull(item, "item cannot be null");
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(Player player) {
Validate.notNull(plugin, "Plugin cannot be null");
Validate.notNull(clickHandler, "click handler cannot be null");
Validate.notNull(player, "Player cannot be null");
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(String text) {
Validate.notNull(text, "text cannot be null");
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(String literalTitle, boolean preserveRenameText) {
Validate.notNull(literalTitle, "literalTitle cannot be null");
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(String json, boolean preserveRenameText) {
Validate.notNull(json, "json cannot be null");
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(Inventory otherInventory) {
Validate.notNull(otherInventory, "otherInventory cannot be null");
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(Runnable runnable) {
Validate.notNull(runnable, "runnable cannot be null");
return (anvilgui, player) -> runnable.run();
}
}
/**
* 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() : "";
}
}
}

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016 Wesley Smith
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
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,135 @@
package net.wesjd.anvilgui.version;
import net.minecraft.core.BlockPos;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.component.DataComponentPatch;
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.inventory.*;
import org.bukkit.craftbukkit.entity.CraftPlayer;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.jetbrains.annotations.NotNull;
public final class PaperWrapper1_21_R4 implements VersionWrapper {
private int getRealNextContainerId(Player player) {
return toNMS(player).nextContainerCounter();
}
private ServerPlayer toNMS(Player player) {
return ((CraftPlayer) player).getHandle();
}
@Override
public int getNextContainerId(Player player, AnvilContainerWrapper container) {
return ((AnvilContainer) container).containerId;
}
@Override
public void handleInventoryCloseEvent(Player player) {
//CraftEventFactory.handleInventoryCloseEvent(toNMS(player));
toNMS(player).doCloseContainer(); // q -> doCloseContainer
}
@Override
public void sendPacketOpenWindow(Player player, int containerId, Object inventoryTitle) {
toNMS(player).connection.send(new ClientboundOpenScreenPacket(containerId, MenuType.ANVIL, Component.literal(inventoryTitle.toString())));
}
@Override
public void sendPacketCloseWindow(Player player, int containerId) {
toNMS(player).connection.send(new ClientboundContainerClosePacket(containerId));
}
@Override
public void sendPacketExperienceChange(Player player, int experienceLevel) {
toNMS(player).connection.send(new ClientboundSetExperiencePacket(0f, 0, experienceLevel));
}
@Override
public void setActiveContainerDefault(Player player) {
toNMS(player).containerMenu = toNMS(player).inventoryMenu; // cd -> containerMenu, cc -> inventoryMenu
}
@Override
public void setActiveContainer(Player player, AnvilContainerWrapper container) {
toNMS(player).containerMenu = (AbstractContainerMenu) container;
}
@Override
public void setActiveContainerId(AnvilContainerWrapper container, int containerId) { }
@Override
public void addActiveContainerSlotListener(AnvilContainerWrapper container, Player player) {
toNMS(player).initMenu((AbstractContainerMenu) container);
}
@Override
public AnvilContainerWrapper newContainerAnvil(Player player, Object title) {
return new AnvilContainer(player, getRealNextContainerId(player), Component.literal(title.toString()));
}
@Override
public Object literalChatComponent(String content) {
return Component.literal(content); // IChatBaseComponent.b -> Component.literal
}
@Override
public Object jsonChatComponent(String json) {
return Component.Serializer.toJson(Component.literal(json), RegistryAccess.EMPTY);
}
private static class AnvilContainer extends AnvilMenu implements AnvilContainerWrapper {
public AnvilContainer(Player player, int containerId, Component guiTitle) {
super(
containerId,
((CraftPlayer) player).getHandle().getInventory(),
ContainerLevelAccess.create(((CraftPlayer) player).getHandle().level(), BlockPos.ZERO)
);
this.checkReachable = false;
setTitle(guiTitle);
}
@Override
public void createResult() {
// If the output is empty, copy the left input into the output
Slot output = getSlot(2); // b -> getSlot
if (!output.hasItem()) { // h -> hasItem
output.set(getSlot(0).getItem().copy()); // f -> set, g -> getItem, v -> copy
}
this.cost.set(0); // y -> cost, a -> set
// Sync to the client
this.sendAllDataToRemote(); // b -> sendAllDataToRemote
this.broadcastChanges(); // d -> broadcastChanges
}
@Override
public boolean setItemName(@NotNull String itemName) {
Slot inputLeft = getSlot(0);
if (inputLeft.hasItem()) {
inputLeft
.getItem()
.applyComponents(DataComponentPatch
.builder()
.set(DataComponents.CUSTOM_NAME, Component.literal(itemName))
.build()
);
return true;
}
return false;
}
@Override
public Inventory getBukkitInventory() {
return this.getBukkitView().getTopInventory();
}
}
}

View file

@ -0,0 +1,51 @@
package net.wesjd.anvilgui.version;
import org.bukkit.Bukkit;
import java.util.HashMap;
import java.util.Map;
/**
* Matches the server's NMS version to its {@link VersionWrapper}
*
* @author Wesley Smith
* @since 1.2.1
*/
public class VersionMatcher {
/** Maps a Minecraft version string to the corresponding revision string */
private static final Map<String, String> VERSION_TO_REVISION = new HashMap<String, String>() {
{
this.put("1.21.4", "1_21_R4");
}
};
/* This needs to be updated to reflect the newest available version wrapper */
private static final String FALLBACK_REVISION = "1_21_R4";
/**
* Matches the server version to it's {@link VersionWrapper}
*
* @return The {@link VersionWrapper} for this server
* @throws IllegalStateException If the version wrapper failed to be instantiated or is unable to be found
*/
public VersionWrapper match() {
String craftBukkitPackage = Bukkit.getServer().getClass().getPackage().getName();
String rVersion;
if (!craftBukkitPackage.contains(".v")) { // cb package not relocated (i.e. paper 1.20.5+)
final String version = Bukkit.getBukkitVersion().split("-")[0];
rVersion = VERSION_TO_REVISION.getOrDefault(version, FALLBACK_REVISION);
} else {
rVersion = craftBukkitPackage.split("\\.")[3].substring(1);
}
try {
return (VersionWrapper) Class.forName(getClass().getPackage().getName() + ".PaperWrapper" + rVersion)
.getDeclaredConstructor()
.newInstance();
} catch (ClassNotFoundException exception) {
throw new IllegalStateException("AnvilGUI does not support server version \"" + rVersion + "\"", exception);
} catch (ReflectiveOperationException exception) {
throw new IllegalStateException("Failed to instantiate version wrapper for version " + rVersion, exception);
}
}
}

View file

@ -0,0 +1,150 @@
package net.wesjd.anvilgui.version;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
/**
* Wraps versions to be able to easily use different NMS server versions
*
* @author Wesley Smith
* @since 1.0
*/
public interface VersionWrapper {
/**
* Gets the next available NMS container id for the player
*
* @param player The player to get the next container id of
* @param container The container that a new id is being generated for
* @return The next available NMS container id
*/
int getNextContainerId(Player player, AnvilContainerWrapper container);
/**
* Closes the current inventory for the player
*
* @param player The player that needs their current inventory closed
*/
void handleInventoryCloseEvent(Player player);
/**
* Sends PacketPlayOutOpenWindow to the player with the container id and window title
*
* @param player The player to send the packet to
* @param containerId The container id to open
* @param inventoryTitle The title of the inventory to be opened (only works in Minecraft 1.14 and above)
*/
void sendPacketOpenWindow(Player player, int containerId, Object inventoryTitle);
/**
* Sends PacketPlayOutCloseWindow to the player with the container id
*
* @param player The player to send the packet to
* @param containerId The container id to close
*/
void sendPacketCloseWindow(Player player, int containerId);
/**
* Sends PacketPlayOutExperience to the player with the experience level
*
* @param player The player to send the packet to
* @param experienceLevel The experience level to set
*/
void sendPacketExperienceChange(Player player, int experienceLevel);
/**
* Sets the NMS player's active container to the default one
*
* @param player The player to set the active container of
*/
void setActiveContainerDefault(Player player);
/**
* Sets the NMS player's active container to the one supplied
*
* @param player The player to set the active container of
* @param container The container to set as active
*/
void setActiveContainer(Player player, AnvilContainerWrapper container);
/**
* Sets the supplied windowId of the supplied Container
*
* @param container The container to set the windowId of
* @param containerId The new windowId
*/
void setActiveContainerId(AnvilContainerWrapper container, int containerId);
/**
* Adds a slot listener to the supplied container for the player
*
* @param container The container to add the slot listener to
* @param player The player to have as a listener
*/
void addActiveContainerSlotListener(AnvilContainerWrapper container, Player player);
/**
* Creates a new ContainerAnvil
*
* @param player The player to get the container of
* @param title The title of the anvil inventory
* @return The Container instance
*/
AnvilContainerWrapper newContainerAnvil(Player player, Object title);
/**
* Checks if the current Minecraft version actually supports custom titles
*
* @return The current supported state
*/
default boolean isCustomTitleSupported() {
return true;
}
/**
* Creates a new chat component that does not handle the content in any special way
*
* @param content The content to display
* @return Version-specific ChatComponent instance
*/
Object literalChatComponent(String content);
/**
* Creates a new rich chat component from the provided json
*
* @param json The component to parse
* @return Version-specific ChatComponent instance
*/
Object jsonChatComponent(String json);
/**
* Interface implemented by the custom NMS AnvilContainer used to interact with it directly
*/
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();
}
}

View file

@ -3,7 +3,6 @@ package xyz.ineanto.nicko;
import com.comphenix.protocol.utility.MinecraftVersion;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.command.PluginCommand;
import org.bukkit.plugin.java.JavaPlugin;
import xyz.ineanto.nicko.appearance.random.RandomNameFetcher;
import xyz.ineanto.nicko.command.NickoCommand;
@ -21,6 +20,7 @@ import xyz.ineanto.nicko.storage.PlayerDataStore;
import xyz.ineanto.nicko.storage.json.JSONStorage;
import xyz.ineanto.nicko.storage.map.MapCache;
import xyz.ineanto.nicko.storage.name.PlayerNameStore;
import xyz.xenondevs.invui.InvUI;
import xyz.xenondevs.invui.gui.structure.Structure;
import xyz.xenondevs.invui.item.builder.ItemBuilder;
import xyz.xenondevs.invui.item.impl.SimpleItem;
@ -30,8 +30,6 @@ import java.io.IOException;
public class Nicko extends JavaPlugin {
private static Nicko plugin;
private final boolean unitTesting;
private MojangAPI mojangAPI;
private PlayerDataStore dataStore;
private ConfigurationManager configurationManager;
@ -40,19 +38,6 @@ public class Nicko extends JavaPlugin {
private PlayerNameStore nameStore;
private RandomNameFetcher nameFetcher;
public Nicko() {
this.unitTesting = false;
}
/**
* Used by MockBukkit
*/
protected Nicko(Configuration configuration) {
this.unitTesting = true;
this.configuration = configuration;
getLogger().info("Unit Testing Mode enabled.");
}
@Override
public void onEnable() {
plugin = this;
@ -96,12 +81,12 @@ public class Nicko extends JavaPlugin {
dataStore.setCache(cache);
}
if (!unitTesting) {
nameStore = new PlayerNameStore();
mojangAPI = new MojangAPI();
nameFetcher = new RandomNameFetcher(this);
new ConfigurationMigrator(this).migrate();
InvUI.getInstance().setPlugin(this);
if (configuration.isCustomLocale()) {
try {
@ -114,10 +99,7 @@ public class Nicko extends JavaPlugin {
}
}
final PluginCommand command = getCommand("nicko");
if (command != null) {
command.setExecutor(new NickoCommand());
}
registerCommand("nicko", new NickoCommand());
Structure.addGlobalIngredient('#', new SimpleItem(new ItemBuilder(Material.AIR)));
Structure.addGlobalIngredient('%', new SimpleItem(new ItemBuilder(Material.BLACK_STAINED_GLASS_PANE).setDisplayName(" ")));
@ -129,7 +111,6 @@ public class Nicko extends JavaPlugin {
getServer().getPluginManager().registerEvents(new PlayerJoinListener(), this);
getServer().getPluginManager().registerEvents(new PlayerQuitListener(), this);
}
getLogger().info("Nicko has been enabled.");
}
@ -145,9 +126,7 @@ public class Nicko extends JavaPlugin {
}
}
if (!unitTesting) {
nameStore.clearStoredNames();
}
getLogger().info("Nicko (Bukkit) has been disabled.");
}

View file

@ -1,28 +1,29 @@
package xyz.ineanto.nicko.command;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import io.papermc.paper.command.brigadier.BasicCommand;
import io.papermc.paper.command.brigadier.CommandSourceStack;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jspecify.annotations.Nullable;
import xyz.ineanto.nicko.gui.HomeGUI;
import xyz.ineanto.nicko.language.PlayerLanguage;
import xyz.ineanto.nicko.language.LanguageKey;
public class NickoCommand implements CommandExecutor {
public class NickoCommand implements BasicCommand {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
if (sender instanceof Player player) {
if (player.isOp() || player.hasPermission("nicko.use") || player.hasPermission("nicko.*")) {
public void execute(CommandSourceStack stack, String[] strings) {
final Entity executor = stack.getExecutor();
final Player player = (Player) executor;
new HomeGUI(player).open();
} else {
final PlayerLanguage playerLanguage = new PlayerLanguage(player);
player.sendMessage(playerLanguage.translate(LanguageKey.Error.PERMISSION, true));
}
return false;
}
sender.sendMessage("This plugin can only be used in-game. Sorry!");
return false;
@Override
public boolean canUse(CommandSender sender) {
return sender instanceof Player && sender.isOp() || sender.hasPermission(permission());
}
@Override
public @Nullable String permission() {
return "nicko.use";
}
}

View file

@ -11,10 +11,12 @@ public class NickoPluginLoader implements PluginLoader {
@Override
public void classloader(PluginClasspathBuilder pluginClasspathBuilder) {
final MavenLibraryResolver resolver = new MavenLibraryResolver();
resolver.addRepository(new RemoteRepository.Builder("xenondevs", "default", "https://repo.xenondevs.xyz/releases/").build());
resolver.addRepository(new RemoteRepository.Builder("wesjd", "default", "https://repo.codemc.io/repository/maven-snapshots/").build());
resolver.addRepository(new RemoteRepository.Builder("codemc", "default", "https://repo.codemc.io/repository/maven-snapshots/").build());
resolver.addDependency(new Dependency(new DefaultArtifact("xyz.xenondevs.invui:invui:pom:1.44"), null));
resolver.addDependency(new Dependency(new DefaultArtifact("net:wesjd:anvilgui:1.10.4-SNAPSHOT"), null));
//resolver.addDependency(new Dependency(new DefaultArtifact("net.wesjd:anvilgui:1.10.4-SNAPSHOT"), null));
pluginClasspathBuilder.addLibrary(resolver);
}
}

View file

@ -1,17 +1,19 @@
name: Nicko
main: xyz.ineanto.nicko.Nicko
loader: xyz.ineanto.nicko.loader.NickoPluginLoader
version: ${version}
author: Ineanto
description: "The feature packed, next generation disguise plugin for Minecraft."
api-version: 1.21
api-version: "1.21"
softdepend: [ PlaceholderAPI ]
depend:
- ProtocolLib
load: POSTWORLD
commands:
nicko:
description: "Opens Nicko's GUI."
permission: nicko.use
# Suppose we require ProtocolLib to be loaded for our plugin
AnvilGUI:
join-classpath: true
required: true
permissions:
nicko.*:
default: op