[LIB] AmpMenus - Advanced Inventory Menus/GUIs

Discussion in 'Resources' started by ampayne2, Jul 19, 2014.

Thread Status:
Not open for further replies.
  1. Offline

    mahlooo

    Do you have a technique to create paginated menus? I can only think of hard coding submenu buttons to go to the next page, but this wouldn't work when you don't know how many items are going to be in the menu.
     
  2. Offline

    ampayne2

    Interesting idea, I'll come up with some code for you and maybe add it to the lib :)

    In the latest update I removed the Menu interface and added a Size enum for ItemMenus instead of using raw slot amounts. :)

    EDIT by Moderator: merged posts, please use the edit button instead of double posting.
     
    Last edited by a moderator: Jun 9, 2016
  3. Offline

    Ultimate_n00b

    Why is this so big? All of my menus are handled flawlessly with only 2 tiny classes
     
  4. Offline

    ampayne2

    Good question - I'd actually call this a very small lib for what it can do haha :p

    A couple contributing factors to the size are the detailed documentation and premade MenuItems (BackItem, CloseItem, SubMenuItem, StaticMenuItem) included. Aside from that there are actually only a few classes:
    • ItemMenu - Handles mostly everything to do with a menu
    • MenuItem - Represents the items in a menu. Stores basic information such as the base ItemStack and the displayname/lore. Implements ItemClickEventHandler to handle ItemClickEvents called when the MenuItem is clicked.
    • MenuHolder - Custom InventoryHolder to link the ItemMenu and Inventory. Far better than getting the ItemMenu from the clicked Inventory's name as that could cause conflicts between multiple menus with the same name.
    • MenuListener - Listens to bukkit InventoryClickEvents. Checks if the clicked Inventory's holder is a MenuHolder, if so passes the event to the ItemMenu. Has much better performance than for example having a separate listener in each ItemMenu.
    If you take a look at the code you can see that it's extremely user friendly. It's also very good for dynamic menus - menus that appear different for different players or in different situations. The two menus shown in the pictures in the OP are a good example of this - the minigame shop uses a single slot in the inventory to let players increase the tier of certain perks. The attribute allocation menu displays a player's attribute levels and lets the player increase/decrease them. The 'increase attribute' buttons only appear green if the player has enough attribute points to actually increase the level.

    I also partly answer this question in the OP:
    "An object-oriented approach"
    "Intended for advanced use cases"
    "Because the items in these menus behave differently depending on the player and certain data or conditions, they would not be possible with a traditional plugin such as http://dev.bukkit.org/bukkit-plugins/chest-commands/ and would be impractical to create with a simpler utility such as https://forums.bukkit.org/threads/icon-menu.108342/"

    Using this or a similar library leads to much more maintainable code. Your menus may be handled 'flawlessly' but I'm interested in what makes your 2 classes better. I also highly doubt they would be able to cleanly & easily - if at all - replicate the functionality of either of my example menus :)

    Did I mention you only have to create one instance of an ItemMenu and it can be used for any player? I don't know how many times I've seen plugins create a separate menu for every player, or even a new menu every time it gets opened.
     
    thomasb454 and TigerHix like this.
  5. Offline

    Ultimate_n00b

    Well, have a go at what I've done. From what I can see, it does everything yours does.

    PlayerGUI:
    Code:java
    1. /**
    2. * Represents a GUI only for one player.
    3. */
    4. public abstract class PlayerGUI implements Listener {
    5.  
    6. /**
    7.   * The player's uuid.
    8.   */
    9. private UUID uuid;
    10.  
    11. /**
    12.   * The inventory.
    13.   */
    14. private Inventory inventory;
    15.  
    16. /**
    17.   * Instantiates a new player gui.
    18.   *
    19.   * @param player the player
    20.   * @param title the title
    21.   * @param slots the slots
    22.   */
    23. public PlayerGUI(@NonNull Player player, @NonNull String title, int slots) {
    24. if (!player.isOnline()) {
    25. return;
    26. }
    27. this.uuid = player.getUniqueId();
    28. this.inventory = Bukkit.createInventory(null, slots, title);
    29. Bukkit.getPluginManager().registerEvents(this, PaloozaLobby.getPlugin());
    30. }
    31.  
    32. /**
    33.   * Gets the spot.
    34.   *
    35.   * @param row the row
    36.   * @param position the position
    37.   * @return the spot
    38.   */
    39. public static int getSpot(int row, int position) {
    40. return (row * 9) + position - 1;
    41. }
    42.  
    43. public static int getInventorySize(int max) {
    44. if (max <= 9) return 9;
    45. if (max <= 18) return 18;
    46. if (max <= 27) return 27;
    47. if (max <= 36) return 36;
    48. if (max <= 45) return 45;
    49. return 54;
    50. }
    51.  
    52. /**
    53.   * Adds the item.
    54.   *
    55.   * @param items the items
    56.   */
    57. public void addItem(ItemStack... items) {
    58. this.inventory.addItem(items);
    59. }
    60.  
    61. /**
    62.   * Sets the item.
    63.   *
    64.   * @param item the item
    65.   * @param position the position
    66.   */
    67. public void setItem(ItemStack item, int position) {
    68. this.inventory.setItem(position, item);
    69. }
    70.  
    71. /**
    72.   * Sets the item.
    73.   *
    74.   * @param item the item
    75.   * @param position the position
    76.   */
    77. public void setItem(ItemBuilder item, int position) {
    78. this.inventory.setItem(position, item.build());
    79. }
    80.  
    81. /**
    82.   * Gets the inventory.
    83.   *
    84.   * @return the inventory
    85.   */
    86. public Inventory getInventory() {
    87. return this.inventory;
    88. }
    89.  
    90. /**
    91.   * Sets the size.
    92.   *
    93.   * @param size the new size
    94.   */
    95. public void setSize(int size) {
    96. Inventory newInv = Bukkit.createInventory(null, size, this.inventory.getTitle());
    97. newInv.setContents(this.inventory.getContents());
    98. this.inventory = newInv;
    99. }
    100.  
    101. /**
    102.   * On player quit.
    103.   *
    104.   * @param event the event
    105.   */
    106. @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
    107. public void onPlayerQuit(PlayerQuitEvent event) {
    108. if (event.getPlayer().getUniqueId() == this.uuid) {
    109. HandlerList.unregisterAll(this);
    110. }
    111. }
    112.  
    113. /**
    114.   * On inventory click.
    115.   *
    116.   * @param event the event
    117.   */
    118. @EventHandler(priority = EventPriority.LOWEST)
    119. public void onInventoryClick(InventoryClickEvent event) {
    120. if (event.getWhoClicked().getUniqueId().equals(this.uuid) && event.getInventory().equals(this.inventory)) {
    121. if (event.getCurrentItem() != null && this.inventory.contains(event.getCurrentItem())) {
    122. event.setCancelled(true);
    123. this.onClick((Player) event.getWhoClicked(), event.getCurrentItem());
    124. HumanEntity who = event.getWhoClicked();
    125. ((Player) who).playSound(who.getLocation(), Sound.FIRE_IGNITE, 0.3f, 0.3f);
    126. }
    127. }
    128. }
    129.  
    130. /**
    131.   * On inventory close.
    132.   *
    133.   * @param event the event
    134.   */
    135. @EventHandler
    136. public void onInventoryClose(InventoryCloseEvent event) {
    137. if (event.getInventory().equals(this.inventory) && event.getPlayer() instanceof Player) {
    138. if (this.onClose((Player) event.getPlayer())) {
    139. ((Player) event.getPlayer()).playSound(event.getPlayer().getLocation(), Sound.CHEST_CLOSE, 0.3f, 0.3f);
    140. }
    141. }
    142. }
    143.  
    144. /**
    145.   * On click.
    146.   *
    147.   * @param player the player
    148.   * @param item the item
    149.   */
    150. public abstract void onClick(Player player, ItemStack item);
    151.  
    152. public boolean onClose(Player player) {
    153. return true;
    154. }
    155.  
    156. }
    157.  

    GUI
    Code:java
    1.  
    2. /**
    3. * The Class GUI.
    4. */
    5. public abstract class GUI implements Listener {
    6.  
    7. /**
    8.   * Gets the inventory.
    9.   *
    10.   * @return the inventory
    11.   */
    12. @Getter
    13. private final Inventory inventory;
    14.  
    15. /**
    16.   * Instantiates a new gui.
    17.   *
    18.   * @param title the title
    19.   * @param size the size
    20.   */
    21. public GUI(String title, int size) {
    22. this.inventory = Bukkit.createInventory(null, size, title);
    23.  
    24. Bukkit.getPluginManager().registerEvents(this, PaloozaLobby.getPlugin());
    25. }
    26.  
    27. /**
    28.   * Adds the item.
    29.   *
    30.   * @param item the item
    31.   */
    32. public void addItem(ItemBuilder item) {
    33. this.inventory.addItem(item.build());
    34. }
    35.  
    36. /**
    37.   * Sets the item.
    38.   *
    39.   * @param index the index
    40.   * @param item the item
    41.   */
    42. public void setItem(int index, ItemBuilder item) {
    43. this.inventory.setItem(index, item.build());
    44. }
    45.  
    46. /**
    47.   * Gets the spot.
    48.   *
    49.   * @param row the row
    50.   * @param position the position
    51.   * @return the spot
    52.   */
    53. public int getSpot(int row, int position) {
    54. return (row * 9) + position - 1;
    55. }
    56.  
    57. /**
    58.   * On inventory click.
    59.   *
    60.   * @param event the event
    61.   */
    62. @EventHandler
    63. public void onInventoryClick(InventoryClickEvent event) {
    64. if (event.getInventory().equals(this.inventory)) {
    65. if (event.getCurrentItem() != null && this.inventory.contains(event.getCurrentItem()) && event.getWhoClicked() instanceof Player) {
    66. this.onClick((Player) event.getWhoClicked(), event.getCurrentItem());
    67. HumanEntity who = event.getWhoClicked();
    68. ((Player) who).playSound(who.getLocation(), Sound.FIRE_IGNITE, 0.3f, 0.3f);
    69. event.setCancelled(true);
    70. }
    71. }
    72. }
    73.  
    74. /**
    75.   * On inventory close.
    76.   *
    77.   * @param event the event
    78.   */
    79. @EventHandler
    80. public void onInventoryClose(InventoryCloseEvent event) {
    81. if (event.getInventory().equals(this.inventory) && event.getPlayer() instanceof Player) {
    82. if (this.onClose((Player) event.getPlayer())) {
    83. ((Player) event.getPlayer()).playSound(event.getPlayer().getLocation(), Sound.CHEST_CLOSE, 0.3f, 0.3f);
    84. }
    85. }
    86. }
    87.  
    88. /**
    89.   * On click.
    90.   *
    91.   * @param player the player
    92.   * @param item the item
    93.   */
    94. public abstract void onClick(Player player, ItemStack item);
    95.  
    96. /**
    97.   * On close. Needs to be overriden.
    98.   *
    99.   * @param player the player
    100.   * @return true, if you want to play the closing sound.
    101.   */
    102. public boolean onClose(Player player) {
    103. // to be overriden
    104. return true;
    105. }
    106.  
    107. }
    108.  
     
  6. Offline

    ampayne2

    Your GUI classes are sufficient for inventory menus that do not change. However you're missing the entire purpose of AmpMenus - it's intended for advanced uses cases, where the inventory frequently changes.

    This cannot be accomplished with your GUI class. It's possible with your PlayerGUI class, however you are forced to create an entire new PlayerGUI instance for every player. Not to mention you would basically need to create a new PlayerGUI instance or manually modify all the ItemStacks in the Inventory every time it needs to change. However with AmpMenus this becomes extremely simple because dynamic inventories is what it's meant to do.

    Straight from the second line of the OP. This has come in handy for many uses:
    • MenuItems for Minigame Shop Menus.
    • A server teleporter menu. I extend MenuItem and add an extra string in the constructor - the name of the server that the item will teleport you to. Then I add the logic that teleports you to that server when the item is clicked. Then I can create the entire menu in a couple lines.
    • Items in a vote token shop to buy pets. I extend MenuItem and add an extra string in the constructor - the name of the pet. Then I add the logic to check if you can afford the pet, if you haven't already bought it, and to add permission for the pet.
    • Basically any situation in which you will be creating multiple MenuItems that do pretty much the same thing. For custom MenuItems that you're only going to use once, instead of extending MenuItem you could just use an anonymous class and modify it's methods there.

    Your GUI classes fall under this reasoning as well - small and easy to use, but they aren't very flexible and they only support static menus.

    Now, what if you only need a static menu? There are several reasons AmpMenus is still superior.
    In your case you are required to create a new instance of PlayerGUI for every player, wasting resources.​

    Your Inventories have a null InventoryHolder.​

    Your GUI classes each contain their own listener. When an InventoryClickEvent is called every single GUI checks if the Inventory clicked is equal to the GUI's Inventory which could lead to serious performance issues, for example if you have a PlayerGUI instance for every player on a full 100 slot server. However, AmpMenus uses this single MenuListener to control all of the ItemMenus. It does this by checking if the clicked Inventory's holder is a MenuHolder, and if so it gets the ItemMenu instance from the MenuHolder and passes the event to it.​

    • AmpMenus comes with a StaticMenuItem that does not rebuild itself every time the ItemMenu is opened or updated to improve the performance of completely or partially static menus.

    Nobody is forcing you to use AmpMenus - however if you think your GUIs do everything AmpMenus does, I challenge you to do this and post the code. Then I'll post my code and we can see which is cleaner & simpler ;)
     
    TigerHix likes this.
  7. Offline

    Ultimate_n00b

    How are mine not dynamic? Just setItem anytime works. They support both static and dynamic easily.
    I do however like the approach that you and some others use, that detect clicking a specific item. However, how easily do those items change? If I wanted to change "Upgrade Level #1" -> "Upgrade Level #2", are your menus advanced to do that? From what it appears, I couldn't do that.
     
  8. Offline

    ampayne2

    You could do that with the TieredGameItem in the example MenuItems for Minigame Shops link - basically there's a GameItem which extends MenuItem to buy non-upgradeable perks, and then the TieredGameItem adds the upgrade level functionality
     
  9. Offline

    Freack100

    ampayne2
    Sorry for this noob question but I'm new to maven.
    Do you know why my plugin trys to register the Listener while it isn't loaded? I'm sure I create the menu in my onEnable() so it should be loaded.
     
  10. Offline

    ampayne2

    I removed the line in an ItemMenu's constructor that registers the listener automatically for version 1.3 in this commit - should solve the problem, just make sure you register the listener on enable or before players can open any menus.
     
  11. Offline

    TigerHix

    Suggestion: automatically replace &[0-9][a-f] with corresponding color codes. :)
     
  12. Offline

    ampayne2

    In Inventory titles and ItemStack display names/lore?
     
  13. Offline

    TigerHix

    ampayne2 Yep.

    "&4&lSHOP &7- &8&oYou have &6XXX &8tokens"

    will be much better than

    "" + ChatColor.DARK_RED + ChatColor.BOLD + "SHOP " + ChatColor.GRAY + "- " + ChatColor.DARK_GRAY + ChatColor.ITALIC + "You have " + ChatColor.GOLD + "XXX" + ChatColor.DARK_GRAY + "tokens"

    :p
     
  14. Offline

    mahlooo


    It's totally personal preference. I personally like using ChatColor because it's more verbose. You can easily use ChatColor.translateAlternateColorCodes('&',textToTranslate) before putting using the text.
     
  15. Offline

    TigerHix

    mahlooo That means you will need to use a method to handle every string.

    Personal preference doesn't mean it must have no meaning at all. And after all, adding a built-in ChatColor.translateAlternateColorCodes() doesn't affect users, if someone decides to use ChatColor, he can still use it and no problem will be caused.

    By the way, operator "+" cannot be applied to two ChatColors. So, instead of ChatColor.RED + ChatColor.BOLD + "PLAYER", you will need to add an empty string like this, "" + ChatColor.RED + ChatColor.BOLD + "PLAYER", otherwise compile error.
     
  16. Offline

    mahlooo

    TigerHix I'm not for or against the idea of implementing it because I will be using ChatColor regardless, but hard coding ChatColor.translateAlternateColorCodes into every String in the library forces a behavior that isn't required for it to function. If someone only uses default colored text, it would be wasting resources every time.
     
  17. Offline

    TigerHix

    mahlooo First, that case is very rare; second, even though there is someone that likes default color codes, like Hypixel's scoreboards before 2014/8, if handling some strings - replacing & with ยง can "waste resource", then that computer is not suitable for opening a Bukkit server as well. :)
     
  18. Offline

    ampayne2

    TigerHix
    I like the idea and thanks for the suggestion but I'm probably going to side with mahlooo here, because it forces a behavior that isn't required and would slightly slow it down. Also using ChatColor may make lines longer but it's still better for readability because some people don't know instantly what certain codes are.
     
    TigerHix likes this.
  19. Offline

    TigerHix

    ampayne2 No problem, after all you are the author. ;)
     
    ampayne2 likes this.
  20. Offline

    Reddeh

    Just wondering if there's a way to use this without maven? I'm using it for menus, and maven doesn't work for me for whatever reason. Is there a way I can use this with regular craftbukkit? I put the .jar as an external jar in eclipse, but the server won't let me run it.
     
  21. Offline

    ampayne2

    Are you sure you're not running into issues because of craftbukkit being taken down? All the AmpMenus builds on Jenkins and the maven repo were built before then so it should work fine. What error are you getting in maven/the server console?
     
  22. Offline

    Reddeh

    Oh no, maven just doesn't work.
    For now I included it in the .jar since no other mods use it, but if I wanted to use it in a separate plugin what then?
     
  23. Offline

    ampayne2

    It looks like the maven repo is down, I'll let the owner of it know and it should be back up soon :)
    Btw, including it in the .jar manually is fine as that's basically what maven does automatically for you.
     
  24. Offline

    Reddeh

    Alright, thanks! c:
     
  25. Offline

    thomasb454

    ampayne2 It would be nice if you could detect right click/shift clicks
    Thoughts?
     
  26. Offline

    Lzp_Hiro

    ampayne2 Hello, i have an issue with this lib :d I have :
    My Menu class :
    Code:java
    1. public class ShopMenu extends ItemMenu {
    2.  
    3. public ShopMenu(JavaPlugin plugin, Size size) {
    4. super("MENU", size.FOUR_LINE, main.instance);
    5.  
    6. // Adding items to your ItemMenu
    7. setItem(17, new TestItem());
    8. }
    9.  
    10. @Override
    11. public void open(Player player) {
    12. super.open(player);
    13. }
    14. }

    My TestItem class:
    Code:java
    1. public class TestItem extends MenuItem {
    2. private static final String DISPLAY_NAME = ChatColor.BLUE + "Click me!";
    3. private static final ItemStack ICON = new ItemStack(Material.COAL_BLOCK);
    4.  
    5. public TestItem() {
    6. super(DISPLAY_NAME, ICON);
    7. }
    8.  
    9.  
    10. @Override
    11. public void onItemClick(ItemClickEvent event) {
    12. event.getPlayer().sendMessage("It works!");
    13. }
    14.  
    15. @Override
    16. public ItemStack getFinalIcon(Player player) {
    17. ItemStack finalIcon = super.getFinalIcon(player);
    18. if (player.isFlying()) {
    19. finalIcon.setType(Material.LEASH);
    20. ItemMeta meta = finalIcon.getItemMeta();
    21. meta.setDisplayName(ChatColor.DARK_RED + "It works too!");
    22. finalIcon.setItemMeta(meta);
    23. }
    24. return finalIcon;
    25. }
    26. }


    And my main class:
    Code:java
    1. public class main extends JavaPlugin {
    2.  
    3. static main instance;
    4.  
    5. @Override
    6. public void onEnable() {
    7. instance = this;
    8. getServer().getPluginManager().registerEvents(new listen(), this);
    9. }
    10. }
    11.  


    Listener class:
    Code:java
    1. public class listen implements Listener {
    2.  
    3. ShopMenu menu;
    4.  
    5. @EventHandler
    6. public void e(PlayerEggThrowEvent e) {
    7. menu.open(e.getPlayer());
    8. }
    9. }
    10.  

    Every time I throw an egg, i have a null pointer exception at line 13 of listen, wich is the open method.

    Thanks, Lzp_Hiro
     
  27. Offline

    mrCookieSlime

    Lzp_Hiro
    You never set the Variable menu to something.
    menu = new ShopMenu();
     
  28. Offline

    thomasb454


    I've done this.
     
  29. Offline

    Lzp_Hiro

    Thank you so much! I'm feeling really stupid right now ^^
     
  30. Offline

    moose517

    just started using this for some GUI's in my plugin but i'm either overthinking something or i'm just not understanding. I've got a MainMenu class that has a menuItem that i want to open a submenu called ManagementMenu, i'm not understanding how to make that menuItem in the MainMenu open the ManagementMenu.
     
Thread Status:
Not open for further replies.

Share This Page