diff --git a/src/main/java/games/dmg/spigottyrant/BukkitRoleControlItemService.java b/src/main/java/games/dmg/spigottyrant/BukkitRoleControlItemService.java index 27207f5..cb3442d 100644 --- a/src/main/java/games/dmg/spigottyrant/BukkitRoleControlItemService.java +++ b/src/main/java/games/dmg/spigottyrant/BukkitRoleControlItemService.java @@ -79,6 +79,18 @@ public final class BukkitRoleControlItemService implements RoleControlItemServic game, player.getUniqueId() ); boolean retained = false; + ItemStack cursor = player.getItemOnCursor(); + if (isBoundRoleControlItem(cursor)) { + if (owner(cursor).filter(player.getUniqueId()::equals).isPresent() && role(cursor).equals(required)) { + retained = true; // Preserve the copy the owner is actively moving. + if (cursor.getAmount() > 1) { + cursor.setAmount(1); + player.setItemOnCursor(cursor); + } + } else { + player.setItemOnCursor(null); + } + } ItemStack[] contents = player.getInventory().getContents(); for (int index = 0; index < contents.length; index++) { ItemStack item = contents[index]; @@ -90,6 +102,10 @@ public final class BukkitRoleControlItemService implements RoleControlItemServic && role(item).equals(required); if (valid) { retained = true; + if (item.getAmount() > 1) { + item.setAmount(1); + player.getInventory().setItem(index, item); + } } else { player.getInventory().setItem(index, null); } @@ -98,6 +114,9 @@ public final class BukkitRoleControlItemService implements RoleControlItemServic @Override public void removeAll(Player player) { + if (isBoundRoleControlItem(player.getItemOnCursor())) { + player.setItemOnCursor(null); + } ItemStack[] contents = player.getInventory().getContents(); for (int index = 0; index < contents.length; index++) { if (isBoundRoleControlItem(contents[index])) { @@ -127,6 +146,11 @@ public final class BukkitRoleControlItemService implements RoleControlItemServic } private boolean contains(Player player, RoleControl role) { + ItemStack cursor = player.getItemOnCursor(); + if (owner(cursor).filter(player.getUniqueId()::equals).isPresent() + && role(cursor).filter(role::equals).isPresent()) { + return true; + } for (ItemStack item : player.getInventory().getContents()) { if (owner(item).filter(player.getUniqueId()::equals).isPresent() && role(item).filter(role::equals).isPresent()) { diff --git a/src/main/java/games/dmg/spigottyrant/RoleControlItemListener.java b/src/main/java/games/dmg/spigottyrant/RoleControlItemListener.java index a9b3f47..1823acc 100644 --- a/src/main/java/games/dmg/spigottyrant/RoleControlItemListener.java +++ b/src/main/java/games/dmg/spigottyrant/RoleControlItemListener.java @@ -89,12 +89,28 @@ public final class RoleControlItemListener implements Listener { @EventHandler public void onInventoryClick(InventoryClickEvent event) { + Player player = event.getWhoClicked() instanceof Player clicked ? clicked : null; + ItemStack swapSource = null; + if (player != null) { + if (event.getHotbarButton() >= 0 && event.getHotbarButton() < 9) { + swapSource = player.getInventory().getItem(event.getHotbarButton()); + } else if (event.getClick() == org.bukkit.event.inventory.ClickType.SWAP_OFFHAND) { + swapSource = player.getInventory().getItemInOffHand(); + } + } if (items.isBoundRoleControlItem(event.getCurrentItem()) - || items.isBoundRoleControlItem(event.getCursor())) { - if (!(event.getWhoClicked() instanceof Player player) - || event.isShiftClick() - || event.getClickedInventory() == null - || !event.getClickedInventory().equals(player.getInventory())) { + || items.isBoundRoleControlItem(event.getCursor()) || items.isBoundRoleControlItem(swapSource)) { + var action = event.getAction(); + boolean scansOtherSlots = event.isShiftClick() + || action == org.bukkit.event.inventory.InventoryAction.MOVE_TO_OTHER_INVENTORY + || action == org.bukkit.event.inventory.InventoryAction.COLLECT_TO_CURSOR; + if (player == null || event.getClickedInventory() == null + || !event.getClickedInventory().equals(player.getInventory()) + || !validFor(player, event.getCurrentItem()) || !validFor(player, event.getCursor()) + || !validFor(player, swapSource) + || action == org.bukkit.event.inventory.InventoryAction.CLONE_STACK + || action == org.bukkit.event.inventory.InventoryAction.UNKNOWN + || scansOtherSlots && !personalCraftingView(event.getView())) { event.setCancelled(true); } } @@ -105,10 +121,28 @@ public final class RoleControlItemListener implements Listener { if (items.isBoundRoleControlItem(event.getOldCursor()) || event.getNewItems().values().stream() .anyMatch(items::isBoundRoleControlItem)) { - event.setCancelled(true); + if (!(event.getWhoClicked() instanceof Player player) + || !validFor(player, event.getOldCursor()) + || event.getNewItems().values().stream().anyMatch(item -> !validFor(player, item)) + || event.getRawSlots().stream().anyMatch(raw -> + !player.getInventory().equals(event.getView().getInventory(raw)))) { + event.setCancelled(true); + } } } + private boolean validFor(Player player, ItemStack item) { + return !items.isBoundRoleControlItem(item) + || items.owner(item).filter(player.getUniqueId()::equals).isPresent() + && items.role(item).equals(RoleControlItemPolicy.requiredRole(stateManager.game(), player.getUniqueId())); + } + + private static boolean personalCraftingView(org.bukkit.inventory.InventoryView view) { + // The native player view has a 2x2 matrix; a workbench has 3x3 and is an external destination. + return view.getTopInventory() instanceof org.bukkit.inventory.CraftingInventory crafting + && crafting.getMatrix().length == 4; + } + @EventHandler public void onInventoryMove(InventoryMoveItemEvent event) { if (items.isBoundRoleControlItem(event.getItem())) { diff --git a/src/test/java/games/dmg/spigottyrant/BukkitRoleControlItemServiceTest.java b/src/test/java/games/dmg/spigottyrant/BukkitRoleControlItemServiceTest.java new file mode 100644 index 0000000..eaec356 --- /dev/null +++ b/src/test/java/games/dmg/spigottyrant/BukkitRoleControlItemServiceTest.java @@ -0,0 +1,150 @@ +package games.dmg.spigottyrant; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.time.Duration; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicReference; +import org.bukkit.NamespacedKey; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.plugin.Plugin; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +class BukkitRoleControlItemServiceTest { + private final UUID owner = UUID.randomUUID(); + private final Player player = mock(Player.class); + private final PlayerInventory inventory = mock(PlayerInventory.class); + private final ItemStack[] contents = new ItemStack[41]; + private final AtomicReference cursor = new AtomicReference<>(); + private BukkitRoleControlItemService service; + + @BeforeEach + void setup() { + Plugin plugin = mock(Plugin.class); + when(plugin.getName()).thenReturn("SpigotTyrant"); + service = new BukkitRoleControlItemService(plugin, PluginSettings.from(Map.of())); + when(player.getUniqueId()).thenReturn(owner); + when(player.getInventory()).thenReturn(inventory); + when(inventory.getContents()).thenAnswer(call -> contents.clone()); + doAnswer(call -> { contents[call.getArgument(0, Integer.class)] = call.getArgument(1); return null; }) + .when(inventory).setItem(anyInt(), nullable(ItemStack.class)); + when(player.getItemOnCursor()).thenAnswer(call -> cursor.get()); + doAnswer(call -> { cursor.set(call.getArgument(0)); return null; }) + .when(player).setItemOnCursor(nullable(ItemStack.class)); + } + + @ParameterizedTest + @EnumSource(RoleControl.class) + void recoveryDoesNotDuplicateOrRepositionCursorHeldControlItem(RoleControl role) { + ItemStack held = item(owner, role); + cursor.set(held); + try (var created = mockConstruction(ItemStack.class, (item, context) -> { + ItemMeta meta = mock(ItemMeta.class); + when(item.getItemMeta()).thenReturn(meta); + when(meta.getPersistentDataContainer()).thenReturn(mock(PersistentDataContainer.class)); + })) { + for (int attempt = 0; attempt < 3; attempt++) { + service.recover(player, game(role)); + } + assertEquals(0, created.constructed().size(), "Cursor-held item already satisfies recovery"); + assertSame(held, cursor.get()); + verify(inventory, never()).addItem(any(ItemStack[].class)); + verify(inventory, never()).setItem(anyInt(), nullable(ItemStack.class)); + } + } + + @ParameterizedTest + @EnumSource(RoleControl.class) + void refreshPreservesCursorChoiceAndRemovesExtraInventoryCopies(RoleControl role) { + ItemStack held = item(owner, role); + cursor.set(held); + contents[5] = item(owner, role); + ItemStack ordinary = mock(ItemStack.class); + contents[8] = ordinary; + var manager = mock(TyrantStateManager.class); + when(manager.game()).thenReturn(game(role)); + var server = mock(org.bukkit.Server.class); + doReturn(java.util.List.of(player)).when(server).getOnlinePlayers(); + var refresh = new RoleControlItemRefreshTask(manager, service, server); + for (int attempt = 0; attempt < 3; attempt++) { + refresh.run(); + } + assertSame(held, cursor.get()); + assertNull(contents[5], "Keep the actively moved cursor copy, not an old recovery duplicate"); + assertSame(ordinary, contents[8]); + verify(inventory, never()).addItem(any(ItemStack[].class)); + } + + @ParameterizedTest + @EnumSource(RoleControl.class) + void roleLossRemovesCursorAndInventoryCopies(RoleControl role) { + cursor.set(item(owner, role)); + contents[5] = item(owner, role); + service.removeInvalid(player, game(null)); + assertNull(cursor.get()); + assertNull(contents[5]); + } + + @ParameterizedTest + @EnumSource(RoleControl.class) + void explicitRemovalIncludesCursorWithoutTouchingOrdinaryItems(RoleControl role) { + cursor.set(item(owner, role)); + contents[5] = item(owner, role); + ItemStack ordinary = mock(ItemStack.class); + contents[8] = ordinary; + service.removeAll(player); + assertNull(cursor.get()); + assertNull(contents[5]); + assertSame(ordinary, contents[8]); + } + + @ParameterizedTest + @EnumSource(RoleControl.class) + void mergedDuplicateStacksAreReducedWithoutMovingTheCanonicalItem(RoleControl role) { + for (boolean heldOnCursor : new boolean[] {false, true}) { + java.util.Arrays.fill(contents, null); + cursor.set(null); + ItemStack held = item(owner, role); + held.setAmount(2); + if (heldOnCursor) { cursor.set(held); } else { contents[5] = held; } + service.removeInvalid(player, game(role)); + assertEquals(1, held.getAmount(), "Recovery duplicates may have merged into one stack"); + assertSame(held, heldOnCursor ? cursor.get() : contents[5]); + } + } + + private ItemStack item(UUID playerId, RoleControl role) { + ItemStack item = mock(ItemStack.class); + ItemMeta meta = mock(ItemMeta.class); + PersistentDataContainer data = mock(PersistentDataContainer.class); + when(item.hasItemMeta()).thenReturn(true); + when(item.getItemMeta()).thenReturn(meta); + var amount = new java.util.concurrent.atomic.AtomicInteger(1); + when(item.getAmount()).thenAnswer(call -> amount.get()); + doAnswer(call -> { amount.set(call.getArgument(0)); return null; }).when(item).setAmount(anyInt()); + when(meta.getPersistentDataContainer()).thenReturn(data); + when(data.get(new NamespacedKey("spigottyrant", "role-control"), PersistentDataType.STRING)) + .thenReturn(role.name()); + when(data.get(new NamespacedKey("spigottyrant", "role-control-owner"), PersistentDataType.STRING)) + .thenReturn(playerId.toString()); + return item; + } + + private GameState game(RoleControl role) { + return new GameState(GameLifecycle.RUNNING, + role == RoleControl.TYRANT ? Optional.of(owner) : Optional.empty(), + role == RoleControl.VIGILANTE ? Optional.of(owner) : Optional.empty(), + Optional.empty(), Optional.empty(), Optional.empty(), Duration.ZERO, 0, 0, Set.of()); + } +} diff --git a/src/test/java/games/dmg/spigottyrant/RoleControlInventoryMovementTest.java b/src/test/java/games/dmg/spigottyrant/RoleControlInventoryMovementTest.java new file mode 100644 index 0000000..065ed90 --- /dev/null +++ b/src/test/java/games/dmg/spigottyrant/RoleControlInventoryMovementTest.java @@ -0,0 +1,257 @@ +package games.dmg.spigottyrant; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicReference; +import org.bukkit.NamespacedKey; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.event.inventory.InventoryAction; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryDragEvent; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryView; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.plugin.Plugin; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +class RoleControlInventoryMovementTest { + @ParameterizedTest + @EnumSource(RoleControl.class) + void ownerCanShiftBetweenOwnInventorySections(RoleControl role) { + Fixture f = new Fixture(role, true); + f.slots.put(10, f.bound(f.owner, role)); + var event = f.click(10, ClickType.SHIFT_LEFT, InventoryAction.MOVE_TO_OTHER_INVENTORY, -1); + f.listener.onInventoryClick(event); + assertFalse(event.isCancelled(), "Personal inventory shift stays in owner inventory"); + } + + @ParameterizedTest + @EnumSource(RoleControl.class) + void ownerCanDragAcrossOnlyTheirInventorySlots(RoleControl role) { + Fixture f = new Fixture(role, false); + ItemStack item = f.bound(f.owner, role); + var event = new InventoryDragEvent(f.view, null, item, false, Map.of(30, item)); + f.listener.onInventoryDrag(event); + assertFalse(event.isCancelled(), "An open chest must not prevent safe bottom-inventory drag"); + } + + @ParameterizedTest + @EnumSource(RoleControl.class) + void boundHotbarSourceCannotBeSwappedIntoAContainer(RoleControl role) { + Fixture f = new Fixture(role, false); + ItemStack source = f.bound(f.owner, role); + when(f.inventory.getItem(2)).thenReturn(source); + f.slots.put(0, mock(ItemStack.class)); + var event = f.click(0, ClickType.NUMBER_KEY, InventoryAction.HOTBAR_SWAP, 2); + f.listener.onInventoryClick(event); + assertTrue(event.isCancelled(), "Inspect the hotbar source, not only clicked item/cursor"); + } + + @ParameterizedTest + @EnumSource(RoleControl.class) + void boundOffhandSourceCannotBeSwappedIntoAContainer(RoleControl role) { + Fixture f = new Fixture(role, false); + ItemStack source = f.bound(f.owner, role); + when(f.inventory.getItemInOffHand()).thenReturn(source); + var event = f.click(0, ClickType.SWAP_OFFHAND, InventoryAction.HOTBAR_SWAP, -1); + f.listener.onInventoryClick(event); + assertTrue(event.isCancelled()); + } + + @ParameterizedTest + @EnumSource(RoleControl.class) + void creativeCloneDoesNotCreateTransferableOrExtraRoleItems(RoleControl role) { + Fixture f = new Fixture(role, true); + f.slots.put(10, f.bound(f.owner, role)); + var event = f.click(10, ClickType.MIDDLE, InventoryAction.CLONE_STACK, -1); + f.listener.onInventoryClick(event); + assertTrue(event.isCancelled()); + } + + @ParameterizedTest + @EnumSource(RoleControl.class) + void ordinaryCursorMovesAndInternalHotbarSwapsStayAllowed(RoleControl role) { + Fixture f = new Fixture(role, false); + ItemStack bound = f.bound(f.owner, role); + f.slots.put(30, bound); + var pickup = f.click(30, ClickType.LEFT, InventoryAction.PICKUP_ALL, -1); + f.listener.onInventoryClick(pickup); + assertFalse(pickup.isCancelled()); + f.slots.remove(30); + f.cursor.set(bound); + var place = f.click(31, ClickType.RIGHT, InventoryAction.PLACE_ONE, -1); + f.listener.onInventoryClick(place); + assertFalse(place.isCancelled()); + f.cursor.set(null); + when(f.inventory.getItem(2)).thenReturn(bound); + var swap = f.click(32, ClickType.NUMBER_KEY, InventoryAction.HOTBAR_SWAP, 2); + f.listener.onInventoryClick(swap); + assertFalse(swap.isCancelled()); + when(f.inventory.getItemInOffHand()).thenReturn(bound); + var offhand = f.click(32, ClickType.SWAP_OFFHAND, InventoryAction.HOTBAR_SWAP, -1); + f.listener.onInventoryClick(offhand); + assertFalse(offhand.isCancelled()); + } + + @ParameterizedTest + @EnumSource(RoleControl.class) + void crossInventoryShiftsDragsAndCollectionAreBlocked(RoleControl role) { + Fixture f = new Fixture(role, false); + ItemStack bound = f.bound(f.owner, role); + f.slots.put(30, bound); + var shift = f.click(30, ClickType.SHIFT_LEFT, InventoryAction.MOVE_TO_OTHER_INVENTORY, -1); + f.listener.onInventoryClick(shift); + assertTrue(shift.isCancelled()); + f.cursor.set(bound); + var place = f.click(0, ClickType.LEFT, InventoryAction.PLACE_ALL, -1); + f.listener.onInventoryClick(place); + assertTrue(place.isCancelled()); + var drag = new InventoryDragEvent(f.view, null, bound, false, Map.of(30, bound, 0, bound)); + f.listener.onInventoryDrag(drag); + assertTrue(drag.isCancelled()); + var collect = f.click(30, ClickType.DOUBLE_CLICK, InventoryAction.COLLECT_TO_CURSOR, -1); + f.listener.onInventoryClick(collect); + assertTrue(collect.isCancelled(), "Collection also scans the external inventory"); + var outside = f.click(-999, ClickType.LEFT, InventoryAction.DROP_ALL_CURSOR, -1); + f.listener.onInventoryClick(outside); + assertTrue(outside.isCancelled()); + } + + @ParameterizedTest + @EnumSource(RoleControl.class) + void foreignControlItemsCannotBeRetainedThroughMovement(RoleControl role) { + Fixture f = new Fixture(role, true); + ItemStack foreign = f.bound(UUID.randomUUID(), role); + f.slots.put(10, foreign); + var pickup = f.click(10, ClickType.LEFT, InventoryAction.PICKUP_ALL, -1); + f.listener.onInventoryClick(pickup); + assertTrue(pickup.isCancelled()); + f.slots.clear(); + when(f.inventory.getItem(2)).thenReturn(foreign); + var swap = f.click(10, ClickType.NUMBER_KEY, InventoryAction.HOTBAR_SWAP, 2); + f.listener.onInventoryClick(swap); + assertTrue(swap.isCancelled()); + var drag = new InventoryDragEvent(f.view, null, foreign, false, Map.of(10, foreign)); + f.listener.onInventoryDrag(drag); + assertTrue(drag.isCancelled()); + } + + @ParameterizedTest + @EnumSource(RoleControl.class) + void craftingGridAndWorkbenchRemainExternalDestinations(RoleControl role) { + Fixture own = new Fixture(role, true); + own.cursor.set(own.bound(own.owner, role)); + var grid = own.click(1, ClickType.LEFT, InventoryAction.PLACE_ALL, -1); + own.listener.onInventoryClick(grid); + assertTrue(grid.isCancelled()); + Fixture bench = new Fixture(role, 9); + bench.slots.put(15, bench.bound(bench.owner, role)); + var shift = bench.click(15, ClickType.SHIFT_LEFT, InventoryAction.MOVE_TO_OTHER_INVENTORY, -1); + bench.listener.onInventoryClick(shift); + assertTrue(shift.isCancelled(), "Only the personal 2x2 view has internal shift routing"); + } + + @ParameterizedTest + @EnumSource(RoleControl.class) + void cancelledSafeMovesStayCancelledAndOrdinaryItemsAreUnaffected(RoleControl role) { + Fixture f = new Fixture(role, true); + f.slots.put(10, f.bound(f.owner, role)); + var event = f.click(10, ClickType.LEFT, InventoryAction.PICKUP_ALL, -1); + event.setCancelled(true); + f.listener.onInventoryClick(event); + assertTrue(event.isCancelled()); + Fixture chest = new Fixture(role, false); + ItemStack ordinary = mock(ItemStack.class); + when(ordinary.clone()).thenReturn(ordinary); + chest.slots.put(30, ordinary); + var shift = chest.click(30, ClickType.SHIFT_LEFT, InventoryAction.MOVE_TO_OTHER_INVENTORY, -1); + chest.listener.onInventoryClick(shift); + assertFalse(shift.isCancelled()); + var drag = new InventoryDragEvent(chest.view, null, ordinary, false, Map.of(0, ordinary)); + chest.listener.onInventoryDrag(drag); + assertFalse(drag.isCancelled()); + } + + private static final class Fixture { + private final UUID owner = UUID.randomUUID(); + private final Player player = mock(Player.class); + private final PlayerInventory inventory = mock(PlayerInventory.class); + private final InventoryView view = mock(InventoryView.class); + private final Map slots = new HashMap<>(); + private final AtomicReference cursor = new AtomicReference<>(); + private final RoleControlItemListener listener; + + Fixture(RoleControl role, boolean personalCrafting) { + this(role, personalCrafting ? 4 : 0); + } + + Fixture(RoleControl role, int matrixSize) { + int topSize = matrixSize > 0 ? matrixSize + 1 : 27; + when(player.getUniqueId()).thenReturn(owner); + when(player.getInventory()).thenReturn(inventory); + when(player.getItemOnCursor()).thenAnswer(call -> cursor.get()); + when(view.getPlayer()).thenReturn(player); + when(view.getBottomInventory()).thenReturn(inventory); + Inventory top; + if (matrixSize > 0) { + var crafting = mock(org.bukkit.inventory.CraftingInventory.class); + when(crafting.getMatrix()).thenReturn(new ItemStack[matrixSize]); + top = crafting; + } else { + top = mock(Inventory.class); + } + when(top.getSize()).thenReturn(topSize); + when(view.getTopInventory()).thenReturn(top); + when(view.getInventory(anyInt())).thenAnswer(call -> { + int raw = call.getArgument(0); + return raw < 0 || raw >= topSize + 41 ? null : raw < topSize ? top : inventory; + }); + when(view.convertSlot(anyInt())).thenAnswer(call -> call.getArgument(0, Integer.class) - topSize); + when(view.getItem(anyInt())).thenAnswer(call -> slots.get(call.getArgument(0))); + when(view.getCursor()).thenAnswer(call -> cursor.get()); + Plugin plugin = mock(Plugin.class); + when(plugin.getName()).thenReturn("SpigotTyrant"); + var items = new BukkitRoleControlItemService(plugin, PluginSettings.from(Map.of())); + var manager = mock(TyrantStateManager.class); + when(manager.game()).thenReturn(new GameState(GameLifecycle.RUNNING, + role == RoleControl.TYRANT ? Optional.of(owner) : Optional.empty(), + role == RoleControl.VIGILANTE ? Optional.of(owner) : Optional.empty(), + Optional.empty(), Optional.empty(), Optional.empty(), Duration.ZERO, 0, 0, Set.of())); + listener = new RoleControlItemListener(manager, items, mock(TyrantControlPanel.class), + mock(VigilanteControlPanel.class)); + } + + InventoryClickEvent click(int raw, ClickType click, InventoryAction action, int hotbar) { + return new InventoryClickEvent(view, InventoryType.SlotType.CONTAINER, raw, click, action, hotbar); + } + + ItemStack bound(UUID id, RoleControl role) { + ItemStack item = mock(ItemStack.class); + ItemMeta meta = mock(ItemMeta.class); + PersistentDataContainer data = mock(PersistentDataContainer.class); + when(item.clone()).thenReturn(item); + when(item.hasItemMeta()).thenReturn(true); + when(item.getItemMeta()).thenReturn(meta); + when(meta.getPersistentDataContainer()).thenReturn(data); + when(data.get(new NamespacedKey("spigottyrant", "role-control"), PersistentDataType.STRING)) + .thenReturn(role.name()); + when(data.get(new NamespacedKey("spigottyrant", "role-control-owner"), PersistentDataType.STRING)) + .thenReturn(id.toString()); + return item; + } + } +}