diff --git a/dough-api/pom.xml b/dough-api/pom.xml index bb6887e5..61f6007d 100644 --- a/dough-api/pom.xml +++ b/dough-api/pom.xml @@ -7,7 +7,7 @@ io.github.baked-libs dough - 1.1.3 + 1.2.0 dough-api diff --git a/dough-chat/pom.xml b/dough-chat/pom.xml index 717b3251..33ae3ca0 100644 --- a/dough-chat/pom.xml +++ b/dough-chat/pom.xml @@ -7,7 +7,7 @@ io.github.baked-libs dough - 1.1.3 + 1.2.0 dough-chat diff --git a/dough-common/pom.xml b/dough-common/pom.xml index 595ea8cd..a07a1db3 100644 --- a/dough-common/pom.xml +++ b/dough-common/pom.xml @@ -7,7 +7,7 @@ io.github.baked-libs dough - 1.1.3 + 1.2.0 dough-common diff --git a/dough-config/pom.xml b/dough-config/pom.xml index 77de1152..e002dfd1 100644 --- a/dough-config/pom.xml +++ b/dough-config/pom.xml @@ -7,7 +7,7 @@ io.github.baked-libs dough - 1.1.3 + 1.2.0 dough-config diff --git a/dough-data/pom.xml b/dough-data/pom.xml index 6f76c52e..8b564c14 100644 --- a/dough-data/pom.xml +++ b/dough-data/pom.xml @@ -7,7 +7,7 @@ io.github.baked-libs dough - 1.1.3 + 1.2.0 dough-data diff --git a/dough-inventories/pom.xml b/dough-inventories/pom.xml index e5bd781e..37db3fe4 100644 --- a/dough-inventories/pom.xml +++ b/dough-inventories/pom.xml @@ -7,7 +7,7 @@ io.github.baked-libs dough - 1.1.3 + 1.2.0 dough-inventories diff --git a/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/InvUtils.java b/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/InvUtils.java index f055742d..1099c8c6 100644 --- a/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/InvUtils.java +++ b/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/InvUtils.java @@ -202,6 +202,7 @@ public static boolean fitAll(@Nonnull Inventory inv, @Nonnull ItemStack[] items, * Whether to replace consumables, e.g. turn potions into glass bottles etc... * @param predicate * The Predicate that tests the item + * * @return Whether the operation was successful */ public static boolean removeItem(@Nonnull Inventory inv, int amount, boolean replaceConsumables, @Nonnull Predicate predicate) { diff --git a/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/Menu.java b/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/Menu.java new file mode 100644 index 00000000..ef1ac001 --- /dev/null +++ b/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/Menu.java @@ -0,0 +1,86 @@ +package io.github.bakedlibs.dough.inventory; + +import java.util.Iterator; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.annotation.ParametersAreNonnullByDefault; + +import org.apache.commons.lang.Validate; +import org.bukkit.entity.HumanEntity; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryHolder; +import org.bukkit.inventory.InventoryView; +import org.bukkit.inventory.ItemStack; + +import io.github.bakedlibs.dough.inventory.factory.MenuFactory; + +public interface Menu extends InventoryHolder { + + /** + * This method returns the {@link MenuFactory} which was used + * to create this {@link Menu}. + * + * @return The original {@link MenuFactory} + */ + @Nonnull + MenuFactory getFactory(); + + /** + * This returns the {@link MenuLayout} which was used to create + * this {@link Menu}. + * + * @return The {@link MenuLayout} + */ + @Nonnull + MenuLayout getLayout(); + + /** + * This returns the title of this {@link Menu}. + * If no title was set, null will be returned. + * + * @return The title of this {@link Menu} or null + */ + @Nullable + String getTitle(); + + void setAll(@Nonnull SlotGroup group, @Nullable ItemStack item); + + default void clear(@Nonnull SlotGroup group) { + setAll(group, null); + } + + @ParametersAreNonnullByDefault + @Nullable + ItemStack addItem(SlotGroup group, ItemStack item); + + void setItem(int slot, @Nullable ItemStack item); + + @Nullable + ItemStack getItem(int slot); + + default @Nonnull InventoryView open(@Nonnull Player player) { + Validate.notNull(player, "The Player must not be null"); + return player.openInventory(getInventory()); + } + + default void closeAllViews() { + Inventory inv = getInventory(); + Iterator iterator = inv.getViewers().iterator(); + + while (iterator.hasNext()) { + iterator.next().closeInventory(); + } + } + + /** + * This returns the size of this {@link Menu}. + * + * @return The size of the {@link Menu}. + */ + default int getSize() { + return getInventory().getSize(); + } + +} diff --git a/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/MenuLayout.java b/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/MenuLayout.java new file mode 100644 index 00000000..7082f379 --- /dev/null +++ b/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/MenuLayout.java @@ -0,0 +1,92 @@ +package io.github.bakedlibs.dough.inventory; + +import java.util.Set; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import org.bukkit.inventory.Inventory; + +import io.github.bakedlibs.dough.inventory.builders.MenuLayoutBuilder; + +/** + * The {@link MenuLayout} covers the different {@link SlotGroup}s and basic characteristics + * of a {@link Menu}. + * + * @author TheBusyBiscuit + * + * @see Menu + * @see SlotGroup + * @see MenuLayoutBuilder + * + */ +public interface MenuLayout { + + /** + * This returns all defined {@link SlotGroup}s for this + * {@link MenuLayout}. + * + * @return A {@link Set} containing every {@link SlotGroup} + */ + @Nonnull + Set getSlotGroups(); + + /** + * This returns the size of the resulting {@link Inventory}. + * + * @return The {@link Inventory} size + */ + int getSize(); + + /** + * This returns the title to be set for the resulting {@link Menu}. + * If no title was set, this will return null. + * + * @return The title or null + */ + @Nullable + String getTitle(); + + /** + * This returns the {@link SlotGroup} with the given identifier. + *

+ * If no corresponding {@link SlotGroup} was found, it will throw an + * {@link IllegalArgumentException}. + * + * @param identifier + * The unique identifier for this {@link SlotGroup}. + * + * @return The corresponding {@link SlotGroup} + */ + @Nonnull + SlotGroup getGroup(char identifier); + + /** + * This returns the {@link SlotGroup} present at the given slot. + *

+ * If no corresponding {@link SlotGroup} was found, it will throw an + * {@link IllegalArgumentException}. + * + * @param slot + * The slot + * + * @return The corresponding {@link SlotGroup} + */ + @Nonnull + SlotGroup getGroup(int slot); + + /** + * This returns the {@link SlotGroup} with the given name. + *

+ * If no corresponding {@link SlotGroup} was found, it will throw an + * {@link IllegalArgumentException}. + * + * @param name + * The unique name of this {@link SlotGroup}. + * + * @return The corresponding {@link SlotGroup} + */ + @Nonnull + SlotGroup getGroup(@Nonnull String name); + +} diff --git a/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/SlotGroup.java b/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/SlotGroup.java new file mode 100644 index 00000000..a87ab5dc --- /dev/null +++ b/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/SlotGroup.java @@ -0,0 +1,102 @@ +package io.github.bakedlibs.dough.inventory; + +import java.util.Iterator; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; + +import io.github.bakedlibs.dough.inventory.builders.SlotGroupBuilder; +import io.github.bakedlibs.dough.inventory.handlers.MenuClickHandler; + +/** + * A {@link SlotGroup} groups slots together and divides an {@link Inventory} + * into distinct regions which can be used for easy access. + * + * @author TheBusyBiscuit + * + * @see MenuLayout + * @see SlotGroupBuilder + * + */ +public interface SlotGroup extends Iterable { + + /** + * This returns the unique identifier for this {@link SlotGroup}, + * we use a {@link Character} for this. + * + * @return The unique identifier of this {@link SlotGroup} + */ + @Nonnull + char getIdentifier(); + + /** + * This method returns whether this {@link SlotGroup} is interactable. + * An interactable {@link SlotGroup} allows the {@link Player} to take + * and store items from/in slots of this group. + *

+ * If {@link #isInteractable()} returns false, the click event will + * be cancelled. + * + * @return Whether this {@link SlotGroup} is interactable + */ + boolean isInteractable(); + + /** + * This returns the name or label of this {@link SlotGroup}, names help + * to make a {@link SlotGroup} easier to identify instead of relying on the + * {@link Character} identifier all the time. + * + * @return The name of this {@link SlotGroup} + */ + @Nonnull + String getName(); + + /** + * This method returns an array containing all the individual slots of + * this {@link SlotGroup}. + * + * @return An array with all slots of this {@link SlotGroup} + */ + @Nonnull + int[] getSlots(); + + /** + * This returns the size of this {@link SlotGroup}. + * + * @return The size of this {@link SlotGroup} + */ + default int size() { + return getSlots().length; + } + + /** + * This returns the default {@link ItemStack} for this {@link SlotGroup}. + * + * @return The default {@link ItemStack}, can be null + */ + @Nullable + ItemStack getDefaultItemStack(); + + /** + * This method returns the {@link MenuClickHandler} belonging to this {@link SlotGroup}. + * If no {@link MenuClickHandler} was added, this will return null. + * + * @return The {@link MenuClickHandler} or null + */ + @Nullable + MenuClickHandler getClickHandler(); + + /** + * This returns an {@link Iterator} allowing you to iterate through + * all slots within this {@link SlotGroup}. + */ + @Override + default @Nonnull Iterator iterator() { + return new SlotGroupIterator(this); + } + +} diff --git a/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/SlotGroupIterator.java b/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/SlotGroupIterator.java new file mode 100644 index 00000000..34f71574 --- /dev/null +++ b/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/SlotGroupIterator.java @@ -0,0 +1,51 @@ +package io.github.bakedlibs.dough.inventory; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +import javax.annotation.Nonnull; + +/** + * This {@link Iterator} implementation iterates through all slots within a {@link SlotGroup}. + * + * @author TheBusyBiscuit + * + */ +class SlotGroupIterator implements Iterator { + + private final int[] slots; + private int index = 0; + + /** + * This creates a new {@link SlotGroupIterator} for the given + * {@link SlotGroup}. + * + * @param slotGroup + * The {@link SlotGroup} + */ + SlotGroupIterator(@Nonnull SlotGroup slotGroup) { + this.slots = slotGroup.getSlots(); + } + + @Override + public boolean hasNext() { + return index < slots.length; + } + + @Override + public Integer next() { + if (index < slots.length) { + int slot = slots[index]; + index++; + return slot; + } else { + throw new NoSuchElementException("No more slots available."); + } + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Iterator#remove() is not supported!"); + } + +} diff --git a/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/builders/MenuLayoutBuilder.java b/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/builders/MenuLayoutBuilder.java new file mode 100644 index 00000000..e0d3e79a --- /dev/null +++ b/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/builders/MenuLayoutBuilder.java @@ -0,0 +1,81 @@ +package io.github.bakedlibs.dough.inventory.builders; + +import java.util.HashSet; +import java.util.Set; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.annotation.ParametersAreNonnullByDefault; + +import org.apache.commons.lang.Validate; + +import io.github.bakedlibs.dough.inventory.Menu; +import io.github.bakedlibs.dough.inventory.MenuLayout; +import io.github.bakedlibs.dough.inventory.SlotGroup; + +/** + * The {@link MenuLayoutBuilder} allows you to construct a {@link MenuLayout} + * easily via the builder pattern. + * + * @author TheBusyBiscuit + * + */ +public class MenuLayoutBuilder { + + protected final int size; + protected final Set groups = new HashSet<>(); + protected String title; + + /** + * This creates a new {@link MenuLayoutBuilder} with the given inventory size. + * + * @param size + * The inventory size for this {@link MenuLayout} + */ + public MenuLayoutBuilder(int size) { + Validate.isTrue(size > 0, "The size must be greater than 0."); + Validate.isTrue(size % 9 == 0, "The size must be a multiple of 9."); + Validate.isTrue(size <= 54, "The size must not be greater than 54."); + + this.size = size; + } + + /** + * This sets an optional title for the resulting {@link Menu}. + * + * @param title + * The title or null + * + * @return Our {@link MenuLayoutBuilder} instance + */ + public @Nonnull MenuLayoutBuilder title(@Nullable String title) { + this.title = title; + return this; + } + + /** + * This adds the given {@link SlotGroup} to this {@link MenuLayout}. + * + * @param group + * The {@link SlotGroup} to add + * + * @return Our {@link MenuLayoutBuilder} instance + */ + @ParametersAreNonnullByDefault + public @Nonnull MenuLayoutBuilder addSlotGroup(SlotGroup group) { + groups.add(group); + return this; + } + + /** + * This creates the final {@link MenuLayout} object from this {@link MenuLayoutBuilder}. + * + * @return The resulting {@link MenuLayout} + */ + public @Nonnull MenuLayout build() { + Validate.notEmpty(groups, "There are no SlotGroups defined."); + + return new MenuLayoutImpl(this); + } + +} diff --git a/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/builders/MenuLayoutImpl.java b/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/builders/MenuLayoutImpl.java new file mode 100644 index 00000000..a6bec979 --- /dev/null +++ b/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/builders/MenuLayoutImpl.java @@ -0,0 +1,143 @@ +package io.github.bakedlibs.dough.inventory.builders; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Predicate; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.annotation.ParametersAreNonnullByDefault; + +import org.apache.commons.lang.Validate; + +import io.github.bakedlibs.dough.inventory.MenuLayout; +import io.github.bakedlibs.dough.inventory.SlotGroup; + +class MenuLayoutImpl implements MenuLayout { + + private final int size; + private final String title; + + private final Set groups = new HashSet<>(); + private final SlotGroup[] groupsBySlot; + + MenuLayoutImpl(@Nonnull MenuLayoutBuilder builder) { + this.size = builder.size; + this.title = builder.title; + this.groups.addAll(builder.groups); + this.groupsBySlot = new SlotGroup[size]; + + Set uniqueCharacters = new HashSet<>(); + Set uniqueNames = new HashSet<>(); + Set coveredSlots = new HashSet<>(); + + for (SlotGroup group : groups) { + Validate.notNull(group, "SlotGroups cannot be null."); + + // Check for duplicate identifiers + if (!uniqueCharacters.add(group.getIdentifier())) { + throw new IllegalStateException("Identifier '" + group.getIdentifier() + "' is used more than once!"); + } + + // Check for duplicate names + if (!uniqueNames.add(group.getName())) { + throw new IllegalStateException("Name '" + group.getName() + "' is used more than once!"); + } + + for (int slot : group) { + Validate.isTrue(slot >= 0 && slot < size, "The slot " + slot + " is outside the bounds of this inventory (0 - " + size + ')'); + + if (!coveredSlots.add(slot)) { + throw new IllegalStateException("Slot " + slot + " is defined by multiple slot groups."); + } + + groupsBySlot[slot] = group; + } + } + + if (coveredSlots.size() != size) { + throw new IllegalStateException("Only " + coveredSlots.size() + " / " + size + " slots are covered by slot groups."); + } + } + + /** + * {@inheritDoc} + */ + @Override + public @Nonnull Set getSlotGroups() { + return Collections.unmodifiableSet(groups); + } + + /** + * {@inheritDoc} + */ + @Override + public int getSize() { + return size; + } + + /** + * {@inheritDoc} + */ + @Override + public @Nullable String getTitle() { + return title; + } + + /** + * {@inheritDoc} + */ + @Override + public @Nonnull SlotGroup getGroup(char identifier) { + SlotGroup result = findGroup(group -> group.getIdentifier() == identifier); + + if (result != null) { + return result; + } else { + throw new IllegalArgumentException("Could not find a SlotGroup with the identifier '" + identifier + "'"); + } + } + + /** + * {@inheritDoc} + */ + @Override + public @Nonnull SlotGroup getGroup(int slot) { + Validate.isTrue(slot >= 0, "Slot cannot be a negative number: " + slot); + Validate.isTrue(slot < size, "Slot " + slot + " is not within the inventory size of " + size); + + /* + * Using an Array makes this much faster. + * And since this method will be used for the click events, some + * optimization here will be good to have. + */ + return groupsBySlot[slot]; + } + + /** + * {@inheritDoc} + */ + @Override + public @Nonnull SlotGroup getGroup(@Nonnull String name) { + SlotGroup result = findGroup(group -> group.getName().equals(name)); + + if (result != null) { + return result; + } else { + throw new IllegalArgumentException("Could not find a SlotGroup with the name '" + name + "'"); + } + } + + @ParametersAreNonnullByDefault + private @Nullable SlotGroup findGroup(Predicate predicate) { + for (SlotGroup group : groups) { + if (predicate.test(group)) { + return group; + } + } + + return null; + } + +} diff --git a/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/builders/SlotGroupBuilder.java b/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/builders/SlotGroupBuilder.java new file mode 100644 index 00000000..aaac098f --- /dev/null +++ b/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/builders/SlotGroupBuilder.java @@ -0,0 +1,164 @@ +package io.github.bakedlibs.dough.inventory.builders; + +import java.util.HashSet; +import java.util.Set; +import java.util.stream.IntStream; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.annotation.ParametersAreNonnullByDefault; + +import org.apache.commons.lang.Validate; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.ItemStack; + +import io.github.bakedlibs.dough.inventory.SlotGroup; +import io.github.bakedlibs.dough.inventory.handlers.MenuClickHandler; + +/** + * The {@link SlotGroupBuilder} allows you to construct a {@link SlotGroup} + * easily via the builder pattern. + * + * @author TheBusyBiscuit + * + */ +public class SlotGroupBuilder { + + protected final char identifier; + protected final String name; + protected final Set slots = new HashSet<>(); + + protected MenuClickHandler clickHandler = null; + protected ItemStack defaultItem = null; + protected boolean interactable = false; + + /** + * This creates a new {@link SlotGroupBuilder} of the given name and char id. + * You can construct the corresponding {@link SlotGroup} using {@link #build()}. + * + * @param identifier + * The unique {@link Character} id for this {@link SlotGroup} + * @param name + * A unique name for this {@link SlotGroup} + */ + @ParametersAreNonnullByDefault + public SlotGroupBuilder(char identifier, String name) { + this.identifier = identifier; + this.name = name; + } + + /** + * This simply returns the name of this {@link SlotGroup}. + * + * @return The name of this {@link SlotGroup} + */ + public @Nonnull String name() { + return name; + } + + /** + * This marks this {@link SlotGroup} as "interactable" or "non-interactable". + * Interactable {@link SlotGroup}s allow the {@link Player} to take items from this + * {@link SlotGroup} or place items into those slots. + *

+ * Non-interactable {@link SlotGroup}s will cancel any {@link InventoryClickEvent} within + * their bounds. + * + * @param interactable + * Whether this {@link SlotGroup} is interactable + * + * @return The {@link SlotGroupBuilder} instance + */ + public @Nonnull SlotGroupBuilder interactable(boolean interactable) { + this.interactable = interactable; + return this; + } + + /** + * This method adds the given slot to this {@link SlotGroup} + * + * @param slot + * The slot to be added + * + * @return The {@link SlotGroupBuilder} instance + */ + public @Nonnull SlotGroupBuilder withSlot(int slot) { + this.slots.add(slot); + return this; + } + + /** + * This method adds all the provided slots to this {@link SlotGroup} + * + * @param slots + * All slots that should be part of this {@link SlotGroup} + * + * @return The {@link SlotGroupBuilder} instance + */ + public @Nonnull SlotGroupBuilder withSlots(int... slots) { + for (int slot : slots) { + this.slots.add(slot); + } + + return this; + } + + /** + * This method adds all slots within the given range to this {@link SlotGroup}. + * Note that both parameters are inclusive. + * + * @param from + * The start of this slot range (inclusive) + * @param to + * The end of this slot range (inclusive) + * + * @return The {@link SlotGroupBuilder} instance + */ + public @Nonnull SlotGroupBuilder withSlotRange(int from, int to) { + IntStream.range(from, to + 1).forEach(slots::add); + return this; + } + + /** + * This sets the {@link ItemStack} for this {@link SlotGroup}. + * This {@link ItemStack} will be placed into all slots from this {@link SlotGroup} + * by default. It can be overridden though. + * + * @param item + * The default {@link ItemStack} for this {@link SlotGroup} + * + * @return The {@link SlotGroupBuilder} instance + */ + public @Nonnull SlotGroupBuilder withDefaultItem(@Nullable ItemStack item) { + this.defaultItem = item; + return this; + } + + /** + * This adds a {@link MenuClickHandler} to this {@link SlotGroup}. + * + * @param clickHandler + * The {@link MenuClickHandler} to fire when a slot from this {@link SlotGroup} was clicked + * + * @return The {@link SlotGroupBuilder} instance + */ + public @Nonnull SlotGroupBuilder onClick(@Nullable MenuClickHandler clickHandler) { + this.clickHandler = clickHandler; + return this; + } + + /** + * This creates the final {@link SlotGroup} object from this {@link SlotGroupBuilder}. + * + * @return The resulting {@link SlotGroup} + */ + public @Nonnull SlotGroup build() { + Validate.notNull(identifier, "The char identifier may not be null."); + Validate.notNull(name, "The name may not be null."); + Validate.notEmpty(slots, "A SlotGroup must have at least one slot."); + + return new SlotGroupImpl(this); + } + +} diff --git a/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/builders/SlotGroupImpl.java b/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/builders/SlotGroupImpl.java new file mode 100644 index 00000000..d466a84b --- /dev/null +++ b/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/builders/SlotGroupImpl.java @@ -0,0 +1,96 @@ +package io.github.bakedlibs.dough.inventory.builders; + +import java.util.Set; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import org.bukkit.inventory.ItemStack; + +import io.github.bakedlibs.dough.inventory.SlotGroup; +import io.github.bakedlibs.dough.inventory.handlers.MenuClickHandler; + +class SlotGroupImpl implements SlotGroup { + + private final char identifier; + private final String name; + private final boolean interactable; + private final ItemStack defaultItem; + private final MenuClickHandler clickHandler; + private final int[] slots; + + SlotGroupImpl(@Nonnull SlotGroupBuilder builder) { + this.identifier = builder.identifier; + this.name = builder.name; + this.interactable = builder.interactable; + this.defaultItem = builder.defaultItem; + this.slots = convertSlots(builder.slots); + this.clickHandler = builder.clickHandler; + } + + /** + * This method converts our {@link Set} of slots into a sorted array. + * + * @param slots + * The slots + * + * @return A sorted array of our slots + */ + private @Nonnull int[] convertSlots(@Nonnull Set slots) { + // @formatter:off + return slots.stream() + .mapToInt(Integer::intValue) + .sorted() + .toArray(); + // @formatter:on + } + + /** + * {@inheritDoc} + */ + @Override + public char getIdentifier() { + return identifier; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isInteractable() { + return interactable; + } + + /** + * {@inheritDoc} + */ + @Override + public @Nonnull String getName() { + return name; + } + + /** + * {@inheritDoc} + */ + @Override + public @Nonnull int[] getSlots() { + return slots; + } + + /** + * {@inheritDoc} + */ + @Override + public @Nullable ItemStack getDefaultItemStack() { + return defaultItem; + } + + /** + * {@inheritDoc} + */ + @Override + public @Nonnull MenuClickHandler getClickHandler() { + return clickHandler; + } + +} diff --git a/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/factory/CustomMenu.java b/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/factory/CustomMenu.java new file mode 100644 index 00000000..745cb6df --- /dev/null +++ b/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/factory/CustomMenu.java @@ -0,0 +1,141 @@ +package io.github.bakedlibs.dough.inventory.factory; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.annotation.ParametersAreNonnullByDefault; + +import org.apache.commons.lang.Validate; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; + +import io.github.bakedlibs.dough.inventory.Menu; +import io.github.bakedlibs.dough.inventory.MenuLayout; +import io.github.bakedlibs.dough.inventory.SlotGroup; + +public class CustomMenu implements Menu { + + private final MenuFactory factory; + private final MenuLayout layout; + private final String title; + private Inventory inventory; + + @ParametersAreNonnullByDefault + protected CustomMenu(MenuFactory factory, MenuLayout layout) { + Validate.notNull(factory, "The factory cannot be null."); + Validate.notNull(layout, "The layout cannot be null."); + + this.factory = factory; + this.layout = layout; + this.title = layout.getTitle(); + } + + final void setInventory(@Nonnull Inventory inventory) { + Validate.notNull(inventory, "The Inventory must not be null."); + Validate.isTrue(inventory.getSize() == layout.getSize(), "The inventory has a different size."); + Validate.isTrue(inventory.getHolder() == this, "The Inventory does not seem to belong here. Holder: " + inventory.getHolder()); + + this.inventory = inventory; + } + + private void validate() { + if (inventory == null) { + throw new UnsupportedOperationException("No inventory found! Menus must be created using MenuFactory#createMenu(...)"); + } + } + + @Override + public final @Nonnull MenuFactory getFactory() { + validate(); + + return factory; + } + + @Override + public final @Nonnull Inventory getInventory() { + validate(); + + return inventory; + } + + @Override + public final @Nonnull MenuLayout getLayout() { + validate(); + + return layout; + } + + @Override + public String getTitle() { + validate(); + + return title; + } + + @Override + public void setAll(SlotGroup group, ItemStack item) { + validate(); + + if (group.size() == 1) { + // Little optimization, we don't need to create an Iterator for this + setItem(group.getSlots()[0], item); + } else { + for (int slot : group) { + setItem(slot, item); + } + } + } + + @Override + @ParametersAreNonnullByDefault + public @Nullable ItemStack addItem(SlotGroup group, ItemStack item) { + Validate.notNull(group, "The Slot group cannot be null!"); + Validate.notNull(item, "The Item cannot be null!"); + validate(); + + for (int slot : group) { + ItemStack itemInSlot = getItem(slot); + + if (itemInSlot == null || itemInSlot.getType().isAir()) { + setItem(slot, item); + return null; + } else { + int currentAmount = itemInSlot.getAmount(); + int maxStackSize = itemInSlot.getType().getMaxStackSize(); + + if (currentAmount < maxStackSize && itemInSlot.isSimilar(item)) { + int amount = currentAmount + item.getAmount(); + + if (amount > maxStackSize) { + item.setAmount(amount - maxStackSize); + itemInSlot.setAmount(maxStackSize); + } else { + itemInSlot.setAmount(Math.min(amount, maxStackSize)); + return null; + } + } + } + } + + return item; + } + + @Override + public void setItem(int slot, @Nullable ItemStack item) { + validate(); + + inventory.setItem(slot, item); + } + + @Override + public @Nullable ItemStack getItem(int slot) { + validate(); + + return inventory.getItem(slot); + } + + @Override + public @Nonnull String toString() { + return getClass().getSimpleName() + " [size: " + layout.getSize() + ", title=" + title + "]"; + } + +} diff --git a/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/factory/MenuFactory.java b/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/factory/MenuFactory.java new file mode 100644 index 00000000..cef5f4d9 --- /dev/null +++ b/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/factory/MenuFactory.java @@ -0,0 +1,113 @@ +package io.github.bakedlibs.dough.inventory.factory; + +import java.util.function.BiFunction; +import java.util.logging.Logger; + +import javax.annotation.Nonnull; +import javax.annotation.OverridingMethodsMustInvokeSuper; +import javax.annotation.ParametersAreNonnullByDefault; + +import org.apache.commons.lang.Validate; +import org.bukkit.Bukkit; +import org.bukkit.Server; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.plugin.Plugin; + +import io.github.bakedlibs.dough.inventory.Menu; +import io.github.bakedlibs.dough.inventory.MenuLayout; +import io.github.bakedlibs.dough.inventory.SlotGroup; + +/** + * The {@link MenuFactory} is the core of this system, this is where everything + * starts. You can use this {@link MenuFactory} to create {@link Menu}s from a + * {@link MenuLayout}. + *

+ * This class also handles the registration of our {@link Listener}. + * + * @author TheBusyBiscuit + * + */ +public class MenuFactory { + + /** + * Our {@link Plugin} instance. + */ + private final Plugin plugin; + + /** + * This constructs a new {@link MenuFactory} for the given {@link Plugin}. + * + * @param plugin + * The {@link Plugin} instance + */ + public MenuFactory(@Nonnull Plugin plugin) { + Validate.notNull(plugin, "The plugin instance cannot be null."); + + this.plugin = plugin; + registerListener(); + } + + /** + * This method registers our {@link MenuListener} to the {@link Server}. + * This way, we can listen to and handle {@link InventoryClickEvent}s and alike. + * + * @return Our registered {@link MenuListener} + */ + private @Nonnull MenuListener registerListener() { + MenuListener listener = new MenuListener(this); + plugin.getServer().getPluginManager().registerEvents(listener, plugin); + return listener; + } + + /** + * This returns the {@link Plugin} which instantiated this {@link MenuFactory}. + * + * @return The {@link Plugin} instance + */ + public final @Nonnull Plugin getPlugin() { + return plugin; + } + + /** + * Shortcut method for getting the {@link Logger} from + * {@link #getPlugin()}. + * + * @return The {@link Logger} of our {@link Plugin} + */ + public final @Nonnull Logger getLogger() { + return plugin.getLogger(); + } + + @OverridingMethodsMustInvokeSuper + public @Nonnull Menu createMenu(@Nonnull MenuLayout layout) { + return createMenu(layout, CustomMenu::new); + } + + @ParametersAreNonnullByDefault + public @Nonnull T createMenu(MenuLayout layout, BiFunction constructor) { + Validate.notNull(layout, "The menu layout cannot be null!"); + Validate.notNull(constructor, "The provided constructor is not allowed to be null!"); + + T menu = constructor.apply(this, layout); + String title = layout.getTitle(); + Inventory inv; + + if (title == null) { + inv = Bukkit.createInventory(menu, layout.getSize()); + } else { + inv = Bukkit.createInventory(menu, layout.getSize(), title); + } + + menu.setInventory(inv); + + // Set all default items + for (SlotGroup group : layout.getSlotGroups()) { + menu.setAll(group, group.getDefaultItemStack()); + } + + return menu; + } + +} diff --git a/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/factory/MenuListener.java b/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/factory/MenuListener.java new file mode 100644 index 00000000..5b54f03e --- /dev/null +++ b/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/factory/MenuListener.java @@ -0,0 +1,98 @@ +package io.github.bakedlibs.dough.inventory.factory; + +import java.util.logging.Level; + +import javax.annotation.Nonnull; + +import org.bukkit.entity.Player; +import org.bukkit.event.Event; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryHolder; + +import io.github.bakedlibs.dough.inventory.Menu; +import io.github.bakedlibs.dough.inventory.SlotGroup; +import io.github.bakedlibs.dough.inventory.handlers.MenuClickHandler; +import io.github.bakedlibs.dough.inventory.payloads.MenuPayloads; + +/** + * The {@link MenuListener} is responsible for handling any + * {@link Event} related to our {@link Menu}s. + *

+ * It is registered by a {@link MenuFactory} and there should only + * be one {@link MenuListener} per {@link MenuFactory}. + * + * @author TheBusyBiscuit + * + */ +class MenuListener implements Listener { + + /** + * Our {@link MenuFactory} instance. + */ + private final MenuFactory factory; + + /** + * This constructs a new {@link MenuListener} for the given + * {@link MenuFactory}. + * + * @param factory + * Our {@link MenuFactory} instance + */ + MenuListener(@Nonnull MenuFactory factory) { + this.factory = factory; + } + + /** + * This returns the {@link MenuFactory} which instantiated and + * registered this {@link MenuListener}. + * + * @return The {@link MenuFactory} + */ + public @Nonnull MenuFactory getFactory() { + return factory; + } + + /** + * Here we listen for the {@link InventoryClickEvent}. + * This event is fired whenever a {@link Player} clicks a slot + * in an {@link Inventory}. We only care for events which happen + * within our {@link Menu} though. + * + * @param e + * The {@link InventoryClickEvent} which was fired + */ + @EventHandler + public void onClick(InventoryClickEvent e) { + InventoryHolder holder = e.getInventory().getHolder(); + + // Check if the Inventory is a Menu + if (holder instanceof Menu) { + Menu inv = (Menu) holder; + + try { + // Check if this was created by our factory and the clicked slot is within the upper inventory + if (inv.getFactory().equals(factory) && e.getRawSlot() < e.getInventory().getSize()) { + SlotGroup slotGroup = inv.getLayout().getGroup(e.getSlot()); + + // Cancel the interaction if that slot is not interactable + if (!slotGroup.isInteractable()) { + e.setCancelled(true); + } + + // Fire the click handler + MenuClickHandler clickHandler = slotGroup.getClickHandler(); + + if (clickHandler != null) { + clickHandler.onClick(MenuPayloads.create(inv, e)); + } + } + } catch (Exception | LinkageError x) { + factory.getLogger().log(Level.SEVERE, x, () -> "Could not pass click event for " + inv + " (slot: " + e.getSlot() + ", player:" + e.getWhoClicked().getName() + ")"); + } + } + } + +} diff --git a/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/handlers/MenuClickHandler.java b/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/handlers/MenuClickHandler.java new file mode 100644 index 00000000..2c2e785c --- /dev/null +++ b/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/handlers/MenuClickHandler.java @@ -0,0 +1,12 @@ +package io.github.bakedlibs.dough.inventory.handlers; + +import javax.annotation.Nonnull; + +import io.github.bakedlibs.dough.inventory.payloads.MenuClickPayload; + +@FunctionalInterface +public interface MenuClickHandler { + + void onClick(@Nonnull MenuClickPayload e); + +} diff --git a/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/json/InvalidLayoutException.java b/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/json/InvalidLayoutException.java new file mode 100644 index 00000000..5c52e750 --- /dev/null +++ b/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/json/InvalidLayoutException.java @@ -0,0 +1,28 @@ +package io.github.bakedlibs.dough.inventory.json; + +import javax.annotation.Nonnull; + +import com.google.gson.JsonObject; + +import io.github.bakedlibs.dough.inventory.MenuLayout; + +/** + * An {@link InvalidLayoutException} is thrown when the {@link MenuLayout} + * was not successfully read from a {@link JsonObject}. + * + * @author TheBusyBiscuit + * + */ +class InvalidLayoutException extends Exception { + + private static final long serialVersionUID = -1891678815608214476L; + + InvalidLayoutException(@Nonnull String message) { + super(message); + } + + InvalidLayoutException(@Nonnull Exception exception) { + super(exception); + } + +} diff --git a/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/json/LayoutParser.java b/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/json/LayoutParser.java new file mode 100644 index 00000000..a6d2fbba --- /dev/null +++ b/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/json/LayoutParser.java @@ -0,0 +1,163 @@ +package io.github.bakedlibs.dough.inventory.json; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import javax.annotation.Nonnull; +import javax.annotation.ParametersAreNonnullByDefault; + +import org.apache.commons.lang.Validate; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; + +import io.github.bakedlibs.dough.inventory.MenuLayout; +import io.github.bakedlibs.dough.inventory.SlotGroup; +import io.github.bakedlibs.dough.inventory.builders.MenuLayoutBuilder; +import io.github.bakedlibs.dough.inventory.builders.SlotGroupBuilder; + +/** + * This class allows you to parse {@link JsonObject}s, {@link String}s or {@link InputStream}s + * into a {@link MenuLayout}, given they follow our format. + * + * @author TheBusyBiscuit + * + */ +public class LayoutParser { + + private LayoutParser() {} + + @ParametersAreNonnullByDefault + public static @Nonnull MenuLayout parseStream(InputStream stream, Consumer slotGroups) throws InvalidLayoutException { + Validate.notNull(stream, "InputStream must not be null"); + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) { + return parseString(reader.lines().collect(Collectors.joining("")), slotGroups); + } catch (IOException x) { + throw new InvalidLayoutException(x); + } + } + + @ParametersAreNonnullByDefault + public static @Nonnull MenuLayout parseStream(InputStream stream) throws InvalidLayoutException { + return parseStream(stream, builder -> {}); + } + + @ParametersAreNonnullByDefault + public static @Nonnull MenuLayout parseString(String string, Consumer slotGroups) throws InvalidLayoutException { + Validate.notNull(string, "String must not be null"); + + try { + JsonParser parser = new JsonParser(); + JsonObject root = parser.parse(string).getAsJsonObject(); + return parseJson(root, slotGroups); + } catch (IllegalStateException | JsonParseException x) { + throw new InvalidLayoutException(x); + } + } + + @ParametersAreNonnullByDefault + public static @Nonnull MenuLayout parseString(String string) throws InvalidLayoutException { + return parseString(string, builder -> {}); + } + + @ParametersAreNonnullByDefault + public static @Nonnull MenuLayout parseJson(JsonObject json, Consumer slotGroups) throws InvalidLayoutException { + Validate.notNull(json, "JsonObject must not be null"); + + try { + LayoutShape shape = parseShape(json); + MenuLayoutBuilder builder = new MenuLayoutBuilder(shape.getSize()); + + for (Map.Entry> entry : shape.getGroups().entrySet()) { + builder.addSlotGroup(parseGroup(json, entry.getKey(), entry.getValue(), slotGroups)); + } + + return builder.build(); + } catch (Exception x) { + throw new InvalidLayoutException(x); + } + } + + @ParametersAreNonnullByDefault + public static @Nonnull MenuLayout parseJson(JsonObject json) throws InvalidLayoutException { + return parseJson(json, builder -> {}); + } + + @ParametersAreNonnullByDefault + private static @Nonnull LayoutShape parseShape(JsonObject json) throws InvalidLayoutException { + String attribute = "layout"; + + if (!json.has(attribute) || !json.get(attribute).isJsonArray()) { + throw new InvalidLayoutException("Missing 'layout' child!"); + } + + JsonArray array = json.getAsJsonArray(attribute); + String[] rows = new String[array.size()]; + + if (rows.length == 0) { + throw new InvalidLayoutException("'layout' is empty!"); + } + + int i = 0; + for (JsonElement row : array) { + if (row.isJsonPrimitive() && row.getAsJsonPrimitive().isString()) { + rows[i] = row.getAsString(); + i++; + } else { + throw new InvalidLayoutException("Expected String in layout, found: " + row); + } + } + + return new LayoutShape(rows); + } + + @ParametersAreNonnullByDefault + private static @Nonnull SlotGroup parseGroup(JsonObject json, char identifier, Set slots, Consumer consumer) throws InvalidLayoutException { + String attribute = "groups"; + + if (!json.has(attribute) || !json.get(attribute).isJsonObject()) { + throw new InvalidLayoutException("Missing 'groups' child!"); + } + + JsonObject groups = json.getAsJsonObject(attribute); + String key = String.valueOf(identifier); + + if (!groups.has(key) || !groups.get(key).isJsonObject()) { + throw new InvalidLayoutException("Missing 'groups." + identifier + "' child!"); + } + + JsonObject group = groups.getAsJsonObject(key); + JsonElement name = group.get("name"); + + if (name == null) { + throw new InvalidLayoutException("Slot group '" + identifier + "' has no name!"); + } + + SlotGroupBuilder builder = new SlotGroupBuilder(identifier, name.getAsString()); + + JsonElement interactable = group.get("interactable"); + + if (interactable != null && interactable.isJsonPrimitive() && interactable.getAsJsonPrimitive().isBoolean()) { + builder.interactable(interactable.getAsBoolean()); + } else { + throw new InvalidLayoutException("Slot group '" + identifier + "' has no valid 'interactable' attribute!"); + } + + builder.withSlots(slots.stream().mapToInt(Integer::intValue).toArray()); + consumer.accept(builder); + + return builder.build(); + } + +} diff --git a/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/json/LayoutShape.java b/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/json/LayoutShape.java new file mode 100644 index 00000000..4c73aadb --- /dev/null +++ b/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/json/LayoutShape.java @@ -0,0 +1,80 @@ +package io.github.bakedlibs.dough.inventory.json; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import javax.annotation.Nonnull; +import javax.annotation.ParametersAreNonnullByDefault; + +import org.apache.commons.lang.Validate; +import org.bukkit.inventory.Inventory; + +import io.github.bakedlibs.dough.inventory.SlotGroup; + +/** + * Little helper class to transition a {@link String} array into + * a {@link SlotGroup}-like map. + * + * @author TheBusyBiscuit + * + */ +class LayoutShape { + + private final Map> groups = new HashMap<>(); + private final int size; + + LayoutShape(@Nonnull String[] rows) throws InvalidLayoutException { + Validate.notNull(rows, "Layout cannot be null."); + + if (rows.length > 0 && rows.length < 7) { + this.size = rows.length * 9; + + int i = 0; + + for (String row : rows) { + if (row.length() == 9) { + addSlots(i, row); + i++; + } else { + throw new InvalidLayoutException("Each row in a layout must have 9 characters."); + } + } + } else { + throw new InvalidLayoutException("Layout has " + rows.length + " rows. Must be 1, 2, 3, 4, 5 or 6."); + } + } + + /** + * This returns the size of this {@link LayoutShape}, aka the size of the + * corresponding {@link Inventory}. + * + * @return The size of this {@link LayoutShape} + */ + public int getSize() { + return size; + } + + /** + * This returns the {@link SlotGroup}-ready representation of this shape. + * Grouped by their unique character keys. + * + * @return The "{@link SlotGroup} map". + */ + public @Nonnull Map> getGroups() { + return groups; + } + + @ParametersAreNonnullByDefault + private void addSlots(int i, String row) { + int j = 0; + + for (char identifier : row.toCharArray()) { + Set slots = groups.computeIfAbsent(identifier, id -> new HashSet<>()); + slots.add(i * 9 + j); + j++; + } + } + +} diff --git a/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/payloads/AbstractMenuPayload.java b/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/payloads/AbstractMenuPayload.java new file mode 100644 index 00000000..3154d2d3 --- /dev/null +++ b/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/payloads/AbstractMenuPayload.java @@ -0,0 +1,54 @@ +package io.github.bakedlibs.dough.inventory.payloads; + +import javax.annotation.Nonnull; +import javax.annotation.ParametersAreNonnullByDefault; + +import org.bukkit.entity.Player; + +import io.github.bakedlibs.dough.inventory.Menu; + +/** + * An abstract super class for our menu event payloads. + * + * @author TheBusyBiscuit + * + */ +abstract class AbstractMenuPayload { + + private final Menu menu; + private final Player player; + + /** + * This constructs a new {@link AbstractMenuPayload} for the given {@link Menu} + * and {@link Player}. + * + * @param menu + * The {@link Menu} + * @param player + * The {@link Player} + */ + @ParametersAreNonnullByDefault + AbstractMenuPayload(Menu menu, Player player) { + this.menu = menu; + this.player = player; + } + + /** + * This returns the {@link Menu} from this payload. + * + * @return The {@link Menu} + */ + public @Nonnull Menu getMenu() { + return menu; + } + + /** + * This returns the {@link Player} who triggered this payload. + * + * @return The {@link Player} + */ + public @Nonnull Player getPlayer() { + return player; + } + +} diff --git a/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/payloads/MenuClickPayload.java b/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/payloads/MenuClickPayload.java new file mode 100644 index 00000000..a437eda0 --- /dev/null +++ b/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/payloads/MenuClickPayload.java @@ -0,0 +1,35 @@ +package io.github.bakedlibs.dough.inventory.payloads; + +import javax.annotation.Nonnull; +import javax.annotation.ParametersAreNonnullByDefault; + +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import io.github.bakedlibs.dough.inventory.Menu; +import io.github.bakedlibs.dough.inventory.SlotGroup; + +public class MenuClickPayload extends AbstractMenuPayload { + + private final int slot; + + @ParametersAreNonnullByDefault + MenuClickPayload(Menu inventory, Player player, int slot) { + super(inventory, player); + + this.slot = slot; + } + + public int getClickedSlot() { + return slot; + } + + public @Nonnull ItemStack getClickedItemStack() { + return getMenu().getItem(getClickedSlot()); + } + + public @Nonnull SlotGroup getClickedSlotGroup() { + return getMenu().getLayout().getGroup(getClickedSlot()); + } + +} diff --git a/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/payloads/MenuPayloads.java b/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/payloads/MenuPayloads.java new file mode 100644 index 00000000..abec6ff0 --- /dev/null +++ b/dough-inventories/src/main/java/io/github/bakedlibs/dough/inventory/payloads/MenuPayloads.java @@ -0,0 +1,44 @@ +package io.github.bakedlibs.dough.inventory.payloads; + +import javax.annotation.Nonnull; +import javax.annotation.ParametersAreNonnullByDefault; + +import org.apache.commons.lang.Validate; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; + +import io.github.bakedlibs.dough.inventory.Menu; +import io.github.bakedlibs.dough.inventory.handlers.MenuClickHandler; + +/** + * Utility class for constructing an event payload for menu handlers. + * + * @author TheBusyBiscuit + * + */ +public class MenuPayloads { + + private MenuPayloads() {} + + /** + * This creates our payload for an {@link InventoryClickEvent}. + * + * @param menu + * The {@link Menu} involved in this event. + * @param e + * The {@link InventoryClickEvent} + * + * @return A {@link MenuClickPayload} to pass onto the {@link MenuClickHandler}. + */ + @ParametersAreNonnullByDefault + public static @Nonnull MenuClickPayload create(Menu menu, InventoryClickEvent e) { + Validate.notNull(menu, "The menu cannot be null"); + Validate.notNull(e, "Cannot create a payload for an event that is null"); + + Player player = (Player) e.getWhoClicked(); + int slot = e.getSlot(); + + return new MenuClickPayload(menu, player, slot); + } + +} diff --git a/dough-inventories/src/test/java/io/github/bakedlibs/dough/inventory/TestSlotGroupIterator.java b/dough-inventories/src/test/java/io/github/bakedlibs/dough/inventory/TestSlotGroupIterator.java new file mode 100644 index 00000000..25818c33 --- /dev/null +++ b/dough-inventories/src/test/java/io/github/bakedlibs/dough/inventory/TestSlotGroupIterator.java @@ -0,0 +1,41 @@ +package io.github.bakedlibs.dough.inventory; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +import org.junit.jupiter.api.Test; + +import io.github.bakedlibs.dough.inventory.builders.SlotGroupBuilder; + +class TestSlotGroupIterator { + + @Test + void testIteratorEnd() { + SlotGroup group = new SlotGroupBuilder('x', "test").withSlots(0, 1, 2).build(); + + assertNotNull(group); + + Iterator iterator = group.iterator(); + + while (iterator.hasNext()) { + iterator.next(); + } + + assertThrows(NoSuchElementException.class, iterator::next); + } + + @Test + void testIteratorRemove() { + SlotGroup group = new SlotGroupBuilder('x', "test").withSlots(0, 1, 2).build(); + + assertNotNull(group); + + Iterator iterator = group.iterator(); + + assertThrows(UnsupportedOperationException.class, iterator::remove); + } + +} diff --git a/dough-inventories/src/test/java/io/github/bakedlibs/dough/inventory/builders/TestLayoutBuilder.java b/dough-inventories/src/test/java/io/github/bakedlibs/dough/inventory/builders/TestLayoutBuilder.java new file mode 100644 index 00000000..e60e22e2 --- /dev/null +++ b/dough-inventories/src/test/java/io/github/bakedlibs/dough/inventory/builders/TestLayoutBuilder.java @@ -0,0 +1,217 @@ +package io.github.bakedlibs.dough.inventory.builders; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import javax.annotation.Nonnull; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +import io.github.bakedlibs.dough.inventory.MenuLayout; +import io.github.bakedlibs.dough.inventory.SlotGroup; + +import be.seeseemelk.mockbukkit.MockBukkit; + +class TestLayoutBuilder { + + @BeforeAll + static void setup() { + MockBukkit.mock(); + } + + @AfterAll + static void teardown() { + MockBukkit.unmock(); + } + + @Test + void testSlotGroups() { + // @formatter:off + MenuLayout layout = new MenuLayoutBuilder(9) + .addSlotGroup( + new SlotGroupBuilder('x', "test") + .interactable(false) + .withSlot(1) + .withSlot(2) + .withSlot(3) + .build() + ) + .addSlotGroup( + new SlotGroupBuilder('y', "test2") + .interactable(true) + .withSlots(0, 4, 5, 6, 7, 8) + .withDefaultItem(new ItemStack(Material.DIAMOND)) + .build() + ) + .build(); + // @formatter:on + + assertNotNull(layout); + assertEquals(9, layout.getSize()); + + SlotGroup group = layout.getGroup('x'); + + assertNotNull(group); + assertEquals(3, group.size()); + assertSame(group, layout.getGroup("test")); + assertSame(group, layout.getGroup(1)); + assertSame(group, layout.getGroup(2)); + assertSame(group, layout.getGroup(3)); + assertFalse(group.isInteractable()); + + SlotGroup group2 = layout.getGroup("test2"); + + assertNotNull(group2); + assertSame(group2, layout.getGroup('y')); + assertNotEquals(group, group2); + assertEquals(6, group2.size()); + assertSame(group2, layout.getGroup(0)); + assertEquals(new ItemStack(Material.DIAMOND), group2.getDefaultItemStack()); + + Set groups = layout.getSlotGroups(); + assertEquals(2, groups.size()); + assertTrue(groups.contains(group)); + assertTrue(groups.contains(group2)); + } + + @Test + void testSlotGroupOverlapping() { + // @formatter:off + MenuLayoutBuilder builder = new MenuLayoutBuilder(9) + .addSlotGroup( + new SlotGroupBuilder('x', "test") + .withSlot(1) + .build() + ) + .addSlotGroup( + new SlotGroupBuilder('y', "test2") + .withSlot(1) + .build() + ); + // @formatter:on + + assertThrows(IllegalStateException.class, builder::build); + } + + @Test + void testSlotGroupIdentifierCollision() { + // @formatter:off + MenuLayoutBuilder builder = new MenuLayoutBuilder(9) + .addSlotGroup( + new SlotGroupBuilder('x', "test") + .withSlotRange(0, 3) + .build() + ) + .addSlotGroup( + new SlotGroupBuilder('x', "test2") + .withSlotRange(4, 8) + .build() + ); + // @formatter:on + + assertThrows(IllegalStateException.class, builder::build); + } + + @Test + void testSlotGroupNameCollision() { + // @formatter:off + MenuLayoutBuilder builder = new MenuLayoutBuilder(9) + .addSlotGroup( + new SlotGroupBuilder('x', "test") + .withSlotRange(0, 3) + .build() + ) + .addSlotGroup( + new SlotGroupBuilder('y', "test") + .withSlotRange(4, 8) + .build() + ); + // @formatter:on + + assertThrows(IllegalStateException.class, builder::build); + } + + @ParameterizedTest + @ValueSource(ints = { -1, 10 }) + void testOutsideSlotGroups(int slot) { + // @formatter:off + MenuLayoutBuilder builder = new MenuLayoutBuilder(9) + .addSlotGroup( + new SlotGroupBuilder('x', "test") + .withSlot(slot) + .build() + ); + // @formatter:on + + assertThrows(IllegalArgumentException.class, builder::build); + } + + @Test + void testUnknownSlotGroups() { + // @formatter:off + MenuLayout layout = new MenuLayoutBuilder(9) + .addSlotGroup( + new SlotGroupBuilder('x', "test") + .withSlotRange(0, 8) + .build() + ) + .build(); + // @formatter:on + + assertThrows(IllegalArgumentException.class, () -> layout.getGroup(-1)); + assertThrows(IllegalArgumentException.class, () -> layout.getGroup(10)); + assertThrows(IllegalArgumentException.class, () -> layout.getGroup('a')); + assertThrows(IllegalArgumentException.class, () -> layout.getGroup("Walshy")); + assertThrows(IllegalArgumentException.class, () -> layout.getGroup(null)); + } + + @Test + void testIncompleteSlotGroups() { + // @formatter:off + MenuLayoutBuilder builder = new MenuLayoutBuilder(9) + .addSlotGroup( + new SlotGroupBuilder('x', "test") + .withSlotRange(1, 8) + .build() + ); + // @formatter:on + + assertThrows(IllegalStateException.class, builder::build); + } + + @ParameterizedTest(name = "{0} is not a valid inventory size.") + @MethodSource("getIllegalInventorySizes") + void testIllegalInventorySizes(int size) { + assertThrows(IllegalArgumentException.class, () -> new MenuLayoutBuilder(size)); + } + + private static @Nonnull Stream getIllegalInventorySizes() { + List validArguments = Arrays.asList(9, 18, 27, 36, 45, 54); + + // @formatter:off + return IntStream.range(-10, 60) + .filter(arg -> !validArguments.contains(arg)) + .mapToObj(Arguments::of); + // @formatter:on + } + +} diff --git a/dough-inventories/src/test/java/io/github/bakedlibs/dough/inventory/factory/MockExceptionHandler.java b/dough-inventories/src/test/java/io/github/bakedlibs/dough/inventory/factory/MockExceptionHandler.java new file mode 100644 index 00000000..015de120 --- /dev/null +++ b/dough-inventories/src/test/java/io/github/bakedlibs/dough/inventory/factory/MockExceptionHandler.java @@ -0,0 +1,32 @@ +package io.github.bakedlibs.dough.inventory.factory; + +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; + +import javax.annotation.ParametersAreNonnullByDefault; + +class MockExceptionHandler extends Handler { + + private final AtomicReference ref; + + @ParametersAreNonnullByDefault + MockExceptionHandler(AtomicReference ref) { + this.ref = ref; + } + + @Override + public void publish(LogRecord record) { + if (record.getLevel() == Level.SEVERE) { + ref.set(record.getThrown()); + } + } + + @Override + public void flush() {} + + @Override + public void close() throws SecurityException {} + +} diff --git a/dough-inventories/src/test/java/io/github/bakedlibs/dough/inventory/factory/MockMenuFactory.java b/dough-inventories/src/test/java/io/github/bakedlibs/dough/inventory/factory/MockMenuFactory.java new file mode 100644 index 00000000..3c84dd6e --- /dev/null +++ b/dough-inventories/src/test/java/io/github/bakedlibs/dough/inventory/factory/MockMenuFactory.java @@ -0,0 +1,34 @@ +package io.github.bakedlibs.dough.inventory.factory; + +import java.io.File; + +import javax.annotation.ParametersAreNonnullByDefault; + +import org.bukkit.plugin.PluginDescriptionFile; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.plugin.java.JavaPluginLoader; + +import be.seeseemelk.mockbukkit.MockBukkit; + +public class MockMenuFactory extends MenuFactory { + + public MockMenuFactory() { + // @formatter:off + super(MockBukkit.loadWith(MockJavaPlugin.class, new PluginDescriptionFile( + "MockPlugin", + "1.0.0", + MockJavaPlugin.class.getName()) + )); + // @formatter:on + } + + public static class MockJavaPlugin extends JavaPlugin { + + @ParametersAreNonnullByDefault + public MockJavaPlugin(JavaPluginLoader loader, PluginDescriptionFile description, File dataFolder, File file) { + super(loader, description, dataFolder, file); + } + + } + +} diff --git a/dough-inventories/src/test/java/io/github/bakedlibs/dough/inventory/factory/TestMenuClicking.java b/dough-inventories/src/test/java/io/github/bakedlibs/dough/inventory/factory/TestMenuClicking.java new file mode 100644 index 00000000..f34b9493 --- /dev/null +++ b/dough-inventories/src/test/java/io/github/bakedlibs/dough/inventory/factory/TestMenuClicking.java @@ -0,0 +1,240 @@ +package io.github.bakedlibs.dough.inventory.factory; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Handler; + +import javax.annotation.Nonnull; +import javax.annotation.ParametersAreNonnullByDefault; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.Server; +import org.bukkit.entity.Player; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.event.inventory.InventoryAction; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryType.SlotType; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryView; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.Plugin; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import io.github.bakedlibs.dough.inventory.Menu; +import io.github.bakedlibs.dough.inventory.MenuLayout; +import io.github.bakedlibs.dough.inventory.builders.MenuLayoutBuilder; +import io.github.bakedlibs.dough.inventory.builders.SlotGroupBuilder; +import io.github.bakedlibs.dough.inventory.payloads.MenuClickPayload; + +import be.seeseemelk.mockbukkit.MockBukkit; +import be.seeseemelk.mockbukkit.ServerMock; + +class TestMenuClicking { + + private static ServerMock server; + private static MenuFactory factory; + private static MenuFactory factory2; + + @BeforeAll + static void setup() { + server = MockBukkit.mock(); + factory = new MockMenuFactory(); + factory2 = new MockMenuFactory(); + } + + @AfterAll + static void teardown() { + MockBukkit.unmock(); + } + + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void testClickItemInSlot(boolean interactable) { + AtomicReference payloadRef = new AtomicReference<>(); + + // @formatter:off + MenuLayout layout = new MenuLayoutBuilder(9) + .addSlotGroup( + new SlotGroupBuilder('x', "test") + .withSlots(0, 1) + .interactable(interactable) + .withDefaultItem(new ItemStack(Material.APPLE)) + .onClick(payloadRef::set) + .build() + ) + .addSlotGroup( + new SlotGroupBuilder('y', "test2") + .withSlotRange(2, 8) + .build() + ) + .build(); + // @formatter:on + + assertListenerRegistered(); + + Menu inv = factory.createMenu(layout); + Player player = server.addPlayer(); + int slot = 1; + + InventoryView view = inv.open(player); + InventoryClickEvent event = new InventoryClickEvent(view, SlotType.CONTAINER, slot, ClickType.LEFT, InventoryAction.PICKUP_ONE); + + Bukkit.getPluginManager().callEvent(event); + MenuClickPayload payload = payloadRef.get(); + + assertNotNull(payload); + assertSame(inv, payload.getMenu()); + assertSame(player, payload.getPlayer()); + + assertEquals(slot, payload.getClickedSlot()); + assertEquals(layout.getGroup("test"), payload.getClickedSlotGroup()); + assertEquals(new ItemStack(Material.APPLE), payload.getClickedItemStack()); + + assertNotEquals(interactable, event.isCancelled()); + } + + @Test + void testOtherInventoriesAreIgnored() { + AtomicReference payloadRef = new AtomicReference<>(); + + // @formatter:off + MenuLayout layout = new MenuLayoutBuilder(9) + .addSlotGroup( + new SlotGroupBuilder('x', "test") + .withSlotRange(0, 8) + .onClick(payloadRef::set) + .build() + ) + .build(); + // @formatter:on + + factory.createMenu(layout); + + Player player = server.addPlayer(); + Inventory inv = Bukkit.createInventory(null, 9); + InventoryView view = player.openInventory(inv); + InventoryClickEvent event = new InventoryClickEvent(view, SlotType.CONTAINER, 1, ClickType.LEFT, InventoryAction.PICKUP_ONE); + + Bukkit.getPluginManager().callEvent(event); + + // Make sure our listener ignored this inventory + assertNull(payloadRef.get()); + } + + @Test + void testExceptionHandling() { + // @formatter:off + MenuLayout layout = new MenuLayoutBuilder(9) + .addSlotGroup( + new SlotGroupBuilder('x', "test") + .withSlotRange(0, 2) + .onClick(payload -> { + throw new NullPointerException("NPE was thrown | This is expected!"); + }) + .build() + ) + .addSlotGroup( + new SlotGroupBuilder('y', "test2") + .withSlotRange(3, 8) + .build() + ) + .build(); + // @formatter:on + + Menu inv = factory.createMenu(layout); + assertExceptionCaughtOnClick(inv); + } + + private void assertExceptionCaughtOnClick(@Nonnull Menu inv) { + AtomicReference thrownException = new AtomicReference<>(); + Handler handler = new MockExceptionHandler(thrownException); + + factory.getLogger().addHandler(handler); + simulateClickEvents(inv, 1); + factory.getLogger().removeHandler(handler); + + assertNotNull(thrownException.get()); + assertTrue(thrownException.get() instanceof NullPointerException); + } + + @Test + void testMultipleFactories() { + Map eventsFired = new HashMap<>(); + + // @formatter:off + MenuLayout layout = new MenuLayoutBuilder(9) + .addSlotGroup( + new SlotGroupBuilder('x', "test") + .withSlotRange(0, 8) + .interactable(true) + .onClick(payload -> { + MenuFactory factory = payload.getMenu().getFactory(); + eventsFired.merge(factory, 1, Integer::sum); + }) + .build() + ) + .build(); + // @formatter:on + + Menu inv = factory.createMenu(layout); + Menu inv2 = factory2.createMenu(layout); + + simulateClickEvents(inv, 2); + simulateClickEvents(inv2, 4); + + assertEquals(2, eventsFired.get(factory)); + assertEquals(4, eventsFired.get(factory2)); + } + + @ParametersAreNonnullByDefault + private void simulateClickEvents(Menu inv, int amount) { + for (int i = 0; i < amount; i++) { + Player player = server.addPlayer(); + InventoryView view = inv.open(player); + InventoryClickEvent event = new InventoryClickEvent(view, SlotType.CONTAINER, 1, ClickType.LEFT, InventoryAction.PICKUP_ONE); + + Bukkit.getPluginManager().callEvent(event); + player.closeInventory(); + } + } + + /** + * This method asserts that the {@link Listener} for our {@link MenuFactory} + * is properly registered to the {@link Server} and listens to the {@link InventoryClickEvent}. + */ + private void assertListenerRegistered() { + assertNotNull(factory.getPlugin()); + assertNotNull(factory.getLogger()); + + // @formatter:off + assertEquals(1,Arrays.stream(InventoryClickEvent.getHandlerList().getRegisteredListeners()) + .filter(listener -> { + Plugin plugin = listener.getPlugin(); + Listener clickListener = listener.getListener(); + + if (plugin.equals(factory.getPlugin()) && clickListener instanceof MenuListener) { + return factory.equals(((MenuListener) clickListener).getFactory()); + } else { + return false; + } + }).count() + ); + // @formatter:off + } + +} diff --git a/dough-inventories/src/test/java/io/github/bakedlibs/dough/inventory/factory/TestMenuClosing.java b/dough-inventories/src/test/java/io/github/bakedlibs/dough/inventory/factory/TestMenuClosing.java new file mode 100644 index 00000000..3a3cb458 --- /dev/null +++ b/dough-inventories/src/test/java/io/github/bakedlibs/dough/inventory/factory/TestMenuClosing.java @@ -0,0 +1,65 @@ +package io.github.bakedlibs.dough.inventory.factory; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +import org.bukkit.entity.Player; +import org.bukkit.inventory.InventoryView; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import io.github.bakedlibs.dough.inventory.Menu; +import io.github.bakedlibs.dough.inventory.MenuLayout; +import io.github.bakedlibs.dough.inventory.builders.MenuLayoutBuilder; +import io.github.bakedlibs.dough.inventory.builders.SlotGroupBuilder; + +import be.seeseemelk.mockbukkit.MockBukkit; +import be.seeseemelk.mockbukkit.ServerMock; + +class TestMenuClosing { + + private static MenuFactory factory; + private static ServerMock server; + + @BeforeAll + static void setup() { + server = MockBukkit.mock(); + factory = new MockMenuFactory(); + } + + @AfterAll + static void teardown() { + MockBukkit.unmock(); + } + + @Test + void testCloseAll() { + // @formatter:off + MenuLayout layout = new MenuLayoutBuilder(9) + .addSlotGroup( + new SlotGroupBuilder('x', "test") + .withSlotRange(0, 8) + .build() + ) + .build(); + // @formatter:on + + Menu inv = factory.createMenu(layout); + + Player player = server.addPlayer(); + InventoryView view = inv.open(player); + + Player player2 = server.addPlayer(); + InventoryView view2 = inv.open(player2); + + assertSame(view, player.getOpenInventory()); + assertSame(view2, player2.getOpenInventory()); + + inv.closeAllViews(); + + assertNull(player.getOpenInventory()); + assertNull(player2.getOpenInventory()); + } + +} diff --git a/dough-inventories/src/test/java/io/github/bakedlibs/dough/inventory/factory/TestMenuCreation.java b/dough-inventories/src/test/java/io/github/bakedlibs/dough/inventory/factory/TestMenuCreation.java new file mode 100644 index 00000000..4c89125e --- /dev/null +++ b/dough-inventories/src/test/java/io/github/bakedlibs/dough/inventory/factory/TestMenuCreation.java @@ -0,0 +1,186 @@ +package io.github.bakedlibs.dough.inventory.factory; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import io.github.bakedlibs.dough.inventory.Menu; +import io.github.bakedlibs.dough.inventory.MenuLayout; +import io.github.bakedlibs.dough.inventory.SlotGroup; +import io.github.bakedlibs.dough.inventory.builders.MenuLayoutBuilder; +import io.github.bakedlibs.dough.inventory.builders.SlotGroupBuilder; + +import be.seeseemelk.mockbukkit.MockBukkit; + +class TestMenuCreation { + + private static MenuFactory factory; + + @BeforeAll + static void setup() { + MockBukkit.mock(); + factory = new MockMenuFactory(); + } + + @AfterAll + static void teardown() { + MockBukkit.unmock(); + } + + @ParameterizedTest + @ValueSource(ints = { 9, 18, 27, 36, 45, 54 }) + void testCreationWithSize(int size) { + // @formatter:off + MenuLayout layout = new MenuLayoutBuilder(size) + .title("Awesome Inventory!") + .addSlotGroup( + new SlotGroupBuilder('x', "test") + .withSlotRange(0, size - 1) + .build() + ) + .build(); + // @formatter:on + + Menu inv = factory.createMenu(layout); + + assertNotNull(inv); + assertEquals(layout, inv.getLayout()); + assertEquals(factory, inv.getFactory()); + assertEquals(layout.getTitle(), inv.getTitle()); + assertEquals(layout.getSize(), inv.getSize()); + + assertNotNull(inv.getInventory()); + assertSame(inv, inv.getInventory().getHolder()); + } + + @Test + void testInventoryValidation() { + // @formatter:off + MenuLayout layout = new MenuLayoutBuilder(9) + .addSlotGroup( + new SlotGroupBuilder('x', "test") + .withSlotRange(0, 8) + .build() + ) + .build(); + // @formatter:on + + Menu inv = new CustomMenu(factory, layout); + + assertThrows(UnsupportedOperationException.class, () -> inv.getInventory()); + } + + @Test + void testInventoryHolderValidation() { + // @formatter:off + MenuLayout layout = new MenuLayoutBuilder(9) + .addSlotGroup( + new SlotGroupBuilder('x', "test") + .withSlotRange(0, 8) + .build() + ) + .build(); + // @formatter:on + + CustomMenu inv = new CustomMenu(factory, layout); + + // InventoryHolder == null + Inventory inventory = Bukkit.createInventory(null, 9); + + assertThrows(IllegalArgumentException.class, () -> inv.setInventory(inventory)); + + // Different inventory size + Inventory inventory2 = Bukkit.createInventory(inv, 18); + + assertThrows(IllegalArgumentException.class, () -> inv.setInventory(inventory2)); + } + + @Test + void testDefaultItem() { + // @formatter:off + MenuLayout layout = new MenuLayoutBuilder(9) + .addSlotGroup( + new SlotGroupBuilder('x', "test") + .withSlots(0, 1) + .withDefaultItem(new ItemStack(Material.APPLE)) + .build() + ) + .addSlotGroup( + new SlotGroupBuilder('y', "test2") + .withSlotRange(2, 8) + .build() + ) + .build(); + // @formatter:on + + Menu inv = factory.createMenu(layout); + + assertEquals(new ItemStack(Material.APPLE), inv.getItem(0)); + assertEquals(new ItemStack(Material.APPLE), inv.getItem(1)); + assertNull(inv.getItem(2)); + assertNull(inv.getItem(3)); + assertNull(inv.getItem(4)); + assertNull(inv.getItem(5)); + assertNull(inv.getItem(6)); + assertNull(inv.getItem(7)); + assertNull(inv.getItem(8)); + } + + @Test + void testAddItem() { + // @formatter:off + MenuLayout layout = new MenuLayoutBuilder(9) + .addSlotGroup( + new SlotGroupBuilder('x', "test") + .withSlotRange(0, 5) + .build() + ) + .addSlotGroup( + new SlotGroupBuilder('y', "test2") + .withSlotRange(6, 8) + .build() + ) + .build(); + // @formatter:on + + Menu inv = factory.createMenu(layout); + SlotGroup group = layout.getGroup('y'); + + inv.setItem(6, new ItemStack(Material.AIR)); + + ItemStack item = new ItemStack(Material.EMERALD); + inv.addItem(group, item); + + assertNull(inv.getItem(0)); + assertEquals(item, inv.getItem(6)); + + inv.addItem(group, item); + assertEquals(2, inv.getItem(6).getAmount()); + + inv.addItem(group, new ItemStack(Material.EMERALD, 63)); + assertEquals(64, inv.getItem(6).getAmount()); + assertEquals(item, inv.getItem(7)); + + inv.addItem(group, item); + assertEquals(2, inv.getItem(7).getAmount()); + + inv.addItem(group, new ItemStack(Material.DIAMOND)); + assertEquals(new ItemStack(Material.DIAMOND), inv.getItem(8)); + + ItemStack cantFit = new ItemStack(Material.GOLDEN_APPLE); + assertEquals(cantFit, inv.addItem(group, cantFit)); + } + +} diff --git a/dough-inventories/src/test/java/io/github/bakedlibs/dough/inventory/json/TestJsonParsing.java b/dough-inventories/src/test/java/io/github/bakedlibs/dough/inventory/json/TestJsonParsing.java new file mode 100644 index 00000000..571b7d6e --- /dev/null +++ b/dough-inventories/src/test/java/io/github/bakedlibs/dough/inventory/json/TestJsonParsing.java @@ -0,0 +1,136 @@ +package io.github.bakedlibs.dough.inventory.json; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.InputStream; + +import javax.annotation.Nonnull; +import javax.annotation.ParametersAreNonnullByDefault; + +import org.junit.jupiter.api.Test; + +import io.github.bakedlibs.dough.inventory.MenuLayout; +import io.github.bakedlibs.dough.inventory.SlotGroup; + +class TestJsonParsing { + + @Test + void testValidFile() { + assertDoesNotThrow(() -> parse("valid1")); + } + + @Test + void testCorrectSlotGroups() throws InvalidLayoutException { + MenuLayout layout = parse("valid2"); + + assertEquals(27, layout.getSize()); + assertEquals(3, layout.getSlotGroups().size()); + + assertSlotGroup(layout, 0, '*', "background", false); + assertSlotGroup(layout, 13, '*', "background", false); + assertSlotGroup(layout, 26, '*', "background", false); + + assertSlotGroup(layout, 10, '+', "input", true); + assertSlotGroup(layout, 11, '+', "input", true); + + assertSlotGroup(layout, 14, '-', "output", true); + assertSlotGroup(layout, 17, '-', "output", true); + } + + @ParametersAreNonnullByDefault + private void assertSlotGroup(MenuLayout layout, int slot, char identifier, String name, boolean interactable) { + SlotGroup group1 = layout.getGroup(slot); + SlotGroup group2 = layout.getGroup(identifier); + SlotGroup group3 = layout.getGroup(name); + + assertSame(group1, group2); + assertSame(group1, group3); + assertSame(group2, group3); + + // All three are the same, so this should apply to all of them. + assertEquals(interactable, group1.isInteractable()); + } + + @Test + void testNonExistingFile() { + assertThrows(IllegalArgumentException.class, () -> parse("I do not exist")); + } + + @Test + void testMalformedJson() { + assertThrows(InvalidLayoutException.class, () -> LayoutParser.parseString("{ Is this \"\" how ] you json...?: false,")); + } + + @Test + void testTooManyRows() { + assertInvalid("invalid1"); + } + + @Test + void testNoRows() { + assertInvalid("invalid2"); + } + + @Test + void testMissingLayoutObject() { + assertInvalid("invalid3"); + } + + @Test + void testMissingGroupsObject() { + assertInvalid("invalid4"); + } + + @Test + void testIncompleteGroupsObject() { + assertInvalid("invalid5"); + } + + @Test + void testLayoutWrongType() { + assertInvalid("invalid6"); + } + + @Test + void testGroupsWrongType() { + assertInvalid("invalid7"); + } + + @Test + void testLayoutRowsWrongType() { + assertInvalid("invalid8"); + } + + @Test + void testMissingSlotGroupName() { + assertInvalid("invalid9"); + } + + @Test + void testMissingSlotGroupInteractable() { + assertInvalid("invalid10"); + } + + @Test + void testSlotGroupInteractableWrongType() { + assertInvalid("invalid11"); + } + + @Test + void testTooLongRow() { + assertInvalid("invalid12"); + } + + private void assertInvalid(@Nonnull String name) { + assertThrows(InvalidLayoutException.class, () -> parse(name)); + } + + private @Nonnull MenuLayout parse(@Nonnull String name) throws InvalidLayoutException { + InputStream inputStream = getClass().getResourceAsStream("/" + name + ".json"); + return LayoutParser.parseStream(inputStream); + } + +} diff --git a/dough-inventories/src/test/java/io/github/bakedlibs/dough/inventory/json/TestSlotGroupInjection.java b/dough-inventories/src/test/java/io/github/bakedlibs/dough/inventory/json/TestSlotGroupInjection.java new file mode 100644 index 00000000..933c85ff --- /dev/null +++ b/dough-inventories/src/test/java/io/github/bakedlibs/dough/inventory/json/TestSlotGroupInjection.java @@ -0,0 +1,111 @@ +package io.github.bakedlibs.dough.inventory.json; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.InputStream; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +import javax.annotation.Nonnull; +import javax.annotation.ParametersAreNonnullByDefault; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +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.InventoryType.SlotType; +import org.bukkit.inventory.InventoryView; +import org.bukkit.inventory.ItemStack; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import io.github.bakedlibs.dough.inventory.Menu; +import io.github.bakedlibs.dough.inventory.MenuLayout; +import io.github.bakedlibs.dough.inventory.builders.SlotGroupBuilder; +import io.github.bakedlibs.dough.inventory.factory.MenuFactory; +import io.github.bakedlibs.dough.inventory.factory.MockMenuFactory; + +import be.seeseemelk.mockbukkit.MockBukkit; +import be.seeseemelk.mockbukkit.ServerMock; + +class TestSlotGroupInjection { + + private static ServerMock server; + private static MenuFactory factory; + + @BeforeAll + static void setup() { + server = MockBukkit.mock(); + factory = new MockMenuFactory(); + } + + @AfterAll + static void teardown() { + MockBukkit.unmock(); + } + + @Test + void testClickEvent() throws InvalidLayoutException { + AtomicInteger clicks = new AtomicInteger(); + + MenuLayout layout = parse("valid3", builder -> { + if (!builder.name().equals("background")) { + builder.onClick(payload -> clicks.incrementAndGet()); + } + }); + + Menu menu = factory.createMenu(layout); + Player player = server.addPlayer(); + InventoryView view = menu.open(player); + + simulateClick(player, view, 1); + simulateClick(player, view, 11); + + // Only slot 11 should have triggered this + assertEquals(1, clicks.get()); + } + + @Test + void testItemInjection() throws InvalidLayoutException { + MenuLayout layout = parse("valid3", builder -> { + if (builder.name().equals("background")) { + builder.withDefaultItem(new ItemStack(Material.GRAY_STAINED_GLASS_PANE)); + } + }); + + Menu menu = factory.createMenu(layout); + Player player = server.addPlayer(); + InventoryView view = menu.open(player); + + InventoryClickEvent clickEvent = simulateClick(player, view, 1); + + assertEquals(new ItemStack(Material.GRAY_STAINED_GLASS_PANE), menu.getItem(clickEvent.getSlot())); + assertTrue(clickEvent.isCancelled()); + + InventoryClickEvent clickEvent2 = simulateClick(player, view, 11); + + assertEquals(null, menu.getItem(clickEvent2.getSlot())); + assertFalse(clickEvent2.isCancelled()); + } + + @ParametersAreNonnullByDefault + private @Nonnull InventoryClickEvent simulateClick(Player player, InventoryView view, int slot) { + InventoryClickEvent event = new InventoryClickEvent(view, SlotType.CONTAINER, slot, ClickType.LEFT, InventoryAction.PICKUP_ONE); + + Bukkit.getPluginManager().callEvent(event); + player.closeInventory(); + return event; + } + + @ParametersAreNonnullByDefault + private @Nonnull MenuLayout parse(String name, Consumer consumer) throws InvalidLayoutException { + InputStream inputStream = getClass().getResourceAsStream("/" + name + ".json"); + return LayoutParser.parseStream(inputStream, consumer); + } + +} diff --git a/dough-inventories/src/test/resources/invalid1.json b/dough-inventories/src/test/resources/invalid1.json new file mode 100644 index 00000000..998124d0 --- /dev/null +++ b/dough-inventories/src/test/resources/invalid1.json @@ -0,0 +1,25 @@ +{ + "layout" : [ + "*********", + "*********", + "++++*----", + "++++*----", + "*********", + "*********", + "*********" + ], + "groups" : { + "*" : { + "name" : "background", + "interactable" : false + }, + "+" : { + "name" : "test1", + "interactable" : false + }, + "-" : { + "name" : "test2", + "interactable" : false + } + } +} diff --git a/dough-inventories/src/test/resources/invalid10.json b/dough-inventories/src/test/resources/invalid10.json new file mode 100644 index 00000000..c1b38173 --- /dev/null +++ b/dough-inventories/src/test/resources/invalid10.json @@ -0,0 +1,21 @@ +{ + "layout" : [ + "*********", + "*********", + "++++*----", + "++++*----", + "*********", + "*********" + ], + "groups" : { + "*" : { + "name" : "background" + }, + "+" : { + "name" : "test1" + }, + "-" : { + "name" : "test2" + } + } +} diff --git a/dough-inventories/src/test/resources/invalid11.json b/dough-inventories/src/test/resources/invalid11.json new file mode 100644 index 00000000..f57f7b24 --- /dev/null +++ b/dough-inventories/src/test/resources/invalid11.json @@ -0,0 +1,24 @@ +{ + "layout" : [ + "*********", + "*********", + "++++*----", + "++++*----", + "*********", + "*********" + ], + "groups" : { + "*" : { + "name" : "background", + "interactable" : 1 + }, + "+" : { + "name" : "test1", + "interactable" : 2 + }, + "-" : { + "name" : "test2", + "interactable" : 3 + } + } +} diff --git a/dough-inventories/src/test/resources/invalid12.json b/dough-inventories/src/test/resources/invalid12.json new file mode 100644 index 00000000..3a99c721 --- /dev/null +++ b/dough-inventories/src/test/resources/invalid12.json @@ -0,0 +1,21 @@ +{ + "layout" : [ + "**************************", + "++++*----", + "++++*----" + ], + "groups" : { + "*" : { + "name" : "background", + "interactable" : false + }, + "+" : { + "name" : "test1", + "interactable" : false + }, + "-" : { + "name" : "test2", + "interactable" : false + } + } +} diff --git a/dough-inventories/src/test/resources/invalid2.json b/dough-inventories/src/test/resources/invalid2.json new file mode 100644 index 00000000..a3089366 --- /dev/null +++ b/dough-inventories/src/test/resources/invalid2.json @@ -0,0 +1,18 @@ +{ + "layout" : [ + ], + "groups" : { + "*" : { + "name" : "background", + "interactable" : false + }, + "+" : { + "name" : "test1", + "interactable" : false + }, + "-" : { + "name" : "test2", + "interactable" : false + } + } +} diff --git a/dough-inventories/src/test/resources/invalid3.json b/dough-inventories/src/test/resources/invalid3.json new file mode 100644 index 00000000..5c72b7d6 --- /dev/null +++ b/dough-inventories/src/test/resources/invalid3.json @@ -0,0 +1,16 @@ +{ + "groups" : { + "*" : { + "name" : "background", + "interactable" : false + }, + "+" : { + "name" : "test1", + "interactable" : false + }, + "-" : { + "name" : "test2", + "interactable" : false + } + } +} diff --git a/dough-inventories/src/test/resources/invalid4.json b/dough-inventories/src/test/resources/invalid4.json new file mode 100644 index 00000000..57831e54 --- /dev/null +++ b/dough-inventories/src/test/resources/invalid4.json @@ -0,0 +1,10 @@ +{ + "layout" : [ + "*********", + "*********", + "++++*----", + "++++*----", + "*********", + "*********" + ] +} diff --git a/dough-inventories/src/test/resources/invalid5.json b/dough-inventories/src/test/resources/invalid5.json new file mode 100644 index 00000000..97497b80 --- /dev/null +++ b/dough-inventories/src/test/resources/invalid5.json @@ -0,0 +1,16 @@ +{ + "layout" : [ + "*********", + "*********", + "++++*----", + "++++*----", + "*********", + "*********" + ], + "groups" : { + "*" : { + "name" : "background", + "interactable" : false + } + } +} diff --git a/dough-inventories/src/test/resources/invalid6.json b/dough-inventories/src/test/resources/invalid6.json new file mode 100644 index 00000000..502f0e05 --- /dev/null +++ b/dough-inventories/src/test/resources/invalid6.json @@ -0,0 +1,17 @@ +{ + "layout" : true, + "groups" : { + "*" : { + "name" : "background", + "interactable" : false + }, + "+" : { + "name" : "test1", + "interactable" : false + }, + "-" : { + "name" : "test2", + "interactable" : false + } + } +} diff --git a/dough-inventories/src/test/resources/invalid7.json b/dough-inventories/src/test/resources/invalid7.json new file mode 100644 index 00000000..90d6e4c2 --- /dev/null +++ b/dough-inventories/src/test/resources/invalid7.json @@ -0,0 +1,11 @@ +{ + "layout" : [ + "*********", + "*********", + "++++*----", + "++++*----", + "*********", + "*********" + ], + "groups" : 6 +} diff --git a/dough-inventories/src/test/resources/invalid8.json b/dough-inventories/src/test/resources/invalid8.json new file mode 100644 index 00000000..8dd56ba1 --- /dev/null +++ b/dough-inventories/src/test/resources/invalid8.json @@ -0,0 +1,22 @@ +{ + "layout" : [ + 1, + 2, + 3, + 4 + ], + "groups" : { + "*" : { + "name" : "background", + "interactable" : false + }, + "+" : { + "name" : "test1", + "interactable" : false + }, + "-" : { + "name" : "test2", + "interactable" : false + } + } +} diff --git a/dough-inventories/src/test/resources/invalid9.json b/dough-inventories/src/test/resources/invalid9.json new file mode 100644 index 00000000..8458deee --- /dev/null +++ b/dough-inventories/src/test/resources/invalid9.json @@ -0,0 +1,21 @@ +{ + "layout" : [ + "*********", + "*********", + "++++*----", + "++++*----", + "*********", + "*********" + ], + "groups" : { + "*" : { + "interactable" : false + }, + "+" : { + "interactable" : false + }, + "-" : { + "interactable" : false + } + } +} diff --git a/dough-inventories/src/test/resources/valid1.json b/dough-inventories/src/test/resources/valid1.json new file mode 100644 index 00000000..9809def6 --- /dev/null +++ b/dough-inventories/src/test/resources/valid1.json @@ -0,0 +1,51 @@ +{ + "layout" : [ + "*********", + "++++*----", + "+ii+P-oo-", + "++++*----", + "*********" + ], + "groups" : { + "*" : { + "name" : "background", + "interactable" : false, + "item" : { + "material" : "minecraft:gray_stained_glass_pane", + "name" : "" + } + }, + "+" : { + "name" : "input_background", + "interactable" : false, + "item" : { + "material" : "minecraft:cyan_stained_glass_pane", + "name" : "" + } + }, + "-" : { + "name" : "output_background", + "interactable" : false, + "item" : { + "material" : "minecraft:orange_stained_glass_pane", + "name" : "" + } + }, + "P" : { + "name" : "progress_bar", + "interactable" : false, + "item" : { + "material" : "minecraft:black_stained_glass_pane", + "name" : "" + } + }, + "i" : { + "name" : "input", + "interactable" : true + }, + "o" : { + "name" : "output", + "interactable" : true + } + } +} diff --git a/dough-inventories/src/test/resources/valid2.json b/dough-inventories/src/test/resources/valid2.json new file mode 100644 index 00000000..aa9c812b --- /dev/null +++ b/dough-inventories/src/test/resources/valid2.json @@ -0,0 +1,25 @@ +{ + "layout" : [ + "*********", + "++++*----", + "*********" + ], + "groups" : { + "*" : { + "name" : "background", + "interactable" : false, + "item" : { + "material" : "minecraft:gray_stained_glass_pane", + "name" : "" + } + }, + "+" : { + "name" : "input", + "interactable" : true + }, + "-" : { + "name" : "output", + "interactable" : true + } + } +} diff --git a/dough-inventories/src/test/resources/valid3.json b/dough-inventories/src/test/resources/valid3.json new file mode 100644 index 00000000..4674484a --- /dev/null +++ b/dough-inventories/src/test/resources/valid3.json @@ -0,0 +1,21 @@ +{ + "layout" : [ + "*********", + "++++*----", + "*********" + ], + "groups" : { + "*" : { + "name" : "background", + "interactable" : false + }, + "+" : { + "name" : "input", + "interactable" : true + }, + "-" : { + "name" : "output", + "interactable" : true + } + } +} diff --git a/dough-items/pom.xml b/dough-items/pom.xml index 3bf126c8..359a4685 100644 --- a/dough-items/pom.xml +++ b/dough-items/pom.xml @@ -7,7 +7,7 @@ io.github.baked-libs dough - 1.1.3 + 1.2.0 dough-items diff --git a/dough-protection/pom.xml b/dough-protection/pom.xml index 16a96d94..bb5b032d 100644 --- a/dough-protection/pom.xml +++ b/dough-protection/pom.xml @@ -7,7 +7,7 @@ io.github.baked-libs dough - 1.1.3 + 1.2.0 dough-protection diff --git a/dough-recipes/pom.xml b/dough-recipes/pom.xml index a94fdf1e..ee47827a 100644 --- a/dough-recipes/pom.xml +++ b/dough-recipes/pom.xml @@ -7,7 +7,7 @@ io.github.baked-libs dough - 1.1.3 + 1.2.0 dough-recipes diff --git a/dough-reflection/pom.xml b/dough-reflection/pom.xml index a72e2ab9..495b07a5 100644 --- a/dough-reflection/pom.xml +++ b/dough-reflection/pom.xml @@ -7,7 +7,7 @@ io.github.baked-libs dough - 1.1.3 + 1.2.0 dough-reflection diff --git a/dough-scheduling/pom.xml b/dough-scheduling/pom.xml index b8ef4efa..3761b599 100644 --- a/dough-scheduling/pom.xml +++ b/dough-scheduling/pom.xml @@ -7,7 +7,7 @@ io.github.baked-libs dough - 1.1.3 + 1.2.0 dough-scheduling diff --git a/dough-skins/pom.xml b/dough-skins/pom.xml index 49056831..90fc17d9 100644 --- a/dough-skins/pom.xml +++ b/dough-skins/pom.xml @@ -7,7 +7,7 @@ io.github.baked-libs dough - 1.1.3 + 1.2.0 dough-skins diff --git a/dough-updater/pom.xml b/dough-updater/pom.xml index d91f321a..8369f050 100644 --- a/dough-updater/pom.xml +++ b/dough-updater/pom.xml @@ -7,7 +7,7 @@ io.github.baked-libs dough - 1.1.3 + 1.2.0 dough-updater diff --git a/pom.xml b/pom.xml index 7d81ea53..909a691d 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.baked-libs dough - 1.1.3 + 1.2.0 pom