From 3946f3fb66d6cf2bbdbdf31877996a8ee65ea07b Mon Sep 17 00:00:00 2001 From: fullwall Date: Sun, 31 Jan 2021 00:29:42 +0800 Subject: [PATCH] Additional work on inventory GUIs --- .../citizensnpcs/api/gui/ClickHandler.java | 10 +- .../citizensnpcs/api/gui/InjectContext.java | 14 +++ .../citizensnpcs/api/gui/InventoryMenu.java | 106 ++++++++++++++---- .../api/gui/InventoryMenuPattern.java | 10 ++ .../api/gui/InventoryMenuSlot.java | 59 ++++++++-- .../api/gui/InventoryMenuTransition.java | 18 ++- .../net/citizensnpcs/api/gui/MenuSlot.java | 18 ++- .../api/trait/trait/Equipment.java | 3 + .../api/trait/trait/Inventory.java | 6 + 9 files changed, 200 insertions(+), 44 deletions(-) create mode 100644 src/main/java/net/citizensnpcs/api/gui/InjectContext.java diff --git a/src/main/java/net/citizensnpcs/api/gui/ClickHandler.java b/src/main/java/net/citizensnpcs/api/gui/ClickHandler.java index 0745ccee..5fd7791e 100644 --- a/src/main/java/net/citizensnpcs/api/gui/ClickHandler.java +++ b/src/main/java/net/citizensnpcs/api/gui/ClickHandler.java @@ -6,7 +6,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.bukkit.event.inventory.ClickType; +import org.bukkit.event.inventory.InventoryAction; import org.bukkit.event.inventory.InventoryClickEvent; /** @@ -18,12 +18,12 @@ @Repeatable(ClickHandlers.class) public @interface ClickHandler { /** - * The slot position to handle clicks for. + * An optional filter for specific actions. Default = handle all clicks */ - int[] slot(); + InventoryAction[] filter() default {}; /** - * An optional filter for specific click types. Default = handle all clicks + * The slot position to handle clicks for. */ - ClickType[] value() default {}; + int[] slot(); } diff --git a/src/main/java/net/citizensnpcs/api/gui/InjectContext.java b/src/main/java/net/citizensnpcs/api/gui/InjectContext.java new file mode 100644 index 00000000..4c06a6a4 --- /dev/null +++ b/src/main/java/net/citizensnpcs/api/gui/InjectContext.java @@ -0,0 +1,14 @@ +package net.citizensnpcs.api.gui; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marker annotation to inject context variables at runtime into {@link InventoryMenuPage}s. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.FIELD }) +public @interface InjectContext { +} diff --git a/src/main/java/net/citizensnpcs/api/gui/InventoryMenu.java b/src/main/java/net/citizensnpcs/api/gui/InventoryMenu.java index d132bc23..f2fa99b0 100644 --- a/src/main/java/net/citizensnpcs/api/gui/InventoryMenu.java +++ b/src/main/java/net/citizensnpcs/api/gui/InventoryMenu.java @@ -14,13 +14,12 @@ import java.util.Queue; import java.util.WeakHashMap; -import javax.inject.Inject; - import org.bukkit.Bukkit; import org.bukkit.entity.Player; +import org.bukkit.event.Event.Result; import org.bukkit.event.EventHandler; +import org.bukkit.event.HandlerList; 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.InventoryCloseEvent; @@ -32,7 +31,9 @@ import com.google.common.collect.Maps; import com.google.common.collect.Queues; -// TODO: injection +import net.citizensnpcs.api.util.Colorizer; + +// TODO: class-based injection? runnables? sub-inventory pages /** * A container class for Inventory GUIs. Expects {@link #onInventoryClick(InventoryClickEvent)} and * {@link #onInventoryClose(InventoryCloseEvent)} to be called by the user (or registered with the event listener @@ -51,7 +52,7 @@ * {@link InventoryMenuPage}s can either annotate specific instances of these concrete classes which will be injected at * runtime or simply place them at the method/class level. * - * Instances of global/contextual variables can be injected dynamically via {@link javax.inject.Inject} which sources + * Instances of global/contextual variables can be injected dynamically via {@link InjectContext} which sources * variables from the {@link MenuContext}. */ public class InventoryMenu implements Listener { @@ -59,12 +60,16 @@ public class InventoryMenu implements Listener { private final Queue stack = Queues.newArrayDeque(); private Collection views = Lists.newArrayList(); + public InventoryMenu(InventoryMenuInfo info, InventoryMenuPage instance) { + transition(info, instance, Maps.newHashMap()); + } + private InventoryMenu(InventoryMenuInfo info, Map context) { - transition(info, context); + transition(info, info.createInstance(), context); } - private boolean acceptFilter(ClickType needle, ClickType[] haystack) { - for (ClickType type : haystack) { + private boolean acceptFilter(InventoryAction needle, InventoryAction[] haystack) { + for (InventoryAction type : haystack) { if (needle == type) { return true; } @@ -72,6 +77,17 @@ private boolean acceptFilter(ClickType needle, ClickType[] haystack) { return haystack.length == 0; } + /** + * Closes the GUI and all associated viewer inventories. + */ + public void close() { + HandlerList.unregisterAll(this); + for (InventoryView view : views) { + page.page.onClose(view.getPlayer()); + view.close(); + } + } + private InventoryMenuSlot createSlot(int pos, MenuSlot slotInfo) { InventoryMenuSlot slot = page.ctx.getSlot(pos); slot.initialise(slotInfo); @@ -80,7 +96,7 @@ private InventoryMenuSlot createSlot(int pos, MenuSlot slotInfo) { private InventoryMenuTransition createTransition(int pos, MenuTransition transitionInfo) { InventoryMenuSlot slot = page.ctx.getSlot(pos); - InventoryMenuTransition transition = new InventoryMenuTransition(this, slot, transitionInfo.value()); + InventoryMenuTransition transition = new InventoryMenuTransition(slot, transitionInfo.value()); return transition; } @@ -170,19 +186,28 @@ public void onInventoryClick(InventoryClickEvent event) { break; } InventoryMenuSlot slot = page.ctx.getSlot(event.getSlot()); + slot.onClick(event); + if (event.isCancelled()) { + return; + } page.page.onClick(slot, event); for (Invokable invokable : page.clickHandlers) { int idx = posToIndex(page.dim, invokable.data.slot()); - if (event.getSlot() == idx && acceptFilter(event.getClick(), invokable.data.value())) { + if (event.getSlot() != idx) + continue; + if (acceptFilter(event.getAction(), invokable.data.filter())) { try { // TODO: bind optional args? invokable.method.invoke(page.page, slot, event); } catch (Throwable e) { e.printStackTrace(); } + } else { + event.setCancelled(true); + event.setResult(Result.DENY); + return; } } - slot.onClick(event); for (InventoryMenuTransition transition : page.transitions) { Class next = transition.accept(slot); if (next != null) { @@ -281,10 +306,11 @@ public void transition(Class clazz, Map context) { + private void transition(InventoryMenuInfo info, InventoryMenuPage instance, Map context) { if (page != null) { context.putAll(page.ctx.data()); page.ctx.data().clear(); @@ -296,23 +322,19 @@ private void transition(InventoryMenuInfo info, Map context) { int size = getInventorySize(type, dim); Inventory inventory; if (type == InventoryType.CHEST || type == null) { - inventory = Bukkit.createInventory(null, size, info.menuAnnotation.title()); + inventory = Bukkit.createInventory(null, size, Colorizer.parseColors(info.menuAnnotation.title())); } else { - inventory = Bukkit.createInventory(null, type, info.menuAnnotation.title()); + inventory = Bukkit.createInventory(null, type, Colorizer.parseColors(info.menuAnnotation.title())); } List transitions = Lists.newArrayList(); InventoryMenuSlot[] slots = new InventoryMenuSlot[inventory.getSize()]; page.patterns = new InventoryMenuPattern[info.patterns.length]; - try { - page.page = info.constructor.newInstance(); - } catch (Exception e) { - throw new RuntimeException(e); - } + page.page = instance; page.dim = dim; page.ctx = new MenuContext(this, slots, inventory, context); for (int i = 0; i < info.slots.length; i++) { Bindable slotInfo = info.slots[i]; - int pos = posToIndex(dim, slotInfo.data.value()); + int pos = posToIndex(dim, slotInfo.data.slot()); InventoryMenuSlot slot = createSlot(pos, slotInfo.data); slotInfo.bind(page.page, slot); } @@ -336,6 +358,27 @@ private void transition(InventoryMenuInfo info, Map context) { transitionViewersToInventory(inventory); } + /** + * Transition to another page. Adds the previous page to a stack which will be returned to when the current page is + * closed. + */ + public void transition(InventoryMenuPage instance) { + transition(instance, Maps.newHashMap()); + } + + /** + * Transition to another page with context. Adds the previous page to a stack which will be returned to when the + * current page is closed. + */ + public void transition(InventoryMenuPage instance, Map context) { + Class clazz = instance.getClass(); + if (!CACHED_INFOS.containsKey(clazz)) { + cacheInfo(clazz); + } + InventoryMenuInfo info = CACHED_INFOS.get(clazz); + transition(info, instance, context); + } + private void transitionViewersToInventory(Inventory inventory) { Collection old = views; views = Lists.newArrayListWithExpectedSize(old.size()); @@ -384,6 +427,14 @@ public InventoryMenuInfo(Class clazz) { injectables = getInjectables(clazz); } + public InventoryMenuPage createInstance() { + try { + return constructor.newInstance(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + @SuppressWarnings({ "unchecked" }) private Bindable[] getBindables(Class clazz, Class annotationType, Class concreteType) { @@ -439,7 +490,7 @@ private Map getInjectables(Class clazz) { Map injectables = Maps.newHashMap(); for (Field field : clazz.getDeclaredFields()) { field.setAccessible(true); - if (!field.isAnnotationPresent(Inject.class)) + if (!field.isAnnotationPresent(InjectContext.class)) continue; try { injectables.put(field.getName(), LOOKUP.unreflectSetter(field)); @@ -508,6 +559,17 @@ public static InventoryMenu create(Class clazz) { return createWithContext(clazz, Maps.newHashMap()); } + /** + * Create an inventory menu instance starting at the given page. + */ + public static InventoryMenu create(InventoryMenuPage instance) { + Class clazz = instance.getClass(); + if (!CACHED_INFOS.containsKey(clazz)) { + cacheInfo(clazz); + } + return new InventoryMenu(CACHED_INFOS.get(clazz), instance); + } + /** * Create an inventory menu instance starting at the given page and with the initial context. */ diff --git a/src/main/java/net/citizensnpcs/api/gui/InventoryMenuPattern.java b/src/main/java/net/citizensnpcs/api/gui/InventoryMenuPattern.java index 9bfb26e9..28f2ee56 100644 --- a/src/main/java/net/citizensnpcs/api/gui/InventoryMenuPattern.java +++ b/src/main/java/net/citizensnpcs/api/gui/InventoryMenuPattern.java @@ -18,14 +18,24 @@ public InventoryMenuPattern(MenuPattern info, Collection slot this.transitions = transitions; } + /** + * @return The pattern string. + */ public String getPattern() { return info.value(); } + /** + * @return The set of {@link InventoryMenuSlot}s that this pattern refers to. + */ public Collection getSlots() { return slots; } + /** + * + * @return The set of {@link InventoryMenuTransition}s that this pattern refers to. + */ public Collection getTransitions() { return transitions; } diff --git a/src/main/java/net/citizensnpcs/api/gui/InventoryMenuSlot.java b/src/main/java/net/citizensnpcs/api/gui/InventoryMenuSlot.java index c3f3aba4..84bcbc17 100644 --- a/src/main/java/net/citizensnpcs/api/gui/InventoryMenuSlot.java +++ b/src/main/java/net/citizensnpcs/api/gui/InventoryMenuSlot.java @@ -5,17 +5,25 @@ import java.util.EnumSet; import java.util.Set; +import org.bukkit.event.Event.Result; import org.bukkit.event.inventory.ClickType; +import org.bukkit.event.inventory.InventoryAction; import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import net.citizensnpcs.api.util.Colorizer; + +/** + * Represents a single inventory slot in a {@link InventoryMenu}. + */ public class InventoryMenuSlot { - private Set clickFilter = EnumSet.allOf(ClickType.class); + private Set actionFilter = EnumSet.allOf(InventoryAction.class); private final int index; private final Inventory inventory; - public InventoryMenuSlot(MenuContext menu, int i) { + InventoryMenuSlot(MenuContext menu, int i) { this.inventory = menu.getInventory(); this.index = i; } @@ -42,28 +50,63 @@ public boolean equals(Object obj) { return true; } + /** + * @return The set of {@link InventoryAction}s that will be allowed + */ + public Collection getFilter() { + return actionFilter; + } + @Override public int hashCode() { int result = 31 + index; return 31 * result + ((inventory == null) ? 0 : inventory.hashCode()); } - public void initialise(MenuSlot data) { + void initialise(MenuSlot data) { ItemStack defaultItem = null; if (data.material() != null) { defaultItem = new ItemStack(data.material(), data.amount()); } + if (defaultItem != null) { + ItemMeta meta = defaultItem.getItemMeta(); + if (!data.lore().isEmpty()) { + meta.setLore(Arrays.asList(Colorizer.parseColors(data.lore()).split("\\n|\n"))); + } + if (!data.title().isEmpty()) { + meta.setDisplayName(Colorizer.parseColors(data.title())); + } + defaultItem.setItemMeta(meta); + } inventory.setItem(index, defaultItem); - setClickFilter(Arrays.asList(data.filter())); + setFilter(Arrays.asList(data.filter())); } - public void onClick(InventoryClickEvent event) { - if (!clickFilter.contains(event.getClick())) { + void onClick(InventoryClickEvent event) { + if (!actionFilter.contains(event.getAction())) { event.setCancelled(true); + event.setResult(Result.DENY); } } - public void setClickFilter(Collection filter) { - this.clickFilter = filter == null || filter.isEmpty() ? EnumSet.allOf(ClickType.class) : EnumSet.copyOf(filter); + /** + * Sets a new {@link ClickType} filter that will only accept clicks with the given type. An empty set is equivalent + * to allowing all click types. + * + * @param filter + * The new filter + */ + public void setFilter(Collection filter) { + this.actionFilter = filter == null || filter.isEmpty() ? EnumSet.allOf(InventoryAction.class) + : EnumSet.copyOf(filter); + } + + /** + * Manually set the {@link ItemStack} for this slot + * + * @param stack + */ + public void setItemStack(ItemStack stack) { + inventory.setItem(index, stack); } } diff --git a/src/main/java/net/citizensnpcs/api/gui/InventoryMenuTransition.java b/src/main/java/net/citizensnpcs/api/gui/InventoryMenuTransition.java index 9c4cbbb5..2aab64e5 100644 --- a/src/main/java/net/citizensnpcs/api/gui/InventoryMenuTransition.java +++ b/src/main/java/net/citizensnpcs/api/gui/InventoryMenuTransition.java @@ -1,18 +1,26 @@ package net.citizensnpcs.api.gui; +/** + * The concrete class of {@link MenuTransition}. Defines a transition from one {@link InventoryMenuPage} to another when + * clicked. + */ public class InventoryMenuTransition { - private final InventoryMenu menu; private final InventoryMenuSlot slot; private final Class transition; - public InventoryMenuTransition(InventoryMenu menu, InventoryMenuSlot slot, - Class transition) { - this.menu = menu; + public InventoryMenuTransition(InventoryMenuSlot slot, Class transition) { this.slot = slot; this.transition = transition; } - public Class accept(InventoryMenuSlot accept) { + Class accept(InventoryMenuSlot accept) { return accept.equals(slot) ? transition : null; } + + /** + * @return The slot holding the transition + */ + public InventoryMenuSlot getSlot() { + return slot; + } } diff --git a/src/main/java/net/citizensnpcs/api/gui/MenuSlot.java b/src/main/java/net/citizensnpcs/api/gui/MenuSlot.java index 6bee982f..7dc3ae2c 100644 --- a/src/main/java/net/citizensnpcs/api/gui/MenuSlot.java +++ b/src/main/java/net/citizensnpcs/api/gui/MenuSlot.java @@ -7,7 +7,7 @@ import java.lang.annotation.Target; import org.bukkit.Material; -import org.bukkit.event.inventory.ClickType; +import org.bukkit.event.inventory.InventoryAction; /** * Defines a slot with a certain item. Can be linked to a {@link InventoryMenuSlot} or simply at the class level. @@ -22,9 +22,14 @@ int amount() default 1; /** - * Whitelist the allowed clicktypes (empty = all allowed). + * Whitelist the allowed actions (empty = all allowed). */ - ClickType[] filter() default {}; + InventoryAction[] filter() default {}; + + /** + * The lore of the inventory item, newline-delimited. + */ + String lore() default ""; /** * The material to display (defaults to AIR). For extra customisation see {@link InventoryMenuSlot}. @@ -39,5 +44,10 @@ /** * The position of the slot within the inventory. */ - int[] value() default { 0, 0 }; + int[] slot() default { 0, 0 }; + + /** + * The display name of the inventory item. + */ + String title() default ""; } diff --git a/src/main/java/net/citizensnpcs/api/trait/trait/Equipment.java b/src/main/java/net/citizensnpcs/api/trait/trait/Equipment.java index 0f5fe61c..3e255758 100644 --- a/src/main/java/net/citizensnpcs/api/trait/trait/Equipment.java +++ b/src/main/java/net/citizensnpcs/api/trait/trait/Equipment.java @@ -172,6 +172,9 @@ public void set(EquipmentSlot slot, ItemStack item) { */ @SuppressWarnings("deprecation") public void set(int slot, ItemStack item) { + if (item != null) { + item = item.clone(); + } equipment[slot] = item; if (slot == 0) { npc.getOrAddTrait(Inventory.class).setItemInHand(item); diff --git a/src/main/java/net/citizensnpcs/api/trait/trait/Inventory.java b/src/main/java/net/citizensnpcs/api/trait/trait/Inventory.java index e26906db..e7a848f5 100644 --- a/src/main/java/net/citizensnpcs/api/trait/trait/Inventory.java +++ b/src/main/java/net/citizensnpcs/api/trait/trait/Inventory.java @@ -222,6 +222,9 @@ public void setContents(ItemStack[] contents) { } public void setItem(int slot, ItemStack item) { + if (item != null) { + item = item.clone(); + } if (view != null && view.getSize() > slot) { view.setItem(slot, item); } else if (contents.length > slot) { @@ -235,6 +238,9 @@ public void setItem(int slot, ItemStack item) { } void setItemInHand(ItemStack item) { + if (item != null) { + item = item.clone(); + } if (view != null && view.getSize() > 0) { view.setItem(0, item); } else if (contents.length > 0) {