feat(sort): sort containers and player storage
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
package games.dmg.spigotinventoryhelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
public final class InventorySorter {
|
||||
public ItemStack[] sort(ItemStack[] contents) {
|
||||
Objects.requireNonNull(contents, "contents");
|
||||
List<ItemStack> items = Arrays.stream(contents)
|
||||
.filter(Objects::nonNull)
|
||||
.filter(item -> item.getType() != Material.AIR)
|
||||
.filter(item -> item.getAmount() > 0)
|
||||
.map(ItemStack::clone)
|
||||
.sorted(Comparator.comparing(item -> item.getType().name()))
|
||||
.toList();
|
||||
|
||||
List<ItemStack> packed = new ArrayList<>();
|
||||
for (ItemStack item : items) {
|
||||
pack(item, packed);
|
||||
}
|
||||
|
||||
ItemStack[] sorted = new ItemStack[contents.length];
|
||||
for (int slot = 0; slot < packed.size(); slot++) {
|
||||
sorted[slot] = packed.get(slot);
|
||||
}
|
||||
return sorted;
|
||||
}
|
||||
|
||||
private static void pack(ItemStack item, List<ItemStack> packed) {
|
||||
int remaining = item.getAmount();
|
||||
for (ItemStack existing : packed) {
|
||||
if (!existing.isSimilar(item) || existing.getAmount() >= existing.getMaxStackSize()) {
|
||||
continue;
|
||||
}
|
||||
int transferred = Math.min(
|
||||
remaining,
|
||||
existing.getMaxStackSize() - existing.getAmount()
|
||||
);
|
||||
existing.setAmount(existing.getAmount() + transferred);
|
||||
remaining -= transferred;
|
||||
if (remaining == 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
while (remaining > 0) {
|
||||
ItemStack stack = item.clone();
|
||||
int amount = Math.min(remaining, stack.getMaxStackSize());
|
||||
stack.setAmount(amount);
|
||||
packed.add(stack);
|
||||
remaining -= amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package games.dmg.spigotinventoryhelper;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.PlayerInventory;
|
||||
|
||||
public final class InventorySortingService {
|
||||
private static final int HOTBAR_SIZE = 9;
|
||||
private static final int PLAYER_STORAGE_SIZE = 36;
|
||||
|
||||
private final InventorySorter sorter;
|
||||
|
||||
public InventorySortingService(InventorySorter sorter) {
|
||||
this.sorter = Objects.requireNonNull(sorter, "sorter");
|
||||
}
|
||||
|
||||
public void sortPlayerMainStorage(PlayerInventory inventory) {
|
||||
Objects.requireNonNull(inventory, "inventory");
|
||||
ItemStack[] storage = inventory.getStorageContents().clone();
|
||||
if (storage.length < PLAYER_STORAGE_SIZE) {
|
||||
throw new IllegalArgumentException("Player storage must contain 36 slots");
|
||||
}
|
||||
ItemStack[] mainStorage = Arrays.copyOfRange(
|
||||
storage,
|
||||
HOTBAR_SIZE,
|
||||
PLAYER_STORAGE_SIZE
|
||||
);
|
||||
ItemStack[] sorted = sorter.sort(mainStorage);
|
||||
System.arraycopy(sorted, 0, storage, HOTBAR_SIZE, sorted.length);
|
||||
inventory.setStorageContents(storage);
|
||||
}
|
||||
|
||||
public void sortContainer(Inventory inventory) {
|
||||
Objects.requireNonNull(inventory, "inventory");
|
||||
inventory.setContents(sorter.sort(inventory.getContents()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package games.dmg.spigotinventoryhelper;
|
||||
|
||||
import java.util.Objects;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.block.Barrel;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.Chest;
|
||||
import org.bukkit.block.Container;
|
||||
import org.bukkit.block.Lockable;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.block.Action;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
import org.bukkit.inventory.EquipmentSlot;
|
||||
|
||||
public final class SortingStickListener implements Listener {
|
||||
private final SortingStickItemFactory sticks;
|
||||
private final InventorySortingService sorting;
|
||||
|
||||
public SortingStickListener(
|
||||
SortingStickItemFactory sticks,
|
||||
InventorySortingService sorting
|
||||
) {
|
||||
this.sticks = Objects.requireNonNull(sticks, "sticks");
|
||||
this.sorting = Objects.requireNonNull(sorting, "sorting");
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onInteract(PlayerInteractEvent event) {
|
||||
if (event.isCancelled()
|
||||
|| event.getHand() != EquipmentSlot.HAND
|
||||
|| !isRightClick(event.getAction())
|
||||
|| !sticks.isSortingStick(event.getItem())) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.setCancelled(true);
|
||||
Player player = event.getPlayer();
|
||||
Block clicked = event.getClickedBlock();
|
||||
if (clicked != null) {
|
||||
Object state = clicked.getState();
|
||||
if (state instanceof Chest || state instanceof Barrel) {
|
||||
if (((Lockable) state).isLocked()) {
|
||||
player.sendActionBar(Component.text(
|
||||
"That container is locked.",
|
||||
NamedTextColor.RED
|
||||
));
|
||||
return;
|
||||
}
|
||||
sorting.sortContainer(((Container) state).getInventory());
|
||||
player.sendActionBar(Component.text("Container sorted.", NamedTextColor.GREEN));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
sorting.sortPlayerMainStorage(player.getInventory());
|
||||
player.sendActionBar(Component.text("Inventory sorted.", NamedTextColor.GREEN));
|
||||
}
|
||||
|
||||
private static boolean isRightClick(Action action) {
|
||||
return action == Action.RIGHT_CLICK_AIR || action == Action.RIGHT_CLICK_BLOCK;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,11 @@ public final class SpigotInventoryHelperPlugin extends JavaPlugin {
|
||||
if (!recipes.register(getServer())) {
|
||||
getLogger().warning("One or more Sorting Stick recipes could not be registered.");
|
||||
}
|
||||
InventorySortingService sorting = new InventorySortingService(new InventorySorter());
|
||||
getServer().getPluginManager().registerEvents(
|
||||
new SortingStickListener(sortingSticks, sorting),
|
||||
this
|
||||
);
|
||||
getLogger().info("Spigot Inventory Helper enabled.");
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package games.dmg.spigotinventoryhelper;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertAll;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class InventorySorterTest {
|
||||
@Test
|
||||
void consolidatesStacksAndOrdersMaterialsAlphabetically() {
|
||||
ItemStack[] contents = {
|
||||
stack(Material.STONE, 5),
|
||||
stack(Material.DIRT, 40),
|
||||
stack(Material.DIRT, 30),
|
||||
null
|
||||
};
|
||||
|
||||
ItemStack[] sorted = new InventorySorter().sort(contents);
|
||||
|
||||
assertAll(
|
||||
() -> assertEquals(Material.DIRT, sorted[0].getType()),
|
||||
() -> assertEquals(64, sorted[0].getAmount()),
|
||||
() -> assertEquals(Material.DIRT, sorted[1].getType()),
|
||||
() -> assertEquals(6, sorted[1].getAmount()),
|
||||
() -> assertEquals(Material.STONE, sorted[2].getType()),
|
||||
() -> assertEquals(5, sorted[2].getAmount()),
|
||||
() -> assertNull(sorted[3])
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void preservesMetadataAndStableOrderForDissimilarVariants() {
|
||||
ItemMeta named = mock(ItemMeta.class, "named");
|
||||
ItemMeta enchanted = mock(ItemMeta.class, "enchanted");
|
||||
ItemStack[] contents = {
|
||||
stack(Material.STONE, 5, named),
|
||||
stack(Material.STONE, 2, enchanted),
|
||||
stack(Material.STONE, 4, named)
|
||||
};
|
||||
|
||||
ItemStack[] sorted = new InventorySorter().sort(contents);
|
||||
|
||||
assertAll(
|
||||
() -> assertSame(named, sorted[0].getItemMeta()),
|
||||
() -> assertEquals(9, sorted[0].getAmount()),
|
||||
() -> assertSame(enchanted, sorted[1].getItemMeta()),
|
||||
() -> assertEquals(2, sorted[1].getAmount()),
|
||||
() -> assertNull(sorted[2])
|
||||
);
|
||||
}
|
||||
|
||||
private static ItemStack stack(Material material, int initialAmount) {
|
||||
return stack(material, initialAmount, null);
|
||||
}
|
||||
|
||||
private static ItemStack stack(
|
||||
Material material,
|
||||
int initialAmount,
|
||||
ItemMeta metadata
|
||||
) {
|
||||
AtomicInteger amount = new AtomicInteger(initialAmount);
|
||||
ItemStack item = mock(ItemStack.class);
|
||||
when(item.getType()).thenReturn(material);
|
||||
when(item.getAmount()).thenAnswer(ignored -> amount.get());
|
||||
when(item.getItemMeta()).thenReturn(metadata);
|
||||
doAnswer(invocation -> {
|
||||
amount.set(invocation.getArgument(0));
|
||||
return null;
|
||||
}).when(item).setAmount(anyInt());
|
||||
when(item.getMaxStackSize()).thenReturn(64);
|
||||
when(item.isSimilar(any(ItemStack.class))).thenAnswer(invocation -> {
|
||||
ItemStack other = invocation.getArgument(0);
|
||||
return other.getType() == material && other.getItemMeta() == metadata;
|
||||
});
|
||||
when(item.clone()).thenAnswer(ignored -> stack(material, amount.get(), metadata));
|
||||
return item;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package games.dmg.spigotinventoryhelper;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.Arrays;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.PlayerInventory;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
final class InventorySortingServiceTest {
|
||||
@Test
|
||||
void sortsOnlyPlayerMainStorageSlots() {
|
||||
PlayerInventory inventory = mock(PlayerInventory.class);
|
||||
InventorySorter sorter = mock(InventorySorter.class);
|
||||
ItemStack[] storage = stacks(36);
|
||||
ItemStack[] mainStorage = Arrays.copyOfRange(storage, 9, 36);
|
||||
ItemStack[] sortedMain = mainStorage.clone();
|
||||
ItemStack first = sortedMain[0];
|
||||
sortedMain[0] = sortedMain[26];
|
||||
sortedMain[26] = first;
|
||||
when(inventory.getStorageContents()).thenReturn(storage);
|
||||
when(sorter.sort(mainStorage)).thenReturn(sortedMain);
|
||||
|
||||
new InventorySortingService(sorter).sortPlayerMainStorage(inventory);
|
||||
|
||||
ArgumentCaptor<ItemStack[]> result = ArgumentCaptor.forClass(ItemStack[].class);
|
||||
verify(inventory).setStorageContents(result.capture());
|
||||
assertArrayEquals(Arrays.copyOfRange(storage, 0, 9),
|
||||
Arrays.copyOfRange(result.getValue(), 0, 9));
|
||||
assertArrayEquals(sortedMain, Arrays.copyOfRange(result.getValue(), 9, 36));
|
||||
}
|
||||
|
||||
private static ItemStack[] stacks(int size) {
|
||||
ItemStack[] stacks = new ItemStack[size];
|
||||
for (int slot = 0; slot < size; slot++) {
|
||||
stacks[slot] = mock(ItemStack.class, "slot-" + slot);
|
||||
}
|
||||
return stacks;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package games.dmg.spigotinventoryhelper;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import org.bukkit.block.Barrel;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.Chest;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.block.Action;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
import org.bukkit.inventory.DoubleChestInventory;
|
||||
import org.bukkit.inventory.EquipmentSlot;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.PlayerInventory;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class SortingStickListenerTest {
|
||||
@Test
|
||||
void sortsAChestWithoutOpeningIt() {
|
||||
SortingStickItemFactory sticks = mock(SortingStickItemFactory.class);
|
||||
InventorySortingService sorting = mock(InventorySortingService.class);
|
||||
PlayerInteractEvent event = mock(PlayerInteractEvent.class);
|
||||
Player player = mock(Player.class);
|
||||
ItemStack stick = mock(ItemStack.class);
|
||||
Block block = mock(Block.class);
|
||||
Chest chest = mock(Chest.class);
|
||||
Inventory inventory = mock(Inventory.class);
|
||||
when(event.getHand()).thenReturn(EquipmentSlot.HAND);
|
||||
when(event.getAction()).thenReturn(Action.RIGHT_CLICK_BLOCK);
|
||||
when(event.getItem()).thenReturn(stick);
|
||||
when(event.getPlayer()).thenReturn(player);
|
||||
when(event.getClickedBlock()).thenReturn(block);
|
||||
when(block.getState()).thenReturn(chest);
|
||||
when(chest.getInventory()).thenReturn(inventory);
|
||||
when(sticks.isSortingStick(stick)).thenReturn(true);
|
||||
|
||||
new SortingStickListener(sticks, sorting).onInteract(event);
|
||||
|
||||
verify(sorting).sortContainer(inventory);
|
||||
verify(event).setCancelled(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void sortsABarrelWithoutOpeningIt() {
|
||||
SortingStickItemFactory sticks = mock(SortingStickItemFactory.class);
|
||||
InventorySortingService sorting = mock(InventorySortingService.class);
|
||||
PlayerInteractEvent event = mock(PlayerInteractEvent.class);
|
||||
ItemStack stick = mock(ItemStack.class);
|
||||
Block block = mock(Block.class);
|
||||
Barrel barrel = mock(Barrel.class);
|
||||
Inventory inventory = mock(Inventory.class);
|
||||
when(event.getHand()).thenReturn(EquipmentSlot.HAND);
|
||||
when(event.getAction()).thenReturn(Action.RIGHT_CLICK_BLOCK);
|
||||
when(event.getItem()).thenReturn(stick);
|
||||
when(event.getPlayer()).thenReturn(mock(Player.class));
|
||||
when(event.getClickedBlock()).thenReturn(block);
|
||||
when(block.getState()).thenReturn(barrel);
|
||||
when(barrel.getInventory()).thenReturn(inventory);
|
||||
when(sticks.isSortingStick(stick)).thenReturn(true);
|
||||
|
||||
new SortingStickListener(sticks, sorting).onInteract(event);
|
||||
|
||||
verify(sorting).sortContainer(inventory);
|
||||
verify(event).setCancelled(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void refusesToSortALockedContainer() {
|
||||
SortingStickItemFactory sticks = mock(SortingStickItemFactory.class);
|
||||
InventorySortingService sorting = mock(InventorySortingService.class);
|
||||
PlayerInteractEvent event = mock(PlayerInteractEvent.class);
|
||||
ItemStack stick = mock(ItemStack.class);
|
||||
Block block = mock(Block.class);
|
||||
Chest chest = mock(Chest.class);
|
||||
when(event.getHand()).thenReturn(EquipmentSlot.HAND);
|
||||
when(event.getAction()).thenReturn(Action.RIGHT_CLICK_BLOCK);
|
||||
when(event.getItem()).thenReturn(stick);
|
||||
when(event.getPlayer()).thenReturn(mock(Player.class));
|
||||
when(event.getClickedBlock()).thenReturn(block);
|
||||
when(block.getState()).thenReturn(chest);
|
||||
when(chest.isLocked()).thenReturn(true);
|
||||
when(sticks.isSortingStick(stick)).thenReturn(true);
|
||||
|
||||
new SortingStickListener(sticks, sorting).onInteract(event);
|
||||
|
||||
verifyNoInteractions(sorting);
|
||||
verify(event).setCancelled(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void sortsTheCombinedInventoryReturnedByEitherHalfOfADoubleChest() {
|
||||
SortingStickItemFactory sticks = mock(SortingStickItemFactory.class);
|
||||
InventorySortingService sorting = mock(InventorySortingService.class);
|
||||
PlayerInteractEvent event = authenticBlockInteraction(sticks);
|
||||
Block block = event.getClickedBlock();
|
||||
Chest chest = mock(Chest.class);
|
||||
DoubleChestInventory combined = mock(DoubleChestInventory.class);
|
||||
when(block.getState()).thenReturn(chest);
|
||||
when(chest.getInventory()).thenReturn(combined);
|
||||
|
||||
new SortingStickListener(sticks, sorting).onInteract(event);
|
||||
|
||||
verify(sorting).sortContainer(combined);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rightClickingAirSortsOnlyThePlayerMainStorage() {
|
||||
SortingStickItemFactory sticks = mock(SortingStickItemFactory.class);
|
||||
InventorySortingService sorting = mock(InventorySortingService.class);
|
||||
PlayerInteractEvent event = mock(PlayerInteractEvent.class);
|
||||
Player player = mock(Player.class);
|
||||
PlayerInventory inventory = mock(PlayerInventory.class);
|
||||
ItemStack stick = mock(ItemStack.class);
|
||||
when(event.getHand()).thenReturn(EquipmentSlot.HAND);
|
||||
when(event.getAction()).thenReturn(Action.RIGHT_CLICK_AIR);
|
||||
when(event.getItem()).thenReturn(stick);
|
||||
when(event.getPlayer()).thenReturn(player);
|
||||
when(player.getInventory()).thenReturn(inventory);
|
||||
when(sticks.isSortingStick(stick)).thenReturn(true);
|
||||
|
||||
new SortingStickListener(sticks, sorting).onInteract(event);
|
||||
|
||||
verify(sorting).sortPlayerMainStorage(inventory);
|
||||
verify(event).setCancelled(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void respectsAnInteractionCancelledByAnotherPlugin() {
|
||||
SortingStickItemFactory sticks = mock(SortingStickItemFactory.class);
|
||||
InventorySortingService sorting = mock(InventorySortingService.class);
|
||||
PlayerInteractEvent event = mock(PlayerInteractEvent.class);
|
||||
when(event.isCancelled()).thenReturn(true);
|
||||
|
||||
new SortingStickListener(sticks, sorting).onInteract(event);
|
||||
|
||||
verifyNoInteractions(sticks, sorting);
|
||||
verify(event, never()).setCancelled(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ignoresAnOrdinaryStick() {
|
||||
SortingStickItemFactory sticks = mock(SortingStickItemFactory.class);
|
||||
InventorySortingService sorting = mock(InventorySortingService.class);
|
||||
PlayerInteractEvent event = mock(PlayerInteractEvent.class);
|
||||
when(event.getHand()).thenReturn(EquipmentSlot.HAND);
|
||||
when(event.getAction()).thenReturn(Action.RIGHT_CLICK_AIR);
|
||||
when(event.getItem()).thenReturn(mock(ItemStack.class));
|
||||
|
||||
new SortingStickListener(sticks, sorting).onInteract(event);
|
||||
|
||||
verifyNoInteractions(sorting);
|
||||
verify(event, never()).setCancelled(true);
|
||||
}
|
||||
|
||||
private static PlayerInteractEvent authenticBlockInteraction(
|
||||
SortingStickItemFactory sticks
|
||||
) {
|
||||
PlayerInteractEvent event = mock(PlayerInteractEvent.class);
|
||||
ItemStack stick = mock(ItemStack.class);
|
||||
when(event.getHand()).thenReturn(EquipmentSlot.HAND);
|
||||
when(event.getAction()).thenReturn(Action.RIGHT_CLICK_BLOCK);
|
||||
when(event.getItem()).thenReturn(stick);
|
||||
when(event.getPlayer()).thenReturn(mock(Player.class));
|
||||
when(event.getClickedBlock()).thenReturn(mock(Block.class));
|
||||
when(sticks.isSortingStick(stick)).thenReturn(true);
|
||||
return event;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user