diff --git a/.gitignore b/.gitignore
index a528356f..e2c8d4ac 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,3 +17,6 @@ src/libs
*/google-java-format.xml
dependency-reduced-pom.xml
+
+# paper-nms:init task temporary folder
+.paper-nms/
\ No newline at end of file
diff --git a/Mojang1_21_R4/pom.xml b/Mojang1_21_R4/pom.xml
new file mode 100644
index 00000000..3c25ed71
--- /dev/null
+++ b/Mojang1_21_R4/pom.xml
@@ -0,0 +1,56 @@
+
+
+ 4.0.0
+
+
+ net.wesjd
+ anvilgui-parent
+ 1.10.6-SNAPSHOT
+
+
+ anvilgui-1_21_R4-mojang
+
+
+
+ ca.bkaw
+ paper-nms
+ 1.21.5-SNAPSHOT
+ provided
+
+
+ net.wesjd
+ anvilgui-abstraction
+ ${project.parent.version}
+ provided
+
+
+
+
+
+ bytecode.space
+ https://repo.bytecode.space/repository/maven-public/
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.8.1
+
+ 21
+ 21
+
+
+
+
+ ca.bkaw
+ paper-nms-maven-plugin
+ 1.4.8-SNAPSHOT
+
+
+
+
diff --git a/Mojang1_21_R4/src/main/java/net/wesjd/anvilgui/version/MojangWrapper1_21_R4.java b/Mojang1_21_R4/src/main/java/net/wesjd/anvilgui/version/MojangWrapper1_21_R4.java
new file mode 100644
index 00000000..e47b5b6a
--- /dev/null
+++ b/Mojang1_21_R4/src/main/java/net/wesjd/anvilgui/version/MojangWrapper1_21_R4.java
@@ -0,0 +1,158 @@
+package net.wesjd.anvilgui.version;
+
+import net.minecraft.core.BlockPos;
+import net.minecraft.core.RegistryAccess;
+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.inventory.*;
+import org.bukkit.craftbukkit.CraftWorld;
+import org.bukkit.craftbukkit.entity.CraftPlayer;
+import org.bukkit.craftbukkit.event.CraftEventFactory;
+import org.bukkit.entity.Player;
+import org.bukkit.event.inventory.InventoryCloseEvent;
+import org.bukkit.inventory.Inventory;
+import org.jetbrains.annotations.NotNull;
+
+public final class MojangWrapper1_21_R4 implements VersionWrapper {
+ 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();
+ }
+
+ @Override
+ public int getNextContainerId(Player player, AnvilContainerWrapper container) {
+ return ((AnvilContainer) container).getContainerId();
+ }
+
+ @Override
+ public void handleInventoryCloseEvent(Player player) {
+ CraftEventFactory.handleInventoryCloseEvent(toNMS(player), InventoryCloseEvent.Reason.UNKNOWN);
+ toNMS(player).doCloseContainer(); // p -> doCloseContainer
+ }
+
+ @Override
+ public void sendPacketOpenWindow(Player player, int containerId, Object inventoryTitle) {
+ toNMS(player).connection.send(new ClientboundOpenScreenPacket(containerId, MenuType.ANVIL, (Component)
+ inventoryTitle));
+ }
+
+ @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; // bR -> containerMenu, bQ -> 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) title);
+ }
+
+ @Override
+ public Object literalChatComponent(String content) {
+ return Component.literal(content); // IChatBaseComponent.b -> Component.literal
+ }
+
+ @Override
+ public Object jsonChatComponent(String json) {
+ return Component.Serializer.fromJson(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(((CraftWorld) player.getWorld()).getHandle(), new BlockPos(0, 0, 0)));
+ this.checkReachable = false;
+ setTitle(guiTitle);
+ }
+
+ @Override
+ public void createResult() {
+ // If the output is empty copy the left input into the output
+ Slot output = this.getSlot(2); // b -> getSlot
+ if (!output.hasItem()) { // h -> hasItem
+ output.set(this.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 void removed(net.minecraft.world.entity.player.@NotNull Player player) {}
+
+ @Override
+ protected void clearContainer(
+ net.minecraft.world.entity.player.@NotNull Player player, @NotNull Container container) {}
+
+ public int getContainerId() {
+ return this.containerId;
+ }
+
+ @Override
+ public String getRenameText() {
+ return this.itemName;
+ }
+
+ @Override
+ 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)); // DataComponents.g -> DataComponents.CUSTOM_NAME
+ }
+ }
+
+ @Override
+ public Inventory getBukkitInventory() {
+ // NOTE: We need to call Container#getBukkitView() instead of ContainerAnvil#getBukkitView()
+ // because ContainerAnvil#getBukkitView() had an ABI breakage in the middle of the Minecraft 1.21
+ // development cycle for Spigot. For more info, see: https://github.com/WesJD/AnvilGUI/issues/342
+ return ((AbstractContainerMenu) this).getBukkitView().getTopInventory();
+ }
+ }
+}
diff --git a/api/pom.xml b/api/pom.xml
index 6d893870..cb93881b 100644
--- a/api/pom.xml
+++ b/api/pom.xml
@@ -229,6 +229,12 @@
${project.parent.version}
compile
+
+ net.wesjd
+ anvilgui-1_21_R4-mojang
+ ${project.parent.version}
+ compile
+
org.geysermc.geyser
api
diff --git a/api/src/main/java/net/wesjd/anvilgui/version/VersionMatcher.java b/api/src/main/java/net/wesjd/anvilgui/version/VersionMatcher.java
index 2955e06e..8d849c9a 100644
--- a/api/src/main/java/net/wesjd/anvilgui/version/VersionMatcher.java
+++ b/api/src/main/java/net/wesjd/anvilgui/version/VersionMatcher.java
@@ -50,14 +50,42 @@ public VersionWrapper match() {
rVersion = craftBukkitPackage.split("\\.")[3].substring(1);
}
+ boolean isMojMap = isMojangMapped(rVersion);
+
try {
- return (VersionWrapper) Class.forName(getClass().getPackage().getName() + ".Wrapper" + rVersion)
- .getDeclaredConstructor()
- .newInstance();
+ return (VersionWrapper)
+ getWrapperClass(rVersion, isMojMap).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);
}
}
+
+ private Class> getWrapperClass(String version, boolean isMojMap) throws ClassNotFoundException {
+ String pkg = getClass().getPackage().getName();
+ if (isMojMap) { // if mojang-mapped server, use MojangWrapper
+ try {
+ return Class.forName(pkg + ".MojangWrapper" + version);
+ } catch (ClassNotFoundException ignored) {
+ }
+ }
+ // then try usual wrapper
+ return Class.forName(pkg + ".Wrapper" + version);
+ }
+
+ private static boolean isMojangMapped(String version) {
+ // firstly check for paper
+ try {
+ Class.forName("com.destroystokyo.paper.ParticleBuilder");
+ } catch (ClassNotFoundException ignored) {
+ return false;
+ }
+ // then check version
+ final String[] versionNumbers = version.replace("R", "").split("_");
+ int major = Integer.parseInt(versionNumbers[1]);
+ int minor = versionNumbers.length > 2 ? Integer.parseInt(versionNumbers[2]) : 0;
+ if (major == 20 && minor == 4) return true; // 1.20.5/6
+ return major > 20; // >= 1.21
+ }
}
diff --git a/pom.xml b/pom.xml
index aa51d921..e353a38a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -50,6 +50,7 @@
1_21_R3
1_21_R4
1_21_R5
+ Mojang1_21_R4