fix(tyrant): preserve bound items during inventory moves
Release / release (push) Successful in 3m8s
CI / build (push) Successful in 1m50s

This commit is contained in:
dmg
2026-09-12 08:59:35 -04:00
parent a4e80ddce0
commit 695c78ea16
4 changed files with 471 additions and 6 deletions
@@ -79,6 +79,18 @@ public final class BukkitRoleControlItemService implements RoleControlItemServic
game, player.getUniqueId() game, player.getUniqueId()
); );
boolean retained = false; 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(); ItemStack[] contents = player.getInventory().getContents();
for (int index = 0; index < contents.length; index++) { for (int index = 0; index < contents.length; index++) {
ItemStack item = contents[index]; ItemStack item = contents[index];
@@ -90,6 +102,10 @@ public final class BukkitRoleControlItemService implements RoleControlItemServic
&& role(item).equals(required); && role(item).equals(required);
if (valid) { if (valid) {
retained = true; retained = true;
if (item.getAmount() > 1) {
item.setAmount(1);
player.getInventory().setItem(index, item);
}
} else { } else {
player.getInventory().setItem(index, null); player.getInventory().setItem(index, null);
} }
@@ -98,6 +114,9 @@ public final class BukkitRoleControlItemService implements RoleControlItemServic
@Override @Override
public void removeAll(Player player) { public void removeAll(Player player) {
if (isBoundRoleControlItem(player.getItemOnCursor())) {
player.setItemOnCursor(null);
}
ItemStack[] contents = player.getInventory().getContents(); ItemStack[] contents = player.getInventory().getContents();
for (int index = 0; index < contents.length; index++) { for (int index = 0; index < contents.length; index++) {
if (isBoundRoleControlItem(contents[index])) { if (isBoundRoleControlItem(contents[index])) {
@@ -127,6 +146,11 @@ public final class BukkitRoleControlItemService implements RoleControlItemServic
} }
private boolean contains(Player player, RoleControl role) { 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()) { for (ItemStack item : player.getInventory().getContents()) {
if (owner(item).filter(player.getUniqueId()::equals).isPresent() if (owner(item).filter(player.getUniqueId()::equals).isPresent()
&& role(item).filter(role::equals).isPresent()) { && role(item).filter(role::equals).isPresent()) {
@@ -89,12 +89,28 @@ public final class RoleControlItemListener implements Listener {
@EventHandler @EventHandler
public void onInventoryClick(InventoryClickEvent event) { 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()) if (items.isBoundRoleControlItem(event.getCurrentItem())
|| items.isBoundRoleControlItem(event.getCursor())) { || items.isBoundRoleControlItem(event.getCursor()) || items.isBoundRoleControlItem(swapSource)) {
if (!(event.getWhoClicked() instanceof Player player) var action = event.getAction();
|| event.isShiftClick() boolean scansOtherSlots = event.isShiftClick()
|| event.getClickedInventory() == null || action == org.bukkit.event.inventory.InventoryAction.MOVE_TO_OTHER_INVENTORY
|| !event.getClickedInventory().equals(player.getInventory())) { || 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); event.setCancelled(true);
} }
} }
@@ -105,9 +121,27 @@ public final class RoleControlItemListener implements Listener {
if (items.isBoundRoleControlItem(event.getOldCursor()) if (items.isBoundRoleControlItem(event.getOldCursor())
|| event.getNewItems().values().stream() || event.getNewItems().values().stream()
.anyMatch(items::isBoundRoleControlItem)) { .anyMatch(items::isBoundRoleControlItem)) {
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); 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 @EventHandler
public void onInventoryMove(InventoryMoveItemEvent event) { public void onInventoryMove(InventoryMoveItemEvent event) {
@@ -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<ItemStack> 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());
}
}
@@ -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<Integer, ItemStack> slots = new HashMap<>();
private final AtomicReference<ItemStack> 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;
}
}
}