├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src └── main └── java └── me └── tom └── sparse └── spigot └── chat ├── menu ├── CMCommand.java ├── CMListener.java ├── CMPlugin.java ├── ChatMenu.java ├── ChatMenuAPI.java ├── IElementContainer.java └── element │ ├── BooleanElement.java │ ├── ButtonElement.java │ ├── Element.java │ ├── GroupElement.java │ ├── HorizontalRuleElement.java │ ├── ImageElement.java │ ├── IncrementalElement.java │ ├── InputElement.java │ ├── LinkButtonElement.java │ ├── NumberSliderElement.java │ ├── TextElement.java │ └── VerticalSelectorElement.java ├── protocol ├── AbstractPacket.java ├── PlayerChatInterceptor.java └── WrapperPlayServerChat.java └── util ├── NumberFormat.java ├── State.java ├── Text.java └── TextUtil.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Maven 2 | target/ 3 | # IDEA 4 | *.iml 5 | .idea/ 6 | 7 | # Java 8 | *.jar 9 | 10 | # ignore ci stuff 11 | /deploy-stuff 12 | .gradletasknamecache 13 | 14 | # these are generated and are unneccesarry 15 | package-info.java 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ChatMenuAPI 2 | An API for making menus inside Minecraft's chat. 3 | This API treats Minecraft's chat like a 2D grid, allowing you to position elements freely in chat. 4 | 5 | ### Fork 6 | 7 | This is a fork for VGL to use gradle, vgls repo and protocollib. 8 | 9 | ## Preview 10 | ![](https://sparse.blue/files/k0ejrc.gif) 11 | 12 | --- 13 | 14 | ## Contents 15 | * [ChatMenuAPI](#chatmenuapi) 16 | - [Preview](#preview) 17 | - [Contents](#contents) 18 | - [Usage](#usage) 19 | + [Setup](#setup) 20 | + [ChatMenu](#chatmenu) 21 | + [Element](#element) 22 | + [States](#states) 23 | + [Displaying](#displaying) 24 | - [Links](#links) 25 | 26 | --- 27 | 28 | ## Usage 29 | 30 | ### Setup 31 | Add `ChatMenuAPI.jar` to your build path, then add it as a dependency in your `plugin.yml`: 32 | ```YAML 33 | depend: [ChatMenuAPI] 34 | ``` 35 | ### ChatMenu 36 | To create a menu, just create a new instance of `ChatMenu`: 37 | ```Java 38 | ChatMenu menu = new ChatMenu(); 39 | ``` 40 | If you are not using this API just for chat formatting, it is recommended that you make the menu a pausing menu: 41 | ```Java 42 | ChatMenu menu = new ChatMenu().pauseChat(); 43 | ``` 44 | When this menu is sent to a player, it will automatically pause outgoing chat to that player so that the menu will not be interrupted. 45 | 46 | **Warning:** If you make a menu pause chat, you need to add a way to close the menu! 47 | 48 | ### Element 49 | Elements are the building blocks of menus. They are used to represent everything in a menu. 50 | There are a few elements provided by default, you can view them by [clicking here](../master/src/me/tom/sparse/spigot/chat/menu/element). 51 | 52 | Basic `TextElement`: 53 | ```Java 54 | menu.add(new TextElement("Hello, world!", 10, 10)); 55 | ``` 56 | 57 | Basic close button: 58 | ```Java 59 | menu.add(new ButtonElement(x, y, ChatColor.RED+"[Close]", (p) -> {menu.close(p); return false;})); 60 | ``` 61 | 62 | Instead of manually creating a close button, you can also just pass the arguments you would use for a close button directly into the `pauseChat` method. 63 | ```Java 64 | ChatMenu menu = new ChatMenu().pauseChat(x, y, ChatColor.RED+"[Close]"); 65 | ``` 66 | 67 | All of the default elements require and X and Y in their constructor, 68 | these coordinates should be greater than or equal to 0 and less than 320 on the X axis and 20 on the Y axis. 69 | The default Minecraft chat is 320 pixels wide and 20 lines tall. 70 | 71 | ### States 72 | Most interactive elements have one or more `State` objects. 73 | 74 | `State`s are used to store information about an `Element`, such as the current number in an `IncrementalElement`. 75 | 76 | Every state can have a change callback to detect when it changes: 77 | ```Java 78 | IncrementalElement incr = ...; 79 | incr.value.setChangeCallback((s) -> { 80 | System.out.println("IncrementalElement changed! "+s.getPrevious()+" -> "+s.getCurrent()); 81 | }); 82 | ``` 83 | 84 | ### Displaying 85 | Once you've created your menu and added all the elements you want, now would probably be a good time to display it. 86 | You can display a menu using `ChatMenu#openFor(Player player)`: 87 | ```Java 88 | Player p = ...; 89 | menu.openFor(p); 90 | ``` 91 | 92 | ## Links 93 | * [Download](https://www.spigotmc.org/resources/chatmenuapi.45144/) 94 | * [JavaDoc](https://sparse.blue/docs/ChatMenuAPI/index.html) -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.voxelgameslib 8 | voxelgameslib-parent 9 | 1.0.0-SNAPSHOT 10 | ../../pom.xml 11 | 12 | 13 | chatmenuapi 14 | 1.0.0-SNAPSHOT 15 | 16 | 17 | 18 | 19 | maven-resources-plugin 20 | 21 | 22 | copy-to-testserver 23 | none 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | com.destroystokyo.paper 34 | paper-api 35 | 1.12.2-R0.1-SNAPSHOT 36 | provided 37 | 38 | 39 | com.comphenix.protocol 40 | ProtocolLib 41 | 4.4.0-SNAPSHOT 42 | provided 43 | 44 | 45 | org.jetbrains 46 | annotations 47 | 17.0.0 48 | provided 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/main/java/me/tom/sparse/spigot/chat/menu/CMCommand.java: -------------------------------------------------------------------------------- 1 | package me.tom.sparse.spigot.chat.menu; 2 | 3 | import org.bukkit.command.Command; 4 | import org.bukkit.command.CommandExecutor; 5 | import org.bukkit.command.CommandSender; 6 | import org.bukkit.entity.Player; 7 | 8 | public class CMCommand implements CommandExecutor { 9 | CMCommand() { 10 | } 11 | 12 | public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { 13 | if (!(sender instanceof Player)) 14 | return true; 15 | 16 | if (args.length > 0) { 17 | String id = args[0]; 18 | if (id.equalsIgnoreCase("close")) { 19 | ChatMenuAPI.setCurrentMenu((Player) sender, null); 20 | return true; 21 | } else if (args.length > 1) { 22 | int element; 23 | try { 24 | element = Integer.parseInt(args[1]); 25 | } catch (NumberFormatException e) { 26 | return true; 27 | } 28 | String[] elementArgs = new String[args.length - 2]; 29 | System.arraycopy(args, 2, elementArgs, 0, elementArgs.length); 30 | ChatMenu menu = ChatMenuAPI.getMenu(id); 31 | if (menu == null || element < 0 || element >= menu.elements.size()) 32 | return true; 33 | menu.edit((Player) sender, element, elementArgs); 34 | } 35 | } 36 | return true; 37 | } 38 | 39 | // private static Filter originalFilter; 40 | // private static PrintStream originalOut; 41 | 42 | static void setLoggerFilter() { 43 | // System.setOut(new PrintStream(new FilterOutputStream(originalOut = System.out) { 44 | // public void write(byte[] b) throws IOException 45 | // { 46 | // if(new String(b).contains("/cmapi")) 47 | // return; 48 | // super.write(b); 49 | // } 50 | // })); 51 | 52 | // Logger logger = Bukkit.getLogger(); 53 | // originalFilter = logger.getFilter(); 54 | // logger.setFilter(record -> !record.getMessage().contains("/cmapi")); 55 | // logger.setFilter(record -> false); 56 | } 57 | 58 | static void restoreLoggerFilter() { 59 | // System.setOut(originalOut); 60 | // Bukkit.getLogger().setFilter(originalFilter); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/me/tom/sparse/spigot/chat/menu/CMListener.java: -------------------------------------------------------------------------------- 1 | package me.tom.sparse.spigot.chat.menu; 2 | 3 | import java.util.Map; 4 | import java.util.concurrent.ConcurrentHashMap; 5 | import java.util.function.BiFunction; 6 | 7 | import org.bukkit.Bukkit; 8 | import org.bukkit.entity.Player; 9 | import org.bukkit.event.EventHandler; 10 | import org.bukkit.event.EventPriority; 11 | import org.bukkit.event.Listener; 12 | import org.bukkit.event.player.AsyncPlayerChatEvent; 13 | import org.bukkit.event.player.PlayerCommandPreprocessEvent; 14 | import org.bukkit.event.player.PlayerQuitEvent; 15 | import org.bukkit.plugin.Plugin; 16 | 17 | public class CMListener implements Listener { 18 | private static Map> chatListeners = new ConcurrentHashMap<>(); 19 | 20 | public static void cancelExpectation(Player player) { 21 | chatListeners.remove(player); 22 | } 23 | 24 | public static void expectPlayerChat(Player player, BiFunction function) { 25 | if (player == null || !player.isOnline()) 26 | throw new IllegalArgumentException("Cannot wait for chat for a null/offline player."); 27 | if (function == null) 28 | throw new IllegalArgumentException("Cannot call null function."); 29 | 30 | chatListeners.put(player, function); 31 | } 32 | 33 | private CMCommand command; 34 | 35 | CMListener(Plugin plugin) { 36 | command = new CMCommand(); 37 | Bukkit.getPluginManager().registerEvents(this, plugin); 38 | } 39 | 40 | @EventHandler 41 | public void onPlayerChat(AsyncPlayerChatEvent e) { 42 | Player player = e.getPlayer(); 43 | BiFunction listener = chatListeners.get(player); 44 | if (listener != null) { 45 | e.setCancelled(true); 46 | if (listener.apply(player, e.getMessage())) 47 | chatListeners.remove(player); 48 | } 49 | } 50 | 51 | @EventHandler 52 | public void onPlayerQuit(PlayerQuitEvent e) { 53 | cancelExpectation(e.getPlayer()); 54 | } 55 | 56 | @EventHandler(priority = EventPriority.LOWEST) 57 | public void onCommandPreprocess(PlayerCommandPreprocessEvent e) { 58 | String cmd = e.getMessage().substring(1); 59 | if (cmd.length() <= 0) return; 60 | String[] unprocessedArgs = cmd.split(" "); 61 | 62 | String label = unprocessedArgs[0]; 63 | String[] args = new String[unprocessedArgs.length - 1]; 64 | System.arraycopy(unprocessedArgs, 1, args, 0, args.length); 65 | 66 | if (label.equalsIgnoreCase("cmapi")) { 67 | e.setCancelled(true); 68 | command.onCommand(e.getPlayer(), null, label, args); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/me/tom/sparse/spigot/chat/menu/CMPlugin.java: -------------------------------------------------------------------------------- 1 | package me.tom.sparse.spigot.chat.menu; 2 | 3 | import org.bukkit.plugin.java.JavaPlugin; 4 | 5 | public class CMPlugin extends JavaPlugin { 6 | public void onEnable() { 7 | ChatMenuAPI.init(this); 8 | } 9 | 10 | public void onDisable() { 11 | ChatMenuAPI.disable(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/me/tom/sparse/spigot/chat/menu/ChatMenu.java: -------------------------------------------------------------------------------- 1 | package me.tom.sparse.spigot.chat.menu; 2 | 3 | import net.md_5.bungee.api.chat.BaseComponent; 4 | 5 | import org.jetbrains.annotations.NotNull; 6 | import org.jetbrains.annotations.Nullable; 7 | 8 | import java.util.ArrayList; 9 | import java.util.Arrays; 10 | import java.util.Collection; 11 | import java.util.Collections; 12 | import java.util.Comparator; 13 | import java.util.List; 14 | import java.util.Objects; 15 | import java.util.Set; 16 | import java.util.concurrent.ConcurrentHashMap; 17 | 18 | import org.bukkit.entity.Player; 19 | 20 | import me.tom.sparse.spigot.chat.menu.element.ButtonElement; 21 | import me.tom.sparse.spigot.chat.menu.element.Element; 22 | import me.tom.sparse.spigot.chat.protocol.PlayerChatInterceptor; 23 | import me.tom.sparse.spigot.chat.util.Text; 24 | 25 | public class ChatMenu implements IElementContainer { 26 | @NotNull 27 | protected final String id; 28 | protected boolean registered; 29 | 30 | @NotNull 31 | protected List elements; 32 | 33 | @NotNull 34 | protected Set viewers = ConcurrentHashMap.newKeySet(); 35 | protected boolean pauseChat = false; 36 | 37 | protected boolean autoUnregister = true; 38 | 39 | /** 40 | * Constructs a chat menu with the elements provided. 41 | * 42 | * @param elements the elements to enable the menu with. 43 | */ 44 | public ChatMenu(@NotNull Element... elements) { 45 | this(Arrays.asList(elements)); 46 | } 47 | 48 | /** 49 | * Constructs a chat menu with the elements provided. 50 | * 51 | * @param elements the elements to enable the menu with. 52 | */ 53 | public ChatMenu(@NotNull Collection elements) { 54 | this.elements = new ArrayList<>(); 55 | this.elements.addAll(elements); 56 | this.id = ChatMenuAPI.registerMenu(this); 57 | registered = true; 58 | } 59 | 60 | /** 61 | * Unregisters this menu, all the elements, and closes the menu for all viewers. 62 | */ 63 | public void destroy() { 64 | unregister(); 65 | elements.clear(); 66 | viewers.forEach(this::close); 67 | } 68 | 69 | /** 70 | * Unregister this menu. 71 | *
72 | * An unregistered menu cannot be interacted with by a player. If you attempt to build this menu with elements that 73 | * don't support unregistered menus, you will get an {@link IllegalStateException}. 74 | *
75 | * Be sure to unregister all menus once you're done with them! 76 | * 77 | * @throws IllegalStateException if this menu is not registered. 78 | */ 79 | public void unregister() { 80 | if (!registered) 81 | throw new IllegalStateException("Menu not registered"); 82 | ChatMenuAPI.unregisterMenu(this); 83 | registered = false; 84 | } 85 | 86 | /** 87 | * @param autoUnregister true if this menu should automatically be unregistered after all players close it. 88 | */ 89 | public void setAutoUnregister(boolean autoUnregister) { 90 | this.autoUnregister = autoUnregister; 91 | } 92 | 93 | /** 94 | * Adds the provided element to this menu. 95 | * 96 | * @param element the element to add to this menu 97 | * @throws IllegalArgumentException if the element is null 98 | */ 99 | @Deprecated 100 | public void addElement(@NotNull Element element) { 101 | add(element); 102 | } 103 | 104 | /** 105 | * Adds the provided element to this menu. 106 | * 107 | * @param t the element to add to this menu 108 | * @param the type of element 109 | * @return the element added 110 | */ 111 | public T add(@NotNull T t) { 112 | Objects.requireNonNull(t); 113 | elements.add(t); 114 | elements.sort(Comparator.comparingInt(Element::getX)); 115 | return t; 116 | } 117 | 118 | /** 119 | * Removes the specified element from this menu. 120 | * 121 | * @param element the element to remove 122 | * @return true if the element was removed 123 | */ 124 | public boolean remove(@NotNull Element element) { 125 | return elements.remove(element); 126 | } 127 | 128 | /** 129 | * @return an unmodifiable list of all the elements in this menu. 130 | */ 131 | @NotNull 132 | public List getElements() { 133 | return Collections.unmodifiableList(elements); 134 | } 135 | 136 | /** 137 | * Called when a player edits something in the menu. 138 | * 139 | * @param player the player that edited something 140 | * @param elementIndex the index of the element that was edited 141 | * @param args the data to be parsed by the element 142 | */ 143 | public void edit(@NotNull Player player, int elementIndex, @NotNull String[] args) { 144 | if (elementIndex < 0 || elementIndex >= elements.size()) 145 | return; 146 | 147 | Element element = elements.get(elementIndex); 148 | element.edit(this, args); 149 | if (element.onClick(this, player)) 150 | refresh(); 151 | } 152 | 153 | /** 154 | * Builds and sends this menu to the provided player. 155 | * 156 | * @param player the player to send the menu to. 157 | */ 158 | public void openFor(@NotNull Player player) { 159 | PlayerChatInterceptor chat = ChatMenuAPI.getChatIntercept(); 160 | if (viewers.add(player) && pauseChat) { 161 | chat.pause(player); 162 | } 163 | for (BaseComponent[] line : build()) { 164 | chat.sendMessage(player, line); 165 | } 166 | ChatMenuAPI.setCurrentMenu(player, this); 167 | } 168 | 169 | /** 170 | * Sends this menu again to all of the players currently viewing it 171 | */ 172 | public void refresh() { 173 | viewers.removeIf(it -> !it.isOnline()); 174 | for (Player viewer : viewers) 175 | openFor(viewer); 176 | } 177 | 178 | @NotNull 179 | public List build() { 180 | Element overlapping = findOverlap(); 181 | if (overlapping != null) { 182 | // System.err.println("WARNING! Potential overlap detected."); 183 | throw new IllegalStateException("Overlapping element(s)! " + overlapping); 184 | } 185 | 186 | List lines = new ArrayList<>(20); 187 | for (int i = 0; i < 20; i++) 188 | lines.add(new Text()); 189 | 190 | for (Element element : elements) { 191 | if (!element.isVisible()) 192 | continue; 193 | 194 | List elementTexts = element.render(this); 195 | for (int j = 0; j < elementTexts.size(); j++) { 196 | int lineY = element.getY() + j; 197 | 198 | if (lineY < 0 || lineY >= 20) 199 | continue; 200 | 201 | Text text = lines.get(lineY); 202 | text.expandToWidth(element.getX()); 203 | 204 | Text toAdd = elementTexts.get(j); 205 | toAdd.expandToWidth(element.getWidth()); 206 | text.append(toAdd); 207 | } 208 | } 209 | 210 | //TODO: Compress to as little packets as possible. 211 | 212 | List result = new ArrayList<>(); 213 | for (Text text : lines) { 214 | if (text.toLegacyText().contains("\n")) 215 | throw new IllegalStateException("Menu contains line with newline character"); 216 | else if (text.getWidth() > 320) 217 | throw new IllegalStateException("Menu contains line exceeds chat width"); 218 | 219 | 220 | List components = text.getComponents(); 221 | result.add(components.toArray(new BaseComponent[components.size()])); 222 | } 223 | return result; 224 | } 225 | 226 | /** 227 | * @return the first element found that overlaps 228 | */ 229 | @Nullable 230 | public Element findOverlap() { 231 | return elements.stream().filter(Element::isVisible).filter(a -> elements.stream().filter(Element::isVisible).anyMatch(b -> a != b && a.overlaps(b))).findFirst().orElse(null); 232 | } 233 | 234 | /** 235 | * Sets the currently opened menu of the provided player to null. 236 | * 237 | * @param player the player that closed the menu 238 | */ 239 | public void close(@NotNull Player player) { 240 | if (viewers.remove(player)) { 241 | ChatMenuAPI.setCurrentMenu(player, null); 242 | ChatMenuAPI.getChatIntercept().resume(player); 243 | } 244 | if (viewers.size() == 0 && autoUnregister) { 245 | unregister(); 246 | } 247 | } 248 | 249 | void onClosed(@NotNull Player player) { 250 | if (viewers.remove(player)) { 251 | ChatMenuAPI.getChatIntercept().resume(player); 252 | } 253 | } 254 | 255 | /** 256 | * @return the command used to interact with this menu 257 | */ 258 | @NotNull 259 | public String getCommand() { 260 | if (!isRegistered()) 261 | throw new IllegalStateException("Unregistered menus can't be interacted with."); 262 | return "/cmapi " + id + " "; 263 | } 264 | 265 | /** 266 | * @param element the element to interact with 267 | * @return the command used to interact with the provided element 268 | */ 269 | @NotNull 270 | public String getCommand(@NotNull Element element) { 271 | return getCommand() + elements.indexOf(element) + " "; 272 | } 273 | 274 | /** 275 | * @return true if this menu is registered 276 | */ 277 | public boolean isRegistered() { 278 | return registered; 279 | } 280 | 281 | /** 282 | * @return true if this menu will pause chat when it is opened 283 | */ 284 | public boolean doesPauseChat() { 285 | return pauseChat; 286 | } 287 | 288 | /** 289 | * Makes this menu pause chat when it is opened 290 | * 291 | * @return this 292 | */ 293 | @NotNull 294 | public ChatMenu pauseChat() { 295 | setPauseChat(true); 296 | return this; 297 | } 298 | 299 | /** 300 | * Makes this menu pause chat when it is opened and adds a close button. 301 | * 302 | * @param x the x coordinate of the close button 303 | * @param y the y coordinate of the close button 304 | * @param text the text of the close button 305 | * @return this 306 | */ 307 | @NotNull 308 | public ChatMenu pauseChat(int x, int y, @NotNull String text) { 309 | setPauseChat(true); 310 | add(ButtonElement.createCloseButton(x, y, text, this)); 311 | return this; 312 | } 313 | 314 | /** 315 | * @param pauseChat true if this menu should pause chat when it is opened 316 | */ 317 | public void setPauseChat(boolean pauseChat) { 318 | this.pauseChat = pauseChat; 319 | } 320 | 321 | public int hashCode() { 322 | return id.hashCode(); 323 | } 324 | 325 | public boolean equals(Object o) { 326 | if (this == o) return true; 327 | if (!(o instanceof ChatMenu)) return false; 328 | 329 | ChatMenu chatMenu = (ChatMenu) o; 330 | 331 | return id.equals(chatMenu.id); 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /src/main/java/me/tom/sparse/spigot/chat/menu/ChatMenuAPI.java: -------------------------------------------------------------------------------- 1 | package me.tom.sparse.spigot.chat.menu; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | import java.util.Map; 7 | import java.util.concurrent.ConcurrentHashMap; 8 | import java.util.concurrent.ThreadLocalRandom; 9 | 10 | import org.bukkit.ChatColor; 11 | import org.bukkit.entity.Player; 12 | import org.bukkit.map.MapFont; 13 | import org.bukkit.map.MinecraftFont; 14 | import org.bukkit.plugin.Plugin; 15 | 16 | import me.tom.sparse.spigot.chat.protocol.PlayerChatInterceptor; 17 | 18 | public final class ChatMenuAPI { 19 | private static final Map MENUS = new ConcurrentHashMap<>(); 20 | private static final Map OPENED_MENUS = new ConcurrentHashMap<>(); 21 | 22 | private static Plugin plugin; 23 | private static PlayerChatInterceptor interceptor; 24 | 25 | private ChatMenuAPI() { 26 | } 27 | 28 | /** 29 | * @param player the player whose current menu should be returned 30 | * @return the menu the player currently has open, or {@code null} if no menu is open. 31 | */ 32 | @Nullable 33 | public static ChatMenu getCurrentMenu(@NotNull Player player) { 34 | return OPENED_MENUS.get(player); 35 | } 36 | 37 | /** 38 | * @param player the player whose current menu should be returned 39 | * @param menu the menu to set as current, or {@code null} if you want to close the current menu. 40 | */ 41 | public static void setCurrentMenu(@NotNull Player player, @Nullable ChatMenu menu) { 42 | ChatMenu old = OPENED_MENUS.remove(player); 43 | if (old != null && old != menu) old.onClosed(player); 44 | if (menu != null) OPENED_MENUS.put(player, menu); 45 | } 46 | 47 | @NotNull 48 | static String registerMenu(ChatMenu menu) { 49 | String id = generateIdentifier(); 50 | MENUS.put(id, menu); 51 | return id; 52 | } 53 | 54 | static void unregisterMenu(@NotNull ChatMenu menu) { 55 | MENUS.values().remove(menu); 56 | } 57 | 58 | @NotNull 59 | private static String generateIdentifier() { 60 | String result = null; 61 | while (result == null || MENUS.containsKey(result)) { 62 | int id = ThreadLocalRandom.current().nextInt(0, 9999); 63 | result = id + ""; 64 | } 65 | 66 | return result; 67 | } 68 | 69 | /** 70 | * Gets the current {@link PlayerChatInterceptor} 71 | * 72 | * @return the {@link PlayerChatInterceptor} 73 | */ 74 | @NotNull 75 | public static PlayerChatInterceptor getChatIntercept() { 76 | return interceptor; 77 | } 78 | 79 | /** 80 | * Calculates the width of the provided text. 81 | *
82 | * Works with formatting codes such as bold. 83 | * 84 | * @param text the text to calculate the width for 85 | * @return the number of pixels in chat the text takes up 86 | */ 87 | public static int getWidth(@NotNull String text) { 88 | if (text.contains("\n")) 89 | throw new IllegalArgumentException("Cannot get width of text containing newline"); 90 | 91 | int width = 0; 92 | 93 | boolean isBold = false; 94 | 95 | char[] chars = text.toCharArray(); 96 | for (int i = 0; i < chars.length; i++) { 97 | char c = chars[i]; 98 | int charWidth = getCharacterWidth(c); 99 | 100 | if (c == ChatColor.COLOR_CHAR && i < chars.length - 1) { 101 | c = chars[++i]; 102 | 103 | if (c != 'l' && c != 'L') { 104 | if (c == 'r' || c == 'R') { 105 | isBold = false; 106 | } 107 | } else { 108 | isBold = true; 109 | } 110 | 111 | charWidth = 0; 112 | } 113 | 114 | if (isBold && c != ' ' && charWidth > 0) { 115 | width++; 116 | } 117 | 118 | width += charWidth; 119 | } 120 | 121 | return width; 122 | } 123 | 124 | /** 125 | * @param c the character to get the width of 126 | * @return the width of the provided character in pixels 127 | */ 128 | public static int getCharacterWidth(char c) { 129 | if (c >= '\u2588' && c <= '\u258F') { 130 | return ('\u258F' - c) + 2; 131 | } 132 | 133 | switch (c) { 134 | case ' ': 135 | return 4; 136 | case '\u2714': 137 | return 8; 138 | case '\u2718': 139 | return 7; 140 | default: 141 | MapFont.CharacterSprite mcChar = MinecraftFont.Font.getChar(c); 142 | if (mcChar != null) 143 | return mcChar.getWidth() + 1; 144 | return 0; 145 | } 146 | } 147 | 148 | static ChatMenu getMenu(String id) { 149 | return MENUS.get(id); 150 | } 151 | 152 | /** 153 | * This method should only be called by you if you're including this API inside your plugin. 154 | *
155 | * Initializes all the necessary things for the ChatMenuAPI to function. This method can only be called once. 156 | * 157 | * @param plugin the plugin to initialize everything with, including listeners and scheduled tasks 158 | */ 159 | public static void init(@NotNull Plugin plugin) { 160 | if (ChatMenuAPI.plugin != null) 161 | return; 162 | 163 | ChatMenuAPI.plugin = plugin; 164 | // Bukkit.getPluginCommand("cmapi").setExecutor(new CMCommand()); 165 | CMCommand.setLoggerFilter(); 166 | new CMListener(plugin); 167 | 168 | interceptor = new PlayerChatInterceptor(plugin); 169 | } 170 | 171 | /** 172 | * This method should only be called by you if you're including this API inside your plugin. 173 | *
174 | * Disables everything necessary for this API to be reloaded properly without restarting. 175 | */ 176 | public static void disable() { 177 | if (plugin == null) 178 | return; 179 | 180 | CMCommand.restoreLoggerFilter(); 181 | plugin = null; 182 | interceptor.disable(); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/main/java/me/tom/sparse/spigot/chat/menu/IElementContainer.java: -------------------------------------------------------------------------------- 1 | package me.tom.sparse.spigot.chat.menu; 2 | 3 | import java.util.List; 4 | 5 | import org.bukkit.entity.Player; 6 | 7 | import me.tom.sparse.spigot.chat.menu.element.Element; 8 | 9 | public interface IElementContainer { 10 | /** 11 | * Add the specified element to this container 12 | * 13 | * @param element the element to add 14 | * @param the type of element being added 15 | * @return the element that was added 16 | */ 17 | T add(T element); 18 | 19 | /** 20 | * Remove the specified element from this container 21 | * 22 | * @param element the element to remove 23 | * @return true if the element was removed 24 | */ 25 | boolean remove(Element element); 26 | 27 | /** 28 | * @return an unmodifiable list of all the elements in this container 29 | */ 30 | List getElements(); 31 | 32 | /** 33 | * @param element the element to interact with 34 | * @return the command used to interact with the provided element 35 | */ 36 | String getCommand(Element element); 37 | 38 | /** 39 | * Display this container to the specified player 40 | * 41 | * @param player the player to open this container for 42 | */ 43 | void openFor(Player player); 44 | 45 | /** 46 | * Displays this container again to all of the players currently viewing it 47 | */ 48 | void refresh(); 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/me/tom/sparse/spigot/chat/menu/element/BooleanElement.java: -------------------------------------------------------------------------------- 1 | package me.tom.sparse.spigot.chat.menu.element; 2 | 3 | import net.md_5.bungee.api.ChatColor; 4 | import net.md_5.bungee.api.chat.BaseComponent; 5 | import net.md_5.bungee.api.chat.ClickEvent; 6 | import net.md_5.bungee.api.chat.TextComponent; 7 | 8 | import org.jetbrains.annotations.NotNull; 9 | import org.jetbrains.annotations.Nullable; 10 | 11 | import java.util.ArrayList; 12 | import java.util.Collections; 13 | import java.util.List; 14 | 15 | import me.tom.sparse.spigot.chat.menu.ChatMenuAPI; 16 | import me.tom.sparse.spigot.chat.menu.IElementContainer; 17 | import me.tom.sparse.spigot.chat.util.State; 18 | import me.tom.sparse.spigot.chat.util.Text; 19 | 20 | /** 21 | * A boolean element. Basically a checkbox (without the box). 22 | */ 23 | public class BooleanElement extends Element { 24 | @NotNull 25 | public final State value; 26 | 27 | @NotNull 28 | protected ChatColor trueColor = ChatColor.GREEN; 29 | @NotNull 30 | protected ChatColor falseColor = ChatColor.RED; 31 | 32 | protected boolean showText = false; 33 | 34 | /** 35 | * Constructs a {@code BooleanElement} 36 | * 37 | * @param x the x coordinate 38 | * @param y the y coordinate 39 | * @param value the starting value 40 | */ 41 | public BooleanElement(int x, int y, boolean value) { 42 | super(x, y); 43 | this.value = new State<>(value); 44 | } 45 | 46 | /** 47 | * Shows the "true/false" text after the symbol. 48 | * 49 | * @return this 50 | */ 51 | @NotNull 52 | public BooleanElement showText() { 53 | setShowText(true); 54 | return this; 55 | } 56 | 57 | /** 58 | * @param showText whether or not to show the "true/false" text 59 | */ 60 | public void setShowText(boolean showText) { 61 | this.showText = showText; 62 | } 63 | 64 | /** 65 | * @param trueColor The color the symbol should be if the value is {@code true} 66 | * @param falseColor The color the symbol should be if the value is {@code false} 67 | * @return this 68 | */ 69 | @NotNull 70 | public BooleanElement colors(@NotNull ChatColor trueColor, @NotNull ChatColor falseColor) { 71 | setTrueColor(trueColor); 72 | setFalseColor(falseColor); 73 | return this; 74 | } 75 | 76 | /** 77 | * @return the color the text will be if the value is {@code false} 78 | */ 79 | @NotNull 80 | public ChatColor getFalseColor() { 81 | return falseColor; 82 | } 83 | 84 | /** 85 | * @param falseColor the color the symbol should be if the value is {@code false} 86 | */ 87 | public void setFalseColor(@Nullable ChatColor falseColor) { 88 | this.falseColor = falseColor == null ? ChatColor.RED : falseColor; 89 | } 90 | 91 | /** 92 | * @return the color the text will be if the value is {@code true} 93 | */ 94 | @NotNull 95 | public ChatColor getTrueColor() { 96 | return trueColor; 97 | } 98 | 99 | /** 100 | * @param trueColor The color the symbol should be if the value is {@code true} 101 | */ 102 | public void setTrueColor(@Nullable ChatColor trueColor) { 103 | this.trueColor = trueColor == null ? ChatColor.GREEN : trueColor; 104 | } 105 | 106 | public int getWidth() { 107 | return 8 + (showText ? ChatMenuAPI.getWidth(" " + value.getCurrent()) : 0); 108 | } 109 | 110 | public int getHeight() { 111 | return 1; 112 | } 113 | 114 | @NotNull 115 | public List render(@NotNull IElementContainer context) { 116 | String baseCommand = context.getCommand(this); 117 | 118 | List components = new ArrayList<>(); 119 | boolean current = value.getOptionalCurrent().orElse(false); 120 | TextComponent c = new TextComponent(current ? "\u2714" : "\u2718"); 121 | c.setColor(current ? trueColor : falseColor); 122 | c.setClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, baseCommand + !current)); 123 | components.add(c); 124 | 125 | if (showText) 126 | components.add(new TextComponent(" " + current)); 127 | 128 | return Collections.singletonList(new Text(components)); 129 | } 130 | 131 | public void edit(@NotNull IElementContainer container, @NotNull String[] args) { 132 | value.setCurrent(Boolean.parseBoolean(args[0])); 133 | } 134 | 135 | /** 136 | * @return the current value 137 | */ 138 | public boolean getValue() { 139 | return value.getOptionalCurrent().orElse(false); 140 | } 141 | 142 | /** 143 | * @param value the new value 144 | */ 145 | public void setValue(boolean value) { 146 | this.value.setCurrent(value); 147 | } 148 | 149 | @NotNull 150 | public List> getStates() { 151 | return Collections.singletonList(value); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/main/java/me/tom/sparse/spigot/chat/menu/element/ButtonElement.java: -------------------------------------------------------------------------------- 1 | package me.tom.sparse.spigot.chat.menu.element; 2 | 3 | import net.md_5.bungee.api.chat.BaseComponent; 4 | import net.md_5.bungee.api.chat.ClickEvent; 5 | import net.md_5.bungee.api.chat.TextComponent; 6 | 7 | import org.jetbrains.annotations.NotNull; 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | import java.util.Collections; 11 | import java.util.List; 12 | import java.util.function.Consumer; 13 | import java.util.function.Function; 14 | 15 | import org.bukkit.entity.Player; 16 | 17 | import me.tom.sparse.spigot.chat.menu.ChatMenu; 18 | import me.tom.sparse.spigot.chat.menu.ChatMenuAPI; 19 | import me.tom.sparse.spigot.chat.menu.IElementContainer; 20 | import me.tom.sparse.spigot.chat.util.Text; 21 | 22 | /** 23 | * A button that runs a callback when clicked. 24 | */ 25 | public class ButtonElement extends Element { 26 | public static ButtonElement createCloseButton(int x, int y, @NotNull String text, @NotNull ChatMenu menu) { 27 | return new ButtonElement(x, y, text, (p) -> { 28 | menu.close(p); 29 | return false; 30 | }); 31 | } 32 | 33 | @NotNull 34 | protected String text; 35 | @Nullable 36 | protected Function callback; 37 | 38 | /** 39 | * Construct a {@code ButtonElement} with no callback. 40 | * 41 | * @param x the x coordinate 42 | * @param y the y coordinate 43 | * @param text the text 44 | */ 45 | public ButtonElement(int x, int y, @NotNull String text) { 46 | this(x, y, text, (Function) null); 47 | } 48 | 49 | /** 50 | * Constructs a {@code ButtonElement} with the provided callback. Will always resend the menu after the button is 51 | * clicked. 52 | * 53 | * @param x the x coordinate 54 | * @param y the y coordinate 55 | * @param text the text 56 | * @param callback the callback to be called when the button is clicked. 57 | */ 58 | public ButtonElement(int x, int y, @NotNull String text, @Nullable Consumer callback) { 59 | this(x, y, text, player -> { 60 | if (callback != null) 61 | callback.accept(player); 62 | return true; 63 | }); 64 | } 65 | 66 | /** 67 | * Constructs a {@code ButtonElement} with the provided callback. 68 | * 69 | * @param x the x coordinate 70 | * @param y the y coordinate 71 | * @param text the text 72 | * @param callback the callback to be called when the button is clicked. Should return {@code true} to automatically 73 | * resend the menu. 74 | */ 75 | public ButtonElement(int x, int y, @NotNull String text, @Nullable Function callback) { 76 | super(x, y); 77 | if (text.contains("\n")) 78 | throw new IllegalArgumentException("Button text cannot contain newline"); 79 | this.text = text; 80 | this.callback = callback; 81 | } 82 | 83 | /** 84 | * @return the text this button displays 85 | */ 86 | @NotNull 87 | public String getText() { 88 | return text; 89 | } 90 | 91 | /** 92 | * @param text the new text this button should display 93 | * @throws IllegalArgumentException if text contains a newline. 94 | */ 95 | public void setText(@NotNull String text) { 96 | if (text.contains("\n")) 97 | throw new IllegalArgumentException("Button text cannot contain newline"); 98 | this.text = text; 99 | } 100 | 101 | public int getWidth() { 102 | return ChatMenuAPI.getWidth(text); 103 | } 104 | 105 | public int getHeight() { 106 | return 1; 107 | } 108 | 109 | public boolean isEnabled() { 110 | return true; 111 | } 112 | 113 | @NotNull 114 | public List render(@NotNull IElementContainer context) { 115 | String baseCommand = context.getCommand(this); 116 | 117 | BaseComponent[] components = TextComponent.fromLegacyText(text); 118 | ClickEvent click = new ClickEvent(ClickEvent.Action.RUN_COMMAND, baseCommand); 119 | for (BaseComponent component : components) 120 | component.setClickEvent(click); 121 | 122 | return Collections.singletonList(new Text(components)); 123 | } 124 | 125 | public boolean onClick(@NotNull IElementContainer container, @NotNull Player player) { 126 | super.onClick(container, player); 127 | return callback == null ? false : callback.apply(player); 128 | } 129 | 130 | public void edit(@NotNull IElementContainer container, @NotNull String[] args) { 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/main/java/me/tom/sparse/spigot/chat/menu/element/Element.java: -------------------------------------------------------------------------------- 1 | package me.tom.sparse.spigot.chat.menu.element; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | import java.util.Collection; 7 | import java.util.Collections; 8 | import java.util.List; 9 | 10 | import org.bukkit.Sound; 11 | import org.bukkit.entity.Player; 12 | 13 | import me.tom.sparse.spigot.chat.menu.IElementContainer; 14 | import me.tom.sparse.spigot.chat.util.State; 15 | import me.tom.sparse.spigot.chat.util.Text; 16 | 17 | public abstract class Element { 18 | protected int x, y; 19 | 20 | @Nullable 21 | protected Sound clickSound = Sound.UI_BUTTON_CLICK; 22 | protected float clickVolume = 0.5f; 23 | protected float clickPitch = 1; 24 | 25 | private boolean visible = true; 26 | 27 | /** 28 | * Constructs an element at the given x and y coordinates. 29 | * 30 | * @param x the x coordinate to put this element at 31 | * @param y the y coordinate to put this element at 32 | */ 33 | public Element(int x, int y) { 34 | this.x = x; 35 | this.y = y; 36 | } 37 | 38 | /** 39 | * @param visible whether this element should be visible 40 | */ 41 | public void setVisible(boolean visible) { 42 | this.visible = visible; 43 | } 44 | 45 | /** 46 | * @return true if this element will be visible 47 | */ 48 | public boolean isVisible() { 49 | return visible; 50 | } 51 | 52 | /** 53 | * @return the pitch of the sound played when a player clicks this element 54 | */ 55 | public float getClickPitch() { 56 | return clickPitch; 57 | } 58 | 59 | /** 60 | * @return the volume of the sound played when a player clicks this element 61 | */ 62 | public float getClickVolume() { 63 | return clickVolume; 64 | } 65 | 66 | /** 67 | * @return the sound played when a player clicks this element 68 | */ 69 | @Nullable 70 | public Sound getClickSound() { 71 | return clickSound; 72 | } 73 | 74 | /** 75 | * Sets the sound and the volume and pitch of the sound played when a player clicks this element. 76 | * 77 | * @param clickSound the new sound 78 | */ 79 | public void setClickSound(@NotNull Sound clickSound) { 80 | this.clickSound = clickSound; 81 | } 82 | 83 | public void setClickVolume(float clickVolume) { 84 | this.clickVolume = clickVolume; 85 | } 86 | 87 | public void setClickPitch(float clickPitch) { 88 | this.clickPitch = clickPitch; 89 | } 90 | 91 | /** 92 | * @return the x coordinate of the left-most part of this element 93 | */ 94 | public final int getLeft() { 95 | return getX(); 96 | } 97 | 98 | /** 99 | * @return the x coordinate of the right-most part of this element 100 | */ 101 | public final int getRight() { 102 | return getX() + getWidth(); 103 | } 104 | 105 | /** 106 | * @return the y coordinate of the top-most part of this element 107 | */ 108 | public final int getTop() { 109 | return getY(); 110 | } 111 | 112 | /** 113 | * @return the y coordinate of the bottom-most part of this element 114 | */ 115 | public final int getBottom() { 116 | return getY() + getHeight(); 117 | } 118 | 119 | /** 120 | * @return the x coordinate of this element 121 | */ 122 | public int getX() { 123 | return x; 124 | } 125 | 126 | /** 127 | * Sets the x coordinate of this element 128 | * 129 | * @param x the new coordinate 130 | */ 131 | public void setX(int x) { 132 | this.x = x; 133 | } 134 | 135 | /** 136 | * @return the width of this element 137 | */ 138 | public abstract int getWidth(); 139 | 140 | /** 141 | * @return the y coordinate of this element 142 | */ 143 | public int getY() { 144 | return y; 145 | } 146 | 147 | /** 148 | * Sets the y coordinate of this element 149 | * 150 | * @param y the new coordinate 151 | */ 152 | public void setY(int y) { 153 | this.y = y; 154 | } 155 | 156 | /** 157 | * @return the height of this element 158 | */ 159 | public abstract int getHeight(); 160 | 161 | /** 162 | * Detects if the provided element overlaps this one. 163 | *
164 | * This method will always return false if the provided element is this element. 165 | * 166 | * @param other the element to detect collision with 167 | * @return true of the elements overlap 168 | */ 169 | public final boolean overlaps(@NotNull Element other) { 170 | if (other == this) 171 | return false; 172 | 173 | int tw = this.getWidth(); 174 | int th = this.getHeight(); 175 | int rw = other.getWidth(); 176 | int rh = other.getHeight(); 177 | 178 | if (rw <= 0 || rh <= 0 || tw <= 0 || th <= 0) { 179 | return false; 180 | } 181 | int tx = this.getX(); 182 | int ty = this.getY(); 183 | int rx = other.getX(); 184 | int ry = other.getY(); 185 | rw += rx; 186 | rh += ry; 187 | tw += tx; 188 | th += ty; 189 | 190 | // overflow || intersect 191 | return ((rw < rx || rw > tx) && 192 | (rh < ry || rh > ty) && 193 | (tw < tx || tw > rx) && 194 | (th < ty || th > ry)); 195 | } 196 | 197 | /** 198 | * @param context the current render context 199 | * @return the rendered text 200 | */ 201 | public abstract List render(IElementContainer context); 202 | 203 | /** 204 | * Called when a player clicks this element. 205 | *
206 | * More specifically, when a player runs the command to edit this element. 207 | * 208 | * @param container the container this element was clicked on 209 | * @param player the player that clicked this element 210 | * @return true if the menu should rebuild and resend 211 | */ 212 | public boolean onClick(@NotNull IElementContainer container, @NotNull Player player) { 213 | if (clickSound != null) 214 | player.playSound(player.getEyeLocation(), clickSound, clickVolume, clickPitch); 215 | return true; 216 | } 217 | 218 | /** 219 | * Called to edit this element 220 | * 221 | * @param container the container this element is being edited on 222 | * @param args the data to be parsed 223 | */ 224 | public abstract void edit(@NotNull IElementContainer container, @NotNull String[] args); 225 | 226 | /** 227 | * @return an unmodifiable {@link java.util.Collection} of all the states in this element. 228 | */ 229 | @NotNull 230 | public Collection> getStates() { 231 | return Collections.emptyList(); 232 | } 233 | 234 | public String toString() { 235 | return getClass().getName() + "{" + 236 | "x=" + x + 237 | ", y=" + y + 238 | ", visible=" + visible + 239 | '}'; 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /src/main/java/me/tom/sparse/spigot/chat/menu/element/GroupElement.java: -------------------------------------------------------------------------------- 1 | package me.tom.sparse.spigot.chat.menu.element; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.util.ArrayList; 6 | import java.util.Collections; 7 | import java.util.Comparator; 8 | import java.util.List; 9 | import java.util.Objects; 10 | 11 | import org.bukkit.entity.Player; 12 | 13 | import me.tom.sparse.spigot.chat.menu.IElementContainer; 14 | import me.tom.sparse.spigot.chat.util.Text; 15 | 16 | public class GroupElement extends Element implements IElementContainer { 17 | @NotNull 18 | protected final IElementContainer parent; 19 | @NotNull 20 | protected List elements; 21 | 22 | /** 23 | * Constructs an element at the given x and y coordinates. 24 | * 25 | * @param parent The parent element of this group. Usually a {@code ChatMenu} or another {@code GroupElement} 26 | * @param x the x coordinate to put this element at 27 | * @param y the y coordinate to put this element at 28 | */ 29 | public GroupElement(@NotNull IElementContainer parent, int x, int y) { 30 | super(x, y); 31 | this.parent = parent; 32 | this.elements = new ArrayList<>(); 33 | } 34 | 35 | /** 36 | * @param element the element to add 37 | * @param the type of element to add 38 | * @return the element that was added 39 | */ 40 | public T add(@NotNull T element) { 41 | Objects.requireNonNull(element); 42 | elements.add(element); 43 | elements.sort(Comparator.comparingInt(Element::getX)); 44 | return element; 45 | } 46 | 47 | /** 48 | * Removes the specified element from this group. 49 | * 50 | * @param element the element to remove 51 | * @return true if the element was removed 52 | */ 53 | public boolean remove(@NotNull Element element) { 54 | return elements.remove(element); 55 | } 56 | 57 | /** 58 | * @return an unmodifiable list of all the elements in this group. 59 | */ 60 | @NotNull 61 | public List getElements() { 62 | return Collections.unmodifiableList(elements); 63 | } 64 | 65 | /** 66 | * @param element the element to interact with 67 | * @return the command used to interact with this element 68 | */ 69 | @NotNull 70 | public String getCommand(@NotNull Element element) { 71 | int index = elements.indexOf(element); 72 | if (index == -1) 73 | throw new IllegalArgumentException("Unable to interact with the provided element"); 74 | return parent.getCommand(this) + index + " "; 75 | } 76 | 77 | public int getWidth() { 78 | Element furthest = elements.stream().max(Comparator.comparingInt(Element::getRight)).orElse(null); 79 | if (furthest == null) return 0; 80 | return furthest.getRight(); 81 | } 82 | 83 | public int getHeight() { 84 | Element furthest = elements.stream().max(Comparator.comparingInt(Element::getBottom)).orElse(null); 85 | if (furthest == null) return 0; 86 | return furthest.getBottom(); 87 | } 88 | 89 | @NotNull 90 | public List render(@NotNull IElementContainer context) { 91 | if (context != parent) 92 | throw new IllegalStateException("Attempted to render GroupElement with non-parent context"); 93 | int height = getHeight(); 94 | List lines = new ArrayList<>(height); 95 | for (int i = 0; i < height; i++) 96 | lines.add(new Text()); 97 | 98 | for (Element element : elements) { 99 | if (!element.isVisible()) 100 | continue; 101 | 102 | List elementTexts = element.render(this); 103 | for (int j = 0; j < elementTexts.size(); j++) { 104 | int lineY = element.getY() + j; 105 | 106 | if (lineY < 0 || lineY >= height) 107 | continue; 108 | 109 | Text text = lines.get(lineY); 110 | text.expandToWidth(element.getX()); 111 | 112 | Text toAdd = elementTexts.get(j); 113 | toAdd.expandToWidth(element.getWidth()); 114 | text.append(toAdd); 115 | } 116 | } 117 | 118 | return lines; 119 | } 120 | 121 | public void edit(@NotNull IElementContainer container, @NotNull String[] args) { 122 | int index = Integer.parseInt(args[0]); 123 | String[] newArgs = new String[args.length - 1]; 124 | System.arraycopy(args, 1, newArgs, 0, newArgs.length); 125 | elements.get(index).edit(container, newArgs); 126 | } 127 | 128 | public void openFor(@NotNull Player player) { 129 | parent.openFor(player); 130 | } 131 | 132 | public void refresh() { 133 | parent.refresh(); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/main/java/me/tom/sparse/spigot/chat/menu/element/HorizontalRuleElement.java: -------------------------------------------------------------------------------- 1 | package me.tom.sparse.spigot.chat.menu.element; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.util.Collections; 6 | import java.util.List; 7 | 8 | import me.tom.sparse.spigot.chat.menu.ChatMenuAPI; 9 | import me.tom.sparse.spigot.chat.menu.IElementContainer; 10 | import me.tom.sparse.spigot.chat.util.Text; 11 | import me.tom.sparse.spigot.chat.util.TextUtil; 12 | 13 | public class HorizontalRuleElement extends Element { 14 | private final String text; 15 | private final int width; 16 | 17 | /** 18 | * Constructs a {@code HorizontalRuleElement} at the provided Y coordinate and a width of {@code 320} (default chat 19 | * width). 20 | * 21 | * @param y the y coordinate to put this element at 22 | */ 23 | public HorizontalRuleElement(int y) { 24 | this(0, y, 320); 25 | } 26 | 27 | /** 28 | * Constructs a {@code HorizontalRuleElement}. 29 | * 30 | * @param x the x coordinate to put this element at 31 | * @param y the y coordinate to put this element at 32 | * @param width the width of this element 33 | */ 34 | public HorizontalRuleElement(int x, int y, int width) { 35 | super(x, y); 36 | this.text = "\u00a7m" + TextUtil.generateWidth(' ', width, false); 37 | this.width = ChatMenuAPI.getWidth(text); 38 | } 39 | 40 | public int getWidth() { 41 | return width; 42 | } 43 | 44 | public int getHeight() { 45 | return 1; 46 | } 47 | 48 | public List render(IElementContainer context) { 49 | return Collections.singletonList(new Text(text)); 50 | } 51 | 52 | public void edit(@NotNull IElementContainer container, @NotNull String[] args) { 53 | 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/me/tom/sparse/spigot/chat/menu/element/ImageElement.java: -------------------------------------------------------------------------------- 1 | package me.tom.sparse.spigot.chat.menu.element; 2 | 3 | import net.md_5.bungee.api.ChatColor; 4 | import net.md_5.bungee.api.chat.BaseComponent; 5 | import net.md_5.bungee.api.chat.ClickEvent; 6 | import net.md_5.bungee.api.chat.TextComponent; 7 | 8 | import org.jetbrains.annotations.NotNull; 9 | 10 | import java.util.ArrayList; 11 | import java.util.Arrays; 12 | import java.util.Collections; 13 | import java.util.List; 14 | import java.util.stream.Collectors; 15 | 16 | import me.tom.sparse.spigot.chat.menu.IElementContainer; 17 | import me.tom.sparse.spigot.chat.util.Text; 18 | 19 | @Deprecated 20 | public class ImageElement extends Element { 21 | public static final List COLORS = Collections.unmodifiableList(Arrays.stream(org.bukkit.ChatColor.values()).filter(org.bukkit.ChatColor::isColor).map(org.bukkit.ChatColor::asBungee).collect(Collectors.toList())); 22 | 23 | protected int[] colors = new int[20 * 20]; 24 | protected PixelClickCallback callback; 25 | 26 | public ImageElement(int x, int y) { 27 | super(x, y); 28 | Arrays.fill(colors, 0); 29 | } 30 | 31 | public void setCallback(PixelClickCallback callback) { 32 | this.callback = callback; 33 | } 34 | 35 | public int onPixelClick(int x, int y, int currentColor) { 36 | return callback == null ? currentColor : callback.onPixelClick(x, y, currentColor); 37 | } 38 | 39 | public int getPixel(int x, int y) { 40 | if (x < 0 || y < 0 || x >= 20 || y >= 20) 41 | return -1; 42 | return colors[x * 20 + y]; 43 | } 44 | 45 | public void setPixel(int x, int y, int color) { 46 | if (x < 0 || y < 0 || x >= 20 || y >= 20) 47 | return; 48 | colors[x * 20 + y] = color; 49 | } 50 | 51 | public int[] getColors() { 52 | return colors; 53 | } 54 | 55 | public int getWidth() { 56 | return 9 * 20; 57 | } 58 | 59 | public int getHeight() { 60 | return 20; 61 | } 62 | 63 | public boolean isEnabled() { 64 | return true; 65 | } 66 | 67 | public List render(IElementContainer context) { 68 | List result = new ArrayList<>(); 69 | String baseCommand = context.getCommand(this); 70 | 71 | for (int y = 0; y < 20; y++) { 72 | List line = new ArrayList<>(); 73 | for (int x = 0; x < 20; x++) { 74 | TextComponent c = new TextComponent("\u2588"); 75 | int colorIndex = x * 20 + y; 76 | c.setColor(COLORS.get(colors[colorIndex])); 77 | c.setClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, baseCommand + colorIndex)); 78 | line.add(c); 79 | } 80 | 81 | result.add(new Text(line)); 82 | } 83 | 84 | return result; 85 | } 86 | 87 | public void edit(@NotNull IElementContainer container, @NotNull String[] args) { 88 | int index = Integer.parseInt(args[0]); 89 | colors[index] = onPixelClick(index / 20, index % 20, colors[index]); 90 | } 91 | 92 | public interface PixelClickCallback { 93 | int onPixelClick(int x, int y, int currentColor); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/me/tom/sparse/spigot/chat/menu/element/IncrementalElement.java: -------------------------------------------------------------------------------- 1 | package me.tom.sparse.spigot.chat.menu.element; 2 | 3 | import net.md_5.bungee.api.ChatColor; 4 | import net.md_5.bungee.api.chat.BaseComponent; 5 | import net.md_5.bungee.api.chat.ClickEvent; 6 | import net.md_5.bungee.api.chat.TextComponent; 7 | 8 | import org.jetbrains.annotations.NotNull; 9 | 10 | import java.util.ArrayList; 11 | import java.util.Collections; 12 | import java.util.List; 13 | 14 | import me.tom.sparse.spigot.chat.menu.ChatMenuAPI; 15 | import me.tom.sparse.spigot.chat.menu.IElementContainer; 16 | import me.tom.sparse.spigot.chat.util.State; 17 | import me.tom.sparse.spigot.chat.util.Text; 18 | 19 | /** 20 | * A number with a subtract button on the left and a add button on the right. 21 | */ 22 | public class IncrementalElement extends Element { 23 | @NotNull 24 | public final State value; 25 | protected int min = Integer.MIN_VALUE, max = Integer.MAX_VALUE; 26 | 27 | /** 28 | * Construct a new {@code IncrementalElement} with a minimum of {@link Integer#MIN_VALUE} and a maximum of {@link 29 | * Integer#MAX_VALUE} 30 | * 31 | * @param x the x coordinate 32 | * @param y the y coordinate 33 | * @param value the starting value 34 | */ 35 | public IncrementalElement(int x, int y, int value) { 36 | super(x, y); 37 | this.value = new State<>(value, this::filter); 38 | } 39 | 40 | /** 41 | * Constructs a new {@code IncrementalElement} 42 | * 43 | * @param x the x coordinate 44 | * @param y the y coordinate 45 | * @param min the minimum value 46 | * @param max the maximum value 47 | * @param value the starting value 48 | */ 49 | public IncrementalElement(int x, int y, int min, int max, int value) { 50 | super(x, y); 51 | this.min = min; 52 | this.max = max; 53 | this.value = new State<>(value, this::filter); 54 | } 55 | 56 | private int filter(int v) { 57 | return Math.min(Math.max(v, min), max); 58 | } 59 | 60 | /** 61 | * @return the minimum value 62 | */ 63 | public int getMin() { 64 | return min; 65 | } 66 | 67 | /** 68 | * @param min the new minimum value 69 | */ 70 | public void setMin(int min) { 71 | this.min = min; 72 | value.setCurrent(value.getCurrent()); 73 | } 74 | 75 | /** 76 | * @return the maximum value 77 | */ 78 | public int getMax() { 79 | return max; 80 | } 81 | 82 | /** 83 | * @param max the new maximum value 84 | */ 85 | public void setMax(int max) { 86 | this.max = max; 87 | } 88 | 89 | /** 90 | * @param value the new value 91 | */ 92 | public void setValue(int value) { 93 | this.value.setCurrent(value); 94 | } 95 | 96 | /** 97 | * @return the current value 98 | */ 99 | public int getValue() { 100 | return value.getOptionalCurrent().orElse(0); 101 | } 102 | 103 | public int getWidth() { 104 | return ChatMenuAPI.getWidth("[-] " + value.getCurrent() + " [+]"); 105 | } 106 | 107 | public int getHeight() { 108 | return 1; 109 | } 110 | 111 | public boolean isEnabled() { 112 | return true; 113 | } 114 | 115 | @NotNull 116 | public List render(@NotNull IElementContainer context) { 117 | String baseCommand = context.getCommand(this); 118 | 119 | List components = new ArrayList<>(); 120 | TextComponent decrement = new TextComponent("[-]"); 121 | int current = value.getOptionalCurrent().orElse(0); 122 | if (current - 1 >= min) { 123 | decrement.setColor(ChatColor.RED); 124 | decrement.setClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, baseCommand + (current - 1))); 125 | } else { 126 | decrement.setColor(ChatColor.DARK_GRAY); 127 | } 128 | 129 | TextComponent increment = new TextComponent("[+]"); 130 | if (current + 1 <= max) { 131 | increment.setColor(ChatColor.GREEN); 132 | increment.setClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, baseCommand + (current + 1))); 133 | } else { 134 | increment.setColor(ChatColor.DARK_GRAY); 135 | } 136 | 137 | TextComponent number = new TextComponent(" " + current + " "); 138 | 139 | components.add(decrement); 140 | components.add(number); 141 | components.add(increment); 142 | 143 | return Collections.singletonList(new Text(components)); 144 | } 145 | 146 | public void edit(@NotNull IElementContainer container, @NotNull String[] args) { 147 | value.setCurrent(Integer.parseInt(args[0])); 148 | } 149 | 150 | @NotNull 151 | public List> getStates() { 152 | return Collections.singletonList(value); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/main/java/me/tom/sparse/spigot/chat/menu/element/InputElement.java: -------------------------------------------------------------------------------- 1 | package me.tom.sparse.spigot.chat.menu.element; 2 | 3 | import net.md_5.bungee.api.ChatColor; 4 | import net.md_5.bungee.api.chat.ClickEvent; 5 | 6 | import org.jetbrains.annotations.NotNull; 7 | import org.jetbrains.annotations.Nullable; 8 | 9 | import java.util.Collections; 10 | import java.util.List; 11 | 12 | import org.bukkit.entity.Player; 13 | 14 | import me.tom.sparse.spigot.chat.menu.CMListener; 15 | import me.tom.sparse.spigot.chat.menu.ChatMenuAPI; 16 | import me.tom.sparse.spigot.chat.menu.IElementContainer; 17 | import me.tom.sparse.spigot.chat.util.State; 18 | import me.tom.sparse.spigot.chat.util.Text; 19 | 20 | public class InputElement extends Element { 21 | @NotNull 22 | public final State value; 23 | 24 | protected int width; 25 | private boolean editing; 26 | 27 | /** 28 | * Constructs a new {@code InputElement} 29 | * 30 | * @param x the x coordinate 31 | * @param y the y coordinate 32 | * @param width the max width of the text 33 | * @param value the starting text 34 | */ 35 | public InputElement(int x, int y, int width, @NotNull String value) { 36 | super(x, y); 37 | this.width = width; 38 | this.value = new State<>(value); 39 | } 40 | 41 | /** 42 | * @return the current value 43 | */ 44 | @Nullable 45 | public String getValue() { 46 | return value.getCurrent(); 47 | } 48 | 49 | /** 50 | * Sets the text of this element, if the text is longer than the max width it will display "Too long" 51 | * 52 | * @param value the new value 53 | */ 54 | public void setValue(@NotNull String value) { 55 | // if(ChatMenuAPI.getWidth(text) > width) 56 | // throw new IllegalArgumentException("The provided text is too wide to fit!"); 57 | this.value.setCurrent(value); 58 | } 59 | 60 | public int getWidth() { 61 | return width; 62 | } 63 | 64 | public int getHeight() { 65 | return 1; 66 | } 67 | 68 | @NotNull 69 | public List render(@NotNull IElementContainer context) { 70 | ClickEvent click = new ClickEvent(ClickEvent.Action.RUN_COMMAND, context.getCommand(this)); 71 | 72 | String current = value.getOptionalCurrent().orElse(""); 73 | boolean tooLong = ChatMenuAPI.getWidth(current) > width; 74 | 75 | Text text = new Text(tooLong ? "Too long" : current); 76 | text.expandToWidth(width); 77 | text.getComponents().forEach(it -> { 78 | if (tooLong) 79 | it.setColor(ChatColor.RED); 80 | if (editing) 81 | it.setColor(ChatColor.GRAY); 82 | it.setUnderlined(true); 83 | it.setClickEvent(click); 84 | }); 85 | 86 | return Collections.singletonList(text); 87 | } 88 | 89 | public boolean onClick(@NotNull IElementContainer container, @NotNull Player player) { 90 | super.onClick(container, player); 91 | container.getElements().stream().filter(it -> it instanceof InputElement && it != this).map(it -> (InputElement) it).forEach(it -> it.editing = false); 92 | editing = !editing; 93 | 94 | if (editing) { 95 | CMListener.expectPlayerChat(player, (p, m) -> { 96 | editing = false; 97 | setValue(m); 98 | container.refresh(); 99 | return true; 100 | }); 101 | } else { 102 | CMListener.cancelExpectation(player); 103 | } 104 | 105 | return true; 106 | } 107 | 108 | public void edit(@NotNull IElementContainer container, @NotNull String[] args) { 109 | 110 | } 111 | 112 | @NotNull 113 | public List> getStates() { 114 | return Collections.singletonList(value); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/me/tom/sparse/spigot/chat/menu/element/LinkButtonElement.java: -------------------------------------------------------------------------------- 1 | package me.tom.sparse.spigot.chat.menu.element; 2 | 3 | import net.md_5.bungee.api.chat.BaseComponent; 4 | import net.md_5.bungee.api.chat.ClickEvent; 5 | import net.md_5.bungee.api.chat.TextComponent; 6 | 7 | import org.jetbrains.annotations.NotNull; 8 | 9 | import java.util.Collections; 10 | import java.util.List; 11 | 12 | import me.tom.sparse.spigot.chat.menu.ChatMenuAPI; 13 | import me.tom.sparse.spigot.chat.menu.IElementContainer; 14 | import me.tom.sparse.spigot.chat.util.Text; 15 | 16 | /** 17 | * A button that opens a link when clicked. 18 | */ 19 | public class LinkButtonElement extends Element { 20 | @NotNull 21 | protected String text; 22 | @NotNull 23 | protected String link; 24 | 25 | /** 26 | * Constructs a new {@code LinkButtonElement} 27 | * 28 | * @param x the x coordinate 29 | * @param y the y coordinate 30 | * @param text the text to display 31 | * @param link the link 32 | * @throws IllegalArgumentException if text contains newlines 33 | */ 34 | public LinkButtonElement(int x, int y, @NotNull String text, @NotNull String link) { 35 | super(x, y); 36 | if (text.contains("\n")) 37 | throw new IllegalArgumentException("Button text cannot contain newline"); 38 | this.text = text; 39 | this.link = link; 40 | } 41 | 42 | /** 43 | * @return the text that displays for this button 44 | */ 45 | @NotNull 46 | public String getText() { 47 | return text; 48 | } 49 | 50 | /** 51 | * @param text the new text to display 52 | */ 53 | public void setText(@NotNull String text) { 54 | if (text.contains("\n")) 55 | throw new IllegalArgumentException("Button text cannot contain newline"); 56 | this.text = text; 57 | } 58 | 59 | /** 60 | * @return the link 61 | */ 62 | @NotNull 63 | public String getLink() { 64 | return link; 65 | } 66 | 67 | /** 68 | * @param link the new link 69 | */ 70 | public void setLink(@NotNull String link) { 71 | this.link = link; 72 | } 73 | 74 | public int getWidth() { 75 | return ChatMenuAPI.getWidth(text); 76 | } 77 | 78 | public int getHeight() { 79 | return 1; 80 | } 81 | 82 | public List render(IElementContainer context) { 83 | BaseComponent[] components = TextComponent.fromLegacyText(text); 84 | ClickEvent click = new ClickEvent(ClickEvent.Action.OPEN_URL, link); 85 | for (BaseComponent component : components) 86 | component.setClickEvent(click); 87 | 88 | return Collections.singletonList(new Text(components)); 89 | } 90 | 91 | public void edit(@NotNull IElementContainer container, @NotNull String[] args) { 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/me/tom/sparse/spigot/chat/menu/element/NumberSliderElement.java: -------------------------------------------------------------------------------- 1 | package me.tom.sparse.spigot.chat.menu.element; 2 | 3 | import net.md_5.bungee.api.ChatColor; 4 | import net.md_5.bungee.api.chat.BaseComponent; 5 | import net.md_5.bungee.api.chat.ClickEvent; 6 | import net.md_5.bungee.api.chat.TextComponent; 7 | 8 | import org.jetbrains.annotations.NotNull; 9 | import org.jetbrains.annotations.Nullable; 10 | 11 | import java.util.ArrayList; 12 | import java.util.Collections; 13 | import java.util.List; 14 | 15 | import org.bukkit.entity.Player; 16 | 17 | import me.tom.sparse.spigot.chat.menu.ChatMenuAPI; 18 | import me.tom.sparse.spigot.chat.menu.IElementContainer; 19 | import me.tom.sparse.spigot.chat.util.NumberFormat; 20 | import me.tom.sparse.spigot.chat.util.State; 21 | import me.tom.sparse.spigot.chat.util.Text; 22 | 23 | public class NumberSliderElement extends Element { 24 | public static final int MIN_PRECISION = 0; 25 | public static final int MAX_PRECISION = 7; 26 | 27 | @NotNull 28 | public final State value; 29 | protected int length; 30 | 31 | @NotNull 32 | protected ChatColor fullColor = ChatColor.GREEN; 33 | @NotNull 34 | protected ChatColor emptyColor = ChatColor.RED; 35 | 36 | @NotNull 37 | protected NumberFormat numberFormat = NumberFormat.PERCENTAGE; 38 | 39 | protected int precision = 6; 40 | 41 | /** 42 | * Constructs a {@code NumberSliderElement} with {@link NumberFormat#PERCENTAGE} formatting 43 | * 44 | * @param x the x coordinate 45 | * @param y the y coordinate 46 | * @param length the number of bars to display 47 | * @param value the number of bars that are full 48 | */ 49 | public NumberSliderElement(int x, int y, int length, int value) { 50 | super(x, y); 51 | this.length = length; 52 | this.value = new State<>(value, this::filter); 53 | } 54 | 55 | /** 56 | * Constructs a {@code NumberSliderElement} 57 | * 58 | * @param x the x coordinate 59 | * @param y the y coordinate 60 | * @param length the number of bars to display 61 | * @param value the number of bars that are full 62 | * @param format the format for the number 63 | */ 64 | public NumberSliderElement(int x, int y, int length, int value, @Nullable NumberFormat format) { 65 | super(x, y); 66 | this.length = length; 67 | this.value = new State<>(value, this::filter); 68 | this.numberFormat = format == null ? NumberFormat.NONE : format; 69 | } 70 | 71 | private int filter(int v) { 72 | return Math.max(Math.min(v, length), 0); 73 | } 74 | 75 | /** 76 | * Sets the colors that should be used when displaying this. 77 | * 78 | * @param fullColor the color for all of the full bars 79 | * @param emptyColor the color for all of the empty bars 80 | * @return this 81 | */ 82 | @NotNull 83 | public NumberSliderElement colors(@NotNull ChatColor fullColor, @NotNull ChatColor emptyColor) { 84 | setFullColor(fullColor); 85 | setEmptyColor(emptyColor); 86 | return this; 87 | } 88 | 89 | /** 90 | * Sets the number format to {@link NumberFormat#NONE} 91 | * 92 | * @return this 93 | */ 94 | public NumberSliderElement hideNumber() { 95 | return numberFormat(NumberFormat.NONE); 96 | } 97 | 98 | /** 99 | * Sets the number format 100 | * 101 | * @param format the new number format 102 | * @return this 103 | */ 104 | public NumberSliderElement numberFormat(@Nullable NumberFormat format) { 105 | setNumberFormat(format); 106 | return this; 107 | } 108 | 109 | /** 110 | * @return the current number format 111 | */ 112 | @NotNull 113 | public NumberFormat getNumberFormat() { 114 | return numberFormat; 115 | } 116 | 117 | /** 118 | * @param format the new number format 119 | */ 120 | public void setNumberFormat(@Nullable NumberFormat format) { 121 | this.numberFormat = format == null ? NumberFormat.NONE : format; 122 | } 123 | 124 | /** 125 | * @return the precision. Must be within (inclusive) {@link NumberSliderElement#MIN_PRECISION} and {@link 126 | * NumberSliderElement#MAX_PRECISION} 127 | */ 128 | public int getPrecision() { 129 | return precision; 130 | } 131 | 132 | /** 133 | * Sets the precision of this. Precision determines how wide the bars will be, higher precision means smaller bars. 134 | * 135 | * @param precision the new precision. Must be within (inclusive) {@link NumberSliderElement#MIN_PRECISION} and 136 | * {@link NumberSliderElement#MAX_PRECISION} 137 | */ 138 | public void setPrecision(int precision) { 139 | if (precision < 0 || precision > 7) 140 | throw new IllegalArgumentException("Precision must be between (inclusive) 0-7"); 141 | this.precision = precision; 142 | } 143 | 144 | /** 145 | * Sets the precision of this. Precision determines how wide the bars will be, higher precision means smaller bars. 146 | * 147 | * @param precision the new precision. Must be within (inclusive) {@link NumberSliderElement#MIN_PRECISION} and 148 | * {@link NumberSliderElement#MAX_PRECISION} 149 | * @return this 150 | */ 151 | public NumberSliderElement precision(int precision) { 152 | setPrecision(precision); 153 | return this; 154 | } 155 | 156 | // * @param fullColor the color for all of the full bars 157 | // * @param emptyColor the color for all of the empty bars 158 | 159 | /** 160 | * @return the color for all of the empty bars 161 | */ 162 | @NotNull 163 | public ChatColor getEmptyColor() { 164 | return emptyColor; 165 | } 166 | 167 | /** 168 | * @param emptyColor the new color for all of the empty bars 169 | */ 170 | public void setEmptyColor(@Nullable ChatColor emptyColor) { 171 | this.emptyColor = emptyColor == null ? ChatColor.RED : emptyColor; 172 | } 173 | 174 | /** 175 | * @return the color for all of the full bars 176 | */ 177 | @NotNull 178 | public ChatColor getFullColor() { 179 | return fullColor; 180 | } 181 | 182 | /** 183 | * @param fullColor the new color for all of the full bars 184 | */ 185 | public void setFullColor(@Nullable ChatColor fullColor) { 186 | this.fullColor = fullColor == null ? ChatColor.GREEN : fullColor; 187 | } 188 | 189 | /** 190 | * @return the number of bars that get displayed 191 | */ 192 | public int getLength() { 193 | return length; 194 | } 195 | 196 | /** 197 | * @param length thew new number of bars to display 198 | */ 199 | public void setLength(int length) { 200 | this.length = length < 0 ? 10 : length; 201 | } 202 | 203 | /** 204 | * Sets the length of this (based on the current precision) to attempt to make the width match as closely as 205 | * possible to the target width. 206 | * 207 | * @param width the width to attempt to match 208 | */ 209 | public void setWidth(int width) { 210 | int charWidth = ChatMenuAPI.getCharacterWidth(getCharacter()); 211 | length = width / charWidth; 212 | } 213 | 214 | /** 215 | * Sets the length of this (based on the current precision) to attempt to make the width match as closely as 216 | * possible to the target width. 217 | * 218 | * @param width the width to attempt to match 219 | * @return this 220 | */ 221 | @NotNull 222 | public NumberSliderElement width(int width) { 223 | setWidth(width); 224 | return this; 225 | } 226 | 227 | /** 228 | * @return the current value 229 | */ 230 | public int getValue() { 231 | return value.getOptionalCurrent().orElse(0); 232 | } 233 | 234 | /** 235 | * @param value the new value. Must not be less than 0 or more than {@code length} 236 | */ 237 | public void setValue(int value) { 238 | this.value.setCurrent(value); 239 | } 240 | 241 | /** 242 | * @return the bar character used based on the current precision 243 | */ 244 | public char getCharacter() { 245 | return (char) ('\u2588' + precision); 246 | } 247 | 248 | public int getWidth() { 249 | return ChatMenuAPI.getWidth(String.valueOf(getCharacter())) * length + ChatMenuAPI.getWidth(getFormattedNumber()); 250 | } 251 | 252 | // private String getPercentageString() 253 | // { 254 | // return String.format(" %.1f%%", value * 100); 255 | // } 256 | 257 | private String getFormattedNumber() { 258 | return " " + numberFormat.format(getValue(), length); 259 | } 260 | 261 | public int getHeight() { 262 | return 1; 263 | } 264 | 265 | public List render(IElementContainer context) { 266 | String baseCommand = context.getCommand(this); 267 | 268 | List components = new ArrayList<>(); 269 | for (int i = 0; i < length; i++) { 270 | // double v = (double) (i + 1) / (double) length; 271 | TextComponent c = new TextComponent(String.valueOf((char) ('\u2588' + precision))); 272 | c.setColor(i <= getValue() ? isEnabled() ? fullColor : ChatColor.GRAY : isEnabled() ? emptyColor : ChatColor.DARK_GRAY); 273 | c.setClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, baseCommand + i)); 274 | components.add(c); 275 | } 276 | components.add(new TextComponent(getFormattedNumber())); 277 | 278 | return Collections.singletonList(new Text(components)); 279 | } 280 | 281 | public boolean isEnabled() { 282 | return true; 283 | } 284 | 285 | public boolean onClick(@NotNull IElementContainer container, @NotNull Player player) { 286 | return isEnabled() && super.onClick(container, player); 287 | } 288 | 289 | public void edit(@NotNull IElementContainer container, @NotNull String[] args) { 290 | if (!isEnabled()) 291 | return; 292 | value.setCurrent(Integer.parseInt(args[0])); 293 | } 294 | 295 | @NotNull 296 | public List> getStates() { 297 | return Collections.singletonList(value); 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /src/main/java/me/tom/sparse/spigot/chat/menu/element/TextElement.java: -------------------------------------------------------------------------------- 1 | package me.tom.sparse.spigot.chat.menu.element; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | import me.tom.sparse.spigot.chat.menu.ChatMenuAPI; 10 | import me.tom.sparse.spigot.chat.menu.IElementContainer; 11 | import me.tom.sparse.spigot.chat.util.Text; 12 | 13 | /** 14 | * Basic text. 15 | */ 16 | public class TextElement extends Element { 17 | protected static final int BORDER_WIDTH = ChatMenuAPI.getWidth("| |"); 18 | 19 | @NotNull 20 | protected String[] lines; 21 | protected int width; 22 | 23 | @NotNull 24 | protected TextAlignment alignment = TextAlignment.LEFT; 25 | protected boolean border; 26 | 27 | /** 28 | * Constructs a {@code TextElement} with {@link TextAlignment#LEFT} and no border 29 | * 30 | * @param text the starting text. May contain {@code \n} 31 | * @param x the x coordinate 32 | * @param y the y coordinate 33 | */ 34 | public TextElement(@NotNull String text, int x, int y) { 35 | super(x, y); 36 | 37 | if (text.contains("\n")) { 38 | lines = text.split("\n"); 39 | } else { 40 | lines = new String[]{text}; 41 | } 42 | for (String line : lines) { 43 | int w = ChatMenuAPI.getWidth(line); 44 | if (w > width) 45 | width = w; 46 | } 47 | } 48 | 49 | /** 50 | * Constructs a {@code TextElement} with {@link TextAlignment#LEFT} and no border 51 | * 52 | * @param x the x coordinate 53 | * @param y the y coordinate 54 | * @param text the lines of text. Lines may not contain {@code \n} 55 | */ 56 | public TextElement(int x, int y, @NotNull String... text) { 57 | super(x, y); 58 | 59 | this.lines = text; 60 | for (String line : lines) { 61 | if (line.contains("\n")) 62 | throw new IllegalArgumentException("Cannot use TextElement line constructor with newline characters."); 63 | int w = ChatMenuAPI.getWidth(line); 64 | if (w > width) 65 | width = w; 66 | } 67 | } 68 | 69 | public void setText(String text) { 70 | setLines(text.split("\n")); 71 | } 72 | 73 | public void setLines(@NotNull String... lines) { 74 | int newWidth = 0; 75 | for (String line : lines) { 76 | if (line.contains("\n")) 77 | throw new IllegalArgumentException("Cannot use TextElement line constructor with newline characters."); 78 | int w = ChatMenuAPI.getWidth(line); 79 | if (w > newWidth) 80 | newWidth = w; 81 | } 82 | this.lines = lines; 83 | this.width = newWidth; 84 | } 85 | 86 | /** 87 | * Adds a border around the text 88 | * 89 | * @return this 90 | */ 91 | @NotNull 92 | public TextElement border() { 93 | this.border = true; 94 | return this; 95 | } 96 | 97 | /** 98 | * @param border whether there should be a border around the text 99 | */ 100 | public void setBorder(boolean border) { 101 | this.border = border; 102 | } 103 | 104 | /** 105 | * @return true if there is a border around the text 106 | */ 107 | public boolean isBordered() { 108 | return border; 109 | } 110 | 111 | /** 112 | * Sets the text alignment 113 | * 114 | * @param alignment the new text alignment 115 | * @return this 116 | */ 117 | @NotNull 118 | public TextElement align(@NotNull TextAlignment alignment) { 119 | setAlignment(alignment); 120 | return this; 121 | } 122 | 123 | /** 124 | * @return the current text alignment 125 | */ 126 | @NotNull 127 | public TextAlignment getAlignment() { 128 | return alignment; 129 | } 130 | 131 | /** 132 | * @param alignment the new text alignment 133 | */ 134 | public void setAlignment(@Nullable TextAlignment alignment) { 135 | this.alignment = alignment == null ? TextAlignment.LEFT : alignment; 136 | } 137 | 138 | /** 139 | * Sets the width of this element, excluding the border 140 | * 141 | * @param width the new width 142 | * @deprecated because the width can be set to less than the actual width of the text 143 | */ 144 | @Deprecated 145 | public void setWidth(int width) { 146 | this.width = width; 147 | } 148 | 149 | public int getWidth() { 150 | return border ? width + BORDER_WIDTH : width; 151 | } 152 | 153 | public int getHeight() { 154 | return border ? lines.length + 2 : lines.length; 155 | } 156 | 157 | public List render(IElementContainer context) { 158 | List result = new ArrayList<>(); 159 | if (alignment == TextAlignment.LEFT) { 160 | for (String lineString : lines) { 161 | if (border) { 162 | Text text = new Text("| "); 163 | text.append(lineString); 164 | text.expandToWidth(width + (BORDER_WIDTH / 2)); 165 | text.append(" |"); 166 | result.add(text); 167 | } else { 168 | result.add(new Text(lineString)); 169 | } 170 | } 171 | } else if (alignment == TextAlignment.CENTERED) { 172 | for (String lineString : lines) { 173 | Text current = new Text(lineString); 174 | int middle = width / 2 - current.getWidth() / 2; 175 | current = new Text(); 176 | current.expandToWidth(middle); 177 | current.append(lineString); 178 | if (border) { 179 | Text text = new Text("| "); 180 | text.append(current); 181 | text.expandToWidth(width + (BORDER_WIDTH / 2)); 182 | text.append(" |"); 183 | result.add(text); 184 | } else { 185 | result.add(current); 186 | } 187 | } 188 | } else if (alignment == TextAlignment.RIGHT) { 189 | for (String lineString : lines) { 190 | Text current = new Text(lineString); 191 | int middle = width - current.getWidth(); 192 | current = new Text(); 193 | current.expandToWidth(middle); 194 | current.append(lineString); 195 | if (border) { 196 | Text text = new Text("| "); 197 | text.append(current); 198 | text.expandToWidth(width + (BORDER_WIDTH / 2)); 199 | text.append(" |"); 200 | result.add(text); 201 | } else { 202 | result.add(current); 203 | } 204 | } 205 | } 206 | if (border) { 207 | String border = "+"; 208 | while (ChatMenuAPI.getWidth(border) < getWidth()) 209 | border += "-"; 210 | if (border.length() > 1) 211 | border = border.substring(0, border.length() - 1) + "+"; 212 | 213 | Text text = new Text(border); 214 | result.add(0, text); 215 | result.add(text); 216 | } 217 | return result; 218 | } 219 | 220 | public void edit(@NotNull IElementContainer container, @NotNull String[] args) { 221 | 222 | } 223 | 224 | public enum TextAlignment { 225 | LEFT, CENTERED, RIGHT 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/main/java/me/tom/sparse/spigot/chat/menu/element/VerticalSelectorElement.java: -------------------------------------------------------------------------------- 1 | package me.tom.sparse.spigot.chat.menu.element; 2 | 3 | import net.md_5.bungee.api.ChatColor; 4 | import net.md_5.bungee.api.chat.BaseComponent; 5 | import net.md_5.bungee.api.chat.ClickEvent; 6 | import net.md_5.bungee.api.chat.TextComponent; 7 | 8 | import org.jetbrains.annotations.NotNull; 9 | import org.jetbrains.annotations.Nullable; 10 | 11 | import java.util.ArrayList; 12 | import java.util.Collections; 13 | import java.util.List; 14 | 15 | import me.tom.sparse.spigot.chat.menu.ChatMenuAPI; 16 | import me.tom.sparse.spigot.chat.menu.IElementContainer; 17 | import me.tom.sparse.spigot.chat.util.State; 18 | import me.tom.sparse.spigot.chat.util.Text; 19 | 20 | /** 21 | * A vertical list of options. Similar to a column of radio buttons. 22 | */ 23 | public class VerticalSelectorElement extends Element { 24 | protected static final int SELECTED_PREFIX_WIDTH = ChatMenuAPI.getWidth("> "); 25 | 26 | @NotNull 27 | protected String[] options; 28 | protected int width; 29 | 30 | // protected int selectedIndex; 31 | @NotNull 32 | public final State value; 33 | 34 | @Nullable 35 | protected ChatColor selectedColor = ChatColor.GREEN; 36 | 37 | /** 38 | * Constructs a {@code VerticalSelectorElement} 39 | * 40 | * @param x the x coordinate 41 | * @param y the y coordinate 42 | * @param defaultSelected the selected option index 43 | * @param options the list of options. Options may not contain {@code \n} 44 | */ 45 | public VerticalSelectorElement(int x, int y, int defaultSelected, @NotNull String... options) { 46 | super(x, y); 47 | for (String option : options) { 48 | if (option.contains("\n")) 49 | throw new IllegalArgumentException("Option cannot contain newline"); 50 | 51 | int w = ChatMenuAPI.getWidth(option); 52 | if (w > width) 53 | width = w; 54 | } 55 | 56 | this.options = options; 57 | this.value = new State<>(defaultSelected, this::filter); 58 | } 59 | 60 | private int filter(int v) { 61 | return Math.max(Math.min(v, options.length), 0); 62 | } 63 | 64 | /** 65 | * @param selectedColor the new color for the selected element. Can be {@code null} 66 | */ 67 | public void setSelectedColor(@Nullable ChatColor selectedColor) { 68 | this.selectedColor = selectedColor; 69 | } 70 | 71 | /** 72 | * @return the color for the selected element 73 | */ 74 | @Nullable 75 | public ChatColor getSelectedColor() { 76 | return selectedColor; 77 | } 78 | 79 | /** 80 | * @param value the new selected option index 81 | */ 82 | public void setSelectedIndex(int value) { 83 | this.value.setCurrent(value); 84 | } 85 | 86 | /** 87 | * @return the currently selected option index 88 | */ 89 | public int getSelectedIndex() { 90 | return value.getOptionalCurrent().orElse(0); 91 | } 92 | 93 | /** 94 | * @return the currently selected option 95 | */ 96 | public String getSelectedOption() { 97 | int selectedIndex = getSelectedIndex(); 98 | return selectedIndex >= 0 && selectedIndex < options.length ? options[selectedIndex] : null; 99 | } 100 | 101 | public int getWidth() { 102 | return SELECTED_PREFIX_WIDTH + width; 103 | } 104 | 105 | public int getHeight() { 106 | return options.length; 107 | } 108 | 109 | public boolean isEnabled() { 110 | return true; 111 | } 112 | 113 | public List render(IElementContainer context) { 114 | String baseCommand = context.getCommand(this); 115 | 116 | List result = new ArrayList<>(); 117 | for (int i = 0; i < options.length; i++) { 118 | Text text = new Text(); 119 | BaseComponent[] components = TextComponent.fromLegacyText(options[i]); 120 | 121 | if (i == getSelectedIndex()) { 122 | text.append("> "); 123 | if (selectedColor != null) 124 | for (BaseComponent component : components) 125 | component.setColor(selectedColor); 126 | } else { 127 | text.expandToWidth(SELECTED_PREFIX_WIDTH); 128 | ClickEvent click = new ClickEvent(ClickEvent.Action.RUN_COMMAND, baseCommand + i); 129 | for (BaseComponent component : components) 130 | component.setClickEvent(click); 131 | } 132 | text.append(components); 133 | result.add(text); 134 | } 135 | return result; 136 | } 137 | 138 | public void edit(@NotNull IElementContainer container, @NotNull String[] args) { 139 | value.setCurrent(Integer.parseInt(args[0])); 140 | } 141 | 142 | @NotNull 143 | public List> getStates() { 144 | return Collections.singletonList(value); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/main/java/me/tom/sparse/spigot/chat/protocol/AbstractPacket.java: -------------------------------------------------------------------------------- 1 | /** 2 | * PacketWrapper - ProtocolLib wrappers for Minecraft packets Copyright (C) dmulloy2 Copyright (C) 3 | * Kristian S. Strangeland 4 | * 5 | * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public 6 | * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later 7 | * version. 8 | * 9 | * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied 10 | * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 11 | * details. 12 | * 13 | * You should have received a copy of the GNU General Public License along with this program. If not, see 14 | * . 15 | */ 16 | package me.tom.sparse.spigot.chat.protocol; 17 | 18 | import com.google.common.base.Objects; 19 | 20 | import com.comphenix.protocol.PacketType; 21 | import com.comphenix.protocol.ProtocolLibrary; 22 | import com.comphenix.protocol.events.PacketContainer; 23 | 24 | import java.lang.reflect.InvocationTargetException; 25 | 26 | import org.bukkit.entity.Player; 27 | 28 | public abstract class AbstractPacket { 29 | // The packet we will be modifying 30 | protected PacketContainer handle; 31 | 32 | /** 33 | * Constructs a new strongly typed wrapper for the given packet. 34 | * 35 | * @param handle - handle to the raw packet data. 36 | * @param type - the packet type. 37 | */ 38 | protected AbstractPacket(PacketContainer handle, PacketType type) { 39 | // Make sure we're given a valid packet 40 | if (handle == null) 41 | throw new IllegalArgumentException("Packet handle cannot be NULL."); 42 | if (!Objects.equal(handle.getType(), type)) 43 | throw new IllegalArgumentException(handle.getHandle() 44 | + " is not a packet of type " + type); 45 | 46 | this.handle = handle; 47 | } 48 | 49 | /** 50 | * Retrieve a handle to the raw packet data. 51 | * 52 | * @return Raw packet data. 53 | */ 54 | public PacketContainer getHandle() { 55 | return handle; 56 | } 57 | 58 | /** 59 | * Send the current packet to the given receiver. 60 | * 61 | * @param receiver - the receiver. 62 | * @throws RuntimeException If the packet cannot be sent. 63 | */ 64 | public void sendPacket(Player receiver) { 65 | try { 66 | ProtocolLibrary.getProtocolManager().sendServerPacket(receiver, 67 | getHandle()); 68 | } catch (InvocationTargetException e) { 69 | throw new RuntimeException("Cannot send packet.", e); 70 | } 71 | } 72 | 73 | /** 74 | * Simulate receiving the current packet from the given sender. 75 | * 76 | * @param sender - the sender. 77 | * @throws RuntimeException If the packet cannot be received. 78 | * @see #receivePacket(Player) 79 | * @deprecated Misspelled. recieve to receive 80 | */ 81 | @Deprecated 82 | public void recievePacket(Player sender) { 83 | try { 84 | ProtocolLibrary.getProtocolManager().recieveClientPacket(sender, 85 | getHandle()); 86 | } catch (Exception e) { 87 | throw new RuntimeException("Cannot recieve packet.", e); 88 | } 89 | } 90 | 91 | /** 92 | * Simulate receiving the current packet from the given sender. 93 | * 94 | * @param sender - the sender. 95 | * @throws RuntimeException if the packet cannot be received. 96 | */ 97 | public void receivePacket(Player sender) { 98 | try { 99 | ProtocolLibrary.getProtocolManager().recieveClientPacket(sender, 100 | getHandle()); 101 | } catch (Exception e) { 102 | throw new RuntimeException("Cannot receive packet.", e); 103 | } 104 | } 105 | } -------------------------------------------------------------------------------- /src/main/java/me/tom/sparse/spigot/chat/protocol/PlayerChatInterceptor.java: -------------------------------------------------------------------------------- 1 | package me.tom.sparse.spigot.chat.protocol; 2 | 3 | import com.comphenix.protocol.PacketType; 4 | import com.comphenix.protocol.ProtocolLibrary; 5 | import com.comphenix.protocol.events.PacketAdapter; 6 | import com.comphenix.protocol.events.PacketEvent; 7 | import com.comphenix.protocol.wrappers.EnumWrappers; 8 | import com.comphenix.protocol.wrappers.WrappedChatComponent; 9 | 10 | import net.md_5.bungee.api.chat.BaseComponent; 11 | import net.md_5.bungee.chat.ComponentSerializer; 12 | 13 | import java.lang.reflect.InvocationTargetException; 14 | import java.util.Map; 15 | import java.util.Queue; 16 | import java.util.UUID; 17 | import java.util.concurrent.ConcurrentHashMap; 18 | import java.util.concurrent.ConcurrentLinkedQueue; 19 | 20 | import org.bukkit.Bukkit; 21 | import org.bukkit.entity.Player; 22 | import org.bukkit.event.EventHandler; 23 | import org.bukkit.event.Listener; 24 | import org.bukkit.event.player.AsyncPlayerChatEvent; 25 | import org.bukkit.event.player.PlayerQuitEvent; 26 | import org.bukkit.plugin.Plugin; 27 | 28 | public class PlayerChatInterceptor implements Listener { 29 | 30 | private Map paused = new ConcurrentHashMap<>(); 31 | private Map> messageQueue = new ConcurrentHashMap<>(); 32 | private Map> allowedMessages = new ConcurrentHashMap<>(); 33 | 34 | public PlayerChatInterceptor(Plugin plugin) { 35 | Bukkit.getPluginManager().registerEvents(this, plugin); 36 | 37 | ProtocolLibrary.getProtocolManager().addPacketListener(new PacketAdapter(plugin, PacketType.Play.Server.CHAT) { 38 | 39 | @Override 40 | public void onPacketSending(PacketEvent event) { 41 | WrapperPlayServerChat chat = new WrapperPlayServerChat(event.getPacket()); 42 | 43 | BaseComponent[] spigot = chat.getHandle().getSpecificModifier(BaseComponent[].class).read(0); 44 | WrappedChatComponent msg; 45 | if (spigot != null) { 46 | msg = WrappedChatComponent.fromJson(ComponentSerializer.toString(spigot)); 47 | } else { 48 | msg = chat.getMessage(); 49 | } 50 | 51 | boolean allowed = isAllowed(event.getPlayer(), msg); 52 | boolean paused = isPaused(event.getPlayer()); 53 | if (!paused || !allowed) { 54 | Queue queue = messageQueue.computeIfAbsent(event.getPlayer().getUniqueId(), (uuid) -> new ConcurrentLinkedQueue<>()); 55 | while (queue.size() > 20) { 56 | queue.remove(); 57 | } 58 | 59 | queue.add(msg); 60 | } 61 | 62 | if (paused && !allowed) { 63 | event.setCancelled(true); 64 | } 65 | } 66 | }); 67 | } 68 | 69 | /** 70 | * Sends a message to the player associated with this, regardless of chat being paused. 71 | * 72 | * @param message the message to send 73 | */ 74 | public void sendMessage(Player player, BaseComponent... message) { 75 | if (isPaused(player)) { 76 | allowedMessages.computeIfAbsent(player.getUniqueId(), (uuid) -> new ConcurrentLinkedQueue<>()).add(WrappedChatComponent.fromJson(ComponentSerializer.toString(message))); 77 | } 78 | player.spigot().sendMessage(message); 79 | } 80 | 81 | public boolean isPaused(Player player) { 82 | return paused.getOrDefault(player.getUniqueId(), false); 83 | } 84 | 85 | public void pause(Player player) { 86 | if (isPaused(player)) return; 87 | paused.put(player.getUniqueId(), true); 88 | } 89 | 90 | public void resume(Player player) { 91 | if (!isPaused(player)) return; 92 | 93 | paused.put(player.getUniqueId(), false); 94 | 95 | int i = 0; 96 | // copy so that we don't catch new messages 97 | Queue queuedMessages = new ConcurrentLinkedQueue<>(messageQueue.getOrDefault(player.getUniqueId(), new ConcurrentLinkedQueue<>())); 98 | while (i < 20 - queuedMessages.size()) { 99 | i++; 100 | player.sendMessage(" "); 101 | } 102 | 103 | for (WrappedChatComponent components : queuedMessages) { 104 | WrapperPlayServerChat chat = new WrapperPlayServerChat(); 105 | chat.setMessage(components); 106 | chat.setChatType(EnumWrappers.ChatType.CHAT); 107 | try { 108 | ProtocolLibrary.getProtocolManager().sendServerPacket(player, chat.getHandle()); 109 | } catch (InvocationTargetException e) { 110 | e.printStackTrace(); 111 | } 112 | } 113 | 114 | allowedMessages.remove(player.getUniqueId()); 115 | messageQueue.remove(player.getUniqueId()); 116 | } 117 | 118 | public boolean isAllowed(Player player, WrappedChatComponent message) { 119 | return !isPaused(player) || allowedMessages.computeIfAbsent(player.getUniqueId(), (uuid) -> new ConcurrentLinkedQueue<>()).remove(message); 120 | } 121 | 122 | @EventHandler 123 | public void onPlayerQuit(PlayerQuitEvent e) { 124 | paused.remove(e.getPlayer().getUniqueId()); 125 | messageQueue.remove(e.getPlayer().getUniqueId()); 126 | } 127 | 128 | @EventHandler 129 | public void onChat(AsyncPlayerChatEvent e) { 130 | if (isPaused(e.getPlayer())) e.setCancelled(true); 131 | } 132 | 133 | public void disable() { 134 | paused.clear(); 135 | messageQueue.clear(); 136 | allowedMessages.clear(); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/main/java/me/tom/sparse/spigot/chat/protocol/WrapperPlayServerChat.java: -------------------------------------------------------------------------------- 1 | /** 2 | * PacketWrapper - ProtocolLib wrappers for Minecraft packets Copyright (C) dmulloy2 Copyright (C) 3 | * Kristian S. Strangeland 4 | * 5 | * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public 6 | * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later 7 | * version. 8 | * 9 | * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied 10 | * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 11 | * details. 12 | * 13 | * You should have received a copy of the GNU General Public License along with this program. If not, see 14 | * . 15 | */ 16 | package me.tom.sparse.spigot.chat.protocol; 17 | 18 | import com.comphenix.protocol.PacketType; 19 | import com.comphenix.protocol.events.PacketContainer; 20 | import com.comphenix.protocol.wrappers.EnumWrappers; 21 | import com.comphenix.protocol.wrappers.EnumWrappers.ChatType; 22 | import com.comphenix.protocol.wrappers.WrappedChatComponent; 23 | 24 | import java.util.Arrays; 25 | 26 | public class WrapperPlayServerChat extends AbstractPacket { 27 | public static final PacketType TYPE = PacketType.Play.Server.CHAT; 28 | 29 | public WrapperPlayServerChat() { 30 | super(new PacketContainer(TYPE), TYPE); 31 | handle.getModifier().writeDefaults(); 32 | } 33 | 34 | public WrapperPlayServerChat(PacketContainer packet) { 35 | super(packet, TYPE); 36 | } 37 | 38 | /** 39 | * Retrieve the chat message. 40 | *

41 | * Limited to 32767 bytes 42 | * 43 | * @return The current message 44 | */ 45 | public WrappedChatComponent getMessage() { 46 | return handle.getChatComponents().read(0); 47 | } 48 | 49 | /** 50 | * Set the message. 51 | * 52 | * @param value - new value. 53 | */ 54 | public void setMessage(WrappedChatComponent value) { 55 | handle.getChatComponents().write(0, value); 56 | } 57 | 58 | public ChatType getChatType() { 59 | return handle.getChatTypes().read(0); 60 | } 61 | 62 | public void setChatType(ChatType type) { 63 | handle.getChatTypes().write(0, type); 64 | } 65 | 66 | /** 67 | * Retrieve Position. 68 | *

69 | * Notes: 0 - Chat (chat box) ,1 - System Message (chat box), 2 - Above action bar 70 | * 71 | * @return The current Position 72 | * @deprecated Magic values replaced by enum 73 | */ 74 | @Deprecated 75 | public byte getPosition() { 76 | Byte position = handle.getBytes().readSafely(0); 77 | if (position != null) { 78 | return position; 79 | } else { 80 | return getChatType().getId(); 81 | } 82 | } 83 | 84 | /** 85 | * Set Position. 86 | * 87 | * @param value - new value. 88 | * @deprecated Magic values replaced by enum 89 | */ 90 | @Deprecated 91 | public void setPosition(byte value) { 92 | handle.getBytes().writeSafely(0, value); 93 | 94 | if (EnumWrappers.getChatTypeClass() != null) { 95 | Arrays.stream(ChatType.values()).filter(t -> t.getId() == value).findAny() 96 | .ifPresent(t -> handle.getChatTypes().writeSafely(0, t)); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/me/tom/sparse/spigot/chat/util/NumberFormat.java: -------------------------------------------------------------------------------- 1 | package me.tom.sparse.spigot.chat.util; 2 | 3 | public interface NumberFormat { 4 | NumberFormat NONE = (v, l) -> ""; 5 | NumberFormat FRACTION = (v, l) -> String.format("%d/%d", v + 1, l); 6 | NumberFormat PERCENTAGE = (v, l) -> String.format("%.1f%%", ((double) (v + 1) / l) * 100); 7 | 8 | String format(int value, int length); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/me/tom/sparse/spigot/chat/util/State.java: -------------------------------------------------------------------------------- 1 | package me.tom.sparse.spigot.chat.util; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | import java.util.Objects; 7 | import java.util.Optional; 8 | import java.util.function.Consumer; 9 | import java.util.function.Function; 10 | 11 | public class State { 12 | private Consumer> changeCallback; 13 | 14 | @NotNull 15 | private Function valueFilter; 16 | 17 | @Nullable 18 | private V current; 19 | @Nullable 20 | private V previous; 21 | 22 | /** 23 | * Constructs a new {@code State} with the provided value and the provided value filter. 24 | *
25 | * The value filter will replace the value every time {@link State#setCurrent} is called. 26 | * 27 | * @param current the starting value 28 | * @param valueFilter the filter for every value 29 | */ 30 | public State(@Nullable V current, @Nullable Function valueFilter) { 31 | this.valueFilter = valueFilter == null ? v -> v : valueFilter; 32 | this.current = this.valueFilter.apply(current); 33 | } 34 | 35 | /** 36 | * Constructs a new {@code State} with the provided value and no input filter. 37 | * 38 | * @param current the starting value 39 | */ 40 | public State(@Nullable V current) { 41 | this(current, v -> v); 42 | } 43 | 44 | /** 45 | * Sets the current value if the provided value is not {@link Object#equals} to the old one, then calls the {@code 46 | * changeCallback}. 47 | * 48 | * @param newValue the new value 49 | */ 50 | public void setCurrent(@Nullable V newValue) { 51 | newValue = valueFilter.apply(newValue); 52 | 53 | if (Objects.equals(newValue, this.current)) 54 | return; 55 | 56 | this.previous = this.current; 57 | this.current = newValue; 58 | 59 | if (changeCallback != null) 60 | changeCallback.accept(this); 61 | } 62 | 63 | /** 64 | * @return the current value as an {@link java.util.Optional} 65 | */ 66 | public Optional getOptionalCurrent() { 67 | return Optional.ofNullable(current); 68 | } 69 | 70 | /** 71 | * @return the previous value as an {@link java.util.Optional} 72 | */ 73 | public Optional getOptionalPrevious() { 74 | return Optional.ofNullable(previous); 75 | } 76 | 77 | /** 78 | * @return the current value. Might be {@code null}. 79 | */ 80 | @Nullable 81 | public V getCurrent() { 82 | return current; 83 | } 84 | 85 | /** 86 | * @return the getPrevious value. Might be {@code null}. 87 | */ 88 | @Nullable 89 | public V getPrevious() { 90 | return previous; 91 | } 92 | 93 | /** 94 | * Sets the change callback. Every time this {@code State} changes, the provided callback will be called. 95 | *
96 | * Replaces any previously setCurrent change callbacks. 97 | * 98 | * @param changeCallback the new change callback. 99 | */ 100 | public void setChangeCallback(@NotNull Consumer> changeCallback) { 101 | this.changeCallback = changeCallback; 102 | } 103 | 104 | public boolean equals(Object o) { 105 | if (this == o) return true; 106 | if (!(o instanceof State)) return false; 107 | 108 | State state = (State) o; 109 | 110 | return current != null ? current.equals(state.current) : state.current == null; 111 | } 112 | 113 | public int hashCode() { 114 | return current != null ? current.hashCode() : 0; 115 | } 116 | 117 | public String toString() { 118 | return "State{" + 119 | "current=" + current + 120 | ", previous=" + previous + 121 | '}'; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/me/tom/sparse/spigot/chat/util/Text.java: -------------------------------------------------------------------------------- 1 | package me.tom.sparse.spigot.chat.util; 2 | 3 | import net.md_5.bungee.api.chat.BaseComponent; 4 | import net.md_5.bungee.api.chat.TextComponent; 5 | 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | import java.util.ArrayList; 9 | import java.util.Arrays; 10 | import java.util.Collection; 11 | import java.util.Collections; 12 | import java.util.List; 13 | 14 | import me.tom.sparse.spigot.chat.menu.ChatMenuAPI; 15 | 16 | /** 17 | * BaseComponent[] wrapper with cached width 18 | */ 19 | public class Text { 20 | @NotNull 21 | protected List components = new ArrayList<>(); 22 | protected int width = 0; 23 | 24 | /** 25 | * Constructs an empty {@code Text} object with 0 width and no components. 26 | */ 27 | public Text() { 28 | 29 | } 30 | 31 | /** 32 | * ] Constructs a {@code Text} object with the provided text. 33 | * 34 | * @param text the starting text 35 | */ 36 | public Text(@NotNull String text) { 37 | if (text.contains("\n")) 38 | throw new IllegalArgumentException("Text cannot have newline characters"); 39 | Collections.addAll(components, TextComponent.fromLegacyText(text)); 40 | 41 | calculateWidth(); 42 | } 43 | 44 | /** 45 | * Constructs a {@code Text} object with the provided components 46 | * 47 | * @param components the starting components 48 | */ 49 | public Text(@NotNull BaseComponent... components) { 50 | this(Arrays.asList(components)); 51 | } 52 | 53 | /** 54 | * Constructs a {@code Text} object with the provided components 55 | * 56 | * @param components the starting components 57 | */ 58 | public Text(@NotNull Collection components) { 59 | this.components.addAll(components); 60 | if (toLegacyText().contains("\n")) 61 | throw new IllegalArgumentException("Text cannot have newline characters"); 62 | calculateWidth(); 63 | } 64 | 65 | /** 66 | * @return the cached width 67 | */ 68 | public int getWidth() { 69 | return width; 70 | } 71 | 72 | /** 73 | * Appends all of the components of the provided {@code Text} object to this. 74 | * 75 | * @param other the {@code Text} to append 76 | */ 77 | public void append(@NotNull Text other) { 78 | components.addAll(other.components); 79 | width += other.width; 80 | } 81 | 82 | /** 83 | * Converts the provided text from legacy text to components and appends it 84 | * 85 | * @param text the text to append 86 | */ 87 | public void append(@NotNull String text) { 88 | if (text.contains("\n")) 89 | throw new IllegalArgumentException("Text cannot have newline characters"); 90 | Collections.addAll(components, TextComponent.fromLegacyText(text)); 91 | calculateWidth(); 92 | } 93 | 94 | /** 95 | * Appends all of the provided components 96 | * 97 | * @param components the components to append 98 | */ 99 | public void append(@NotNull BaseComponent... components) { 100 | Collections.addAll(this.components, components); 101 | calculateWidth(); 102 | } 103 | 104 | /** 105 | * Appends spaces to the end such that the width is as close as possible to the target width 106 | *
107 | * The resulting width may be more or less than the target width by 1-2 pixels 108 | * 109 | * @param targetWidth the width to expand to 110 | */ 111 | public void expandToWidth(int targetWidth) { 112 | calculateWidth(); 113 | 114 | if (width >= targetWidth) 115 | return; 116 | 117 | components.add(new TextComponent(TextUtil.generateSpaces((int) Math.round((targetWidth - width) / 4.0)))); 118 | } 119 | 120 | /** 121 | * Appends spaces to the end such that the width is as close as possible to the target width without going over. 122 | *
123 | * The resulting width may be less than the target width 1-3 pixels 124 | * 125 | * @param targetWidth the width to expand to 126 | */ 127 | public void expandToWidthNoExceed(int targetWidth) { 128 | calculateWidth(); 129 | 130 | if (width >= targetWidth) 131 | return; 132 | 133 | components.add(new TextComponent(TextUtil.generateSpaces((int) Math.floor((targetWidth - width) / 4.0)))); 134 | } 135 | 136 | /** 137 | * Recalculates the width of all the components 138 | */ 139 | public void calculateWidth() { 140 | width = ChatMenuAPI.getWidth(toLegacyText()); 141 | } 142 | 143 | /** 144 | * @return the components of this {@code Text} object converted to legacy. 145 | */ 146 | @NotNull 147 | public String toLegacyText() { 148 | return TextComponent.toLegacyText(components.toArray(new BaseComponent[components.size()])); 149 | } 150 | 151 | /** 152 | * If you make any changes to this list, call {@link Text#calculateWidth()} 153 | * 154 | * @return the backing list of the components in this {@code Text} object. 155 | */ 156 | @NotNull 157 | public List getComponents() { 158 | return components; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/main/java/me/tom/sparse/spigot/chat/util/TextUtil.java: -------------------------------------------------------------------------------- 1 | package me.tom.sparse.spigot.chat.util; 2 | 3 | import java.util.Arrays; 4 | 5 | import me.tom.sparse.spigot.chat.menu.ChatMenuAPI; 6 | 7 | public final class TextUtil { 8 | public static String generateSpaces(int count) { 9 | return repeatCharacter(' ', count); 10 | } 11 | 12 | public static String repeatCharacter(char character, int count) { 13 | char[] chars = new char[count]; 14 | Arrays.fill(chars, character); 15 | return new String(chars); 16 | } 17 | 18 | public static String generateWidth(char character, int width, boolean canExceed) { 19 | int charWidth = ChatMenuAPI.getCharacterWidth(character); 20 | int count = (int) (canExceed ? Math.round(width / (double) charWidth) : Math.floor(width / (double) charWidth)); 21 | return repeatCharacter(character, count); 22 | } 23 | 24 | private TextUtil() { 25 | } 26 | } 27 | --------------------------------------------------------------------------------