diff --git a/pom.xml b/pom.xml
index 3334243..2bde20c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -67,6 +67,12 @@
5.14.0
test
+
+ org.junit.platform
+ junit-platform-launcher
+ 1.14.0
+ test
+
org.mockbukkit.mockbukkit
mockbukkit-v1.21
@@ -113,7 +119,7 @@
maven-surefire-plugin
3.5.4
- @{argLine} -javaagent:${org.mockito:mockito-core:jar} -Xshare:off
+ @{argLine} -javaagent:${org.mockito:mockito-core:jar} -javaagent:${net.bytebuddy:byte-buddy-agent:jar} -Xshare:off
true
diff --git a/src/main/java/net/tfminecraft/surgery/SurgeryPlugin.java b/src/main/java/net/tfminecraft/surgery/SurgeryPlugin.java
index 76aeef4..c0bbaf5 100644
--- a/src/main/java/net/tfminecraft/surgery/SurgeryPlugin.java
+++ b/src/main/java/net/tfminecraft/surgery/SurgeryPlugin.java
@@ -72,7 +72,7 @@ public void onDisable() {
// a new classloader, so the instanceof check in the listener no longer
// matches and players could pull items out of stale menus
for (Player player : getServer().getOnlinePlayers()) {
- if (player.getOpenInventory().getTopInventory().getHolder() instanceof SurgeryMenuHolder) {
+ if (player.getOpenInventory().getTopInventory().getHolder(false) instanceof SurgeryMenuHolder) {
player.closeInventory();
}
}
diff --git a/src/main/java/net/tfminecraft/surgery/managers/SurgeryCompletionHandler.java b/src/main/java/net/tfminecraft/surgery/managers/SurgeryCompletionHandler.java
index 2e9ef20..1f341be 100644
--- a/src/main/java/net/tfminecraft/surgery/managers/SurgeryCompletionHandler.java
+++ b/src/main/java/net/tfminecraft/surgery/managers/SurgeryCompletionHandler.java
@@ -191,7 +191,7 @@ private void reportFailure(Player surgeon, String message, String extra) {
private Inventory surgeryMenu(Player surgeon, Outcome outcome) {
Inventory menu = surgeon.getOpenInventory().getTopInventory();
- return menu.getHolder() instanceof SurgeryMenuHolder holder && holder.matches(surgeon, outcome.operationId())
+ return menu.getHolder(false) instanceof SurgeryMenuHolder holder && holder.matches(surgeon, outcome.operationId())
? menu : null;
}
diff --git a/src/main/java/net/tfminecraft/surgery/managers/SurgeryMenuManager.java b/src/main/java/net/tfminecraft/surgery/managers/SurgeryMenuManager.java
index 9c641f1..75e7c30 100644
--- a/src/main/java/net/tfminecraft/surgery/managers/SurgeryMenuManager.java
+++ b/src/main/java/net/tfminecraft/surgery/managers/SurgeryMenuManager.java
@@ -89,7 +89,7 @@ public void openSurgeryMenu(Player surgeon, Player patient, HealingInjury ailmen
// Checks if an inventory is the surgery menu (by holder, not title)
// ==============================================
public boolean isSurgeryMenu(org.bukkit.inventory.Inventory inventory) {
- return inventory != null && inventory.getHolder() instanceof SurgeryMenuHolder;
+ return inventory != null && inventory.getHolder(false) instanceof SurgeryMenuHolder;
}
// ==============================================
@@ -103,7 +103,7 @@ public void handleItemClick(Player player, Inventory inventory, ItemStack clicke
}
private boolean ownsMenu(Player player, Inventory inventory) {
- return inventory.getHolder() instanceof SurgeryMenuHolder holder
+ return inventory.getHolder(false) instanceof SurgeryMenuHolder holder
&& holder.matches(player, stateManager.getOperationId(player.getUniqueId()));
}
diff --git a/src/test/java/net/tfminecraft/surgery/HolderLookupPatch.java b/src/test/java/net/tfminecraft/surgery/HolderLookupPatch.java
new file mode 100644
index 0000000..a6294e6
--- /dev/null
+++ b/src/test/java/net/tfminecraft/surgery/HolderLookupPatch.java
@@ -0,0 +1,91 @@
+package net.tfminecraft.surgery;
+
+import static net.bytebuddy.matcher.ElementMatchers.is;
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import net.bytebuddy.agent.ByteBuddyAgent;
+import net.bytebuddy.agent.builder.AgentBuilder;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.dynamic.DynamicType;
+import net.bytebuddy.utility.JavaModule;
+import org.bukkit.inventory.Inventory;
+import org.bukkit.inventory.InventoryHolder;
+import org.junit.platform.launcher.LauncherSession;
+import org.junit.platform.launcher.LauncherSessionListener;
+import org.mockbukkit.mockbukkit.inventory.InventoryMock;
+
+/**
+ * MockBukkit throws on Paper's {@code getHolder(false)}. Its inventories hold no block snapshot,
+ * so the patch answers with the plain holder, which is what Paper returns for plugin GUIs.
+ */
+public final class HolderLookupPatch implements LauncherSessionListener {
+ private static boolean installed;
+
+ @Override
+ public void launcherSessionOpened(LauncherSession session) {
+ install();
+ }
+
+ /** Patches {@link InventoryMock#getHolder(boolean)} once per test JVM. */
+ public static synchronized void install() {
+ if (installed) {
+ return;
+ }
+ AtomicBoolean patched = new AtomicBoolean();
+ AtomicBoolean rejected = new AtomicBoolean();
+ new AgentBuilder.Default()
+ .disableClassFormatChanges()
+ .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
+ .with(AgentBuilder.InitializationStrategy.NoOp.INSTANCE)
+ .with(AgentBuilder.TypeStrategy.Default.REDEFINE)
+ .with(AgentBuilder.Listener.StreamWriting.toSystemError().withErrorsOnly())
+ .with(new AgentBuilder.Listener.Adapter() {
+ @Override
+ public void onTransformation(TypeDescription type, ClassLoader loader, JavaModule module,
+ boolean loaded, DynamicType dynamicType) {
+ if (type.getName().equals(InventoryMock.class.getName())) {
+ patched.set(true);
+ }
+ }
+
+ // The JVM can still reject the new bytes after onTransformation.
+ @Override
+ public void onError(String typeName, ClassLoader loader, JavaModule module, boolean loaded,
+ Throwable throwable) {
+ if (typeName.equals(InventoryMock.class.getName())) {
+ rejected.set(true);
+ }
+ }
+ })
+ .type(is(InventoryMock.class))
+ .transform((builder, type, loader, module, domain) -> builder.visit(Advice.to(PlainHolder.class)
+ .on(named("getHolder").and(takesArguments(boolean.class)))))
+ .installOn(ByteBuddyAgent.install());
+ // A failed retransformation only reaches the listeners, so check it took before marking it done.
+ if (!patched.get() || rejected.get()) {
+ throw new IllegalStateException("Could not patch InventoryMock#getHolder(boolean)");
+ }
+ installed = true;
+ }
+
+ /** Skips the throwing body and returns {@code getHolder()} instead. */
+ static final class PlainHolder {
+ private PlainHolder() {
+ }
+
+ @Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class)
+ static boolean skip() {
+ return true;
+ }
+
+ @Advice.OnMethodExit
+ static void exit(@Advice.This Inventory inventory,
+ @Advice.Return(readOnly = false) InventoryHolder holder) {
+ holder = inventory.getHolder();
+ }
+ }
+}
diff --git a/src/test/resources/META-INF/services/org.junit.platform.launcher.LauncherSessionListener b/src/test/resources/META-INF/services/org.junit.platform.launcher.LauncherSessionListener
new file mode 100644
index 0000000..bd70596
--- /dev/null
+++ b/src/test/resources/META-INF/services/org.junit.platform.launcher.LauncherSessionListener
@@ -0,0 +1 @@
+net.tfminecraft.surgery.HolderLookupPatch