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