├── .gitignore ├── src └── main │ ├── java │ └── com │ │ └── sentropic │ │ └── guiapi │ │ ├── gui │ │ ├── Alignment.java │ │ ├── AnonComponent.java │ │ ├── TemporaryGUIComponent.java │ │ ├── Font.java │ │ ├── GUIComponent.java │ │ └── GUI.java │ │ ├── command │ │ └── ReloadCommand.java │ │ ├── PacketManager.java │ │ ├── GUIManager.java │ │ ├── GUIAPI.java │ │ └── GUIConfig.java │ └── resources │ ├── config.yml │ └── plugin.yml ├── LICENSE ├── README.md └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | /*target/ 2 | /*.idea/ 3 | GUI-API.iml 4 | -------------------------------------------------------------------------------- /src/main/java/com/sentropic/guiapi/gui/Alignment.java: -------------------------------------------------------------------------------- 1 | package com.sentropic.guiapi.gui; 2 | 3 | public enum Alignment { 4 | LEFT, RIGHT, CENTER 5 | } 6 | -------------------------------------------------------------------------------- /src/main/resources/config.yml: -------------------------------------------------------------------------------- 1 | send_period_milis: 1900 2 | anon_period_ticks: 20 3 | anon_duration_ticks: 40 4 | debug: 5 | default: 6 | offset: 0 7 | text: "This text should start at the screen center" 8 | font: 9 | id: "minecraft:default" 10 | alignment: "left" 11 | test: 12 | offset: -10 13 | text: "Some text" 14 | font: 15 | id: "minecraft:default" 16 | height: 8 17 | alignment: "right" -------------------------------------------------------------------------------- /src/main/resources/plugin.yml: -------------------------------------------------------------------------------- 1 | name: GUIAPI 2 | version: ${project.version} 3 | main: com.sentropic.guiapi.GUIAPI 4 | api-version: 1.16 5 | softdepend: [ProtocolLib] 6 | authors: [Sentropic] 7 | commands: 8 | guiapi: 9 | description: Main plugin command 10 | usage: /guiapi reload, /guiapi debug 11 | permission: guiapi.command 12 | permissions: 13 | guiapi.command: 14 | description: Gives access to /guiapi command 15 | default: false -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Sentropic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/java/com/sentropic/guiapi/command/ReloadCommand.java: -------------------------------------------------------------------------------- 1 | package com.sentropic.guiapi.command; 2 | 3 | import com.sentropic.guiapi.GUIAPI; 4 | import com.sentropic.guiapi.gui.GUI; 5 | import org.bukkit.ChatColor; 6 | import org.bukkit.command.Command; 7 | import org.bukkit.command.CommandExecutor; 8 | import org.bukkit.command.CommandSender; 9 | import org.bukkit.entity.Player; 10 | import org.jetbrains.annotations.NotNull; 11 | 12 | public class ReloadCommand implements CommandExecutor { 13 | @Override 14 | public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { 15 | if (args.length == 1 && args[0].equals("reload")) { 16 | GUIAPI.getGUIConfig().reload(); 17 | sender.sendMessage(ChatColor.GREEN+"[GUI API] Reloaded config.yml"); 18 | return true; 19 | } else if (args.length == 1 && args[0].equals("debug")) { 20 | if (sender instanceof Player) { 21 | GUI gui = GUIAPI.getGUIManager().getGUI((Player) sender); 22 | gui.setDebug(!gui.isDebugging()); 23 | return true; 24 | } else { sender.sendMessage("Command only runnable in-game"); } 25 | } 26 | return false; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/sentropic/guiapi/gui/AnonComponent.java: -------------------------------------------------------------------------------- 1 | package com.sentropic.guiapi.gui; 2 | 3 | import com.sentropic.guiapi.GUIAPI; 4 | import net.md_5.bungee.api.chat.BaseComponent; 5 | import org.bukkit.scheduler.BukkitRunnable; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | public class AnonComponent extends GUIComponent { 9 | private RemoveTask task; 10 | private final GUI gui; 11 | 12 | AnonComponent(@NotNull BaseComponent component, @NotNull GUI gui) { 13 | super(GUI.ID_DEFAULT, component, 0, Alignment.CENTER, true); 14 | this.gui = gui; 15 | } 16 | 17 | void refresh() { 18 | if (task != null) { 19 | try { 20 | task.cancel(); 21 | } catch (IllegalArgumentException ignored) { } 22 | } 23 | task = new RemoveTask(); 24 | task.runTaskLater(GUIAPI.getPlugin(), GUIAPI.getGUIConfig().getAnonDuration()); 25 | } 26 | 27 | void cancelTask() { 28 | if (task != null) { 29 | try { 30 | task.cancel(); 31 | } catch (IllegalArgumentException ignored) { } 32 | } 33 | } 34 | 35 | private class RemoveTask extends BukkitRunnable { 36 | @Override 37 | public void run() { gui.removeAnonComponent(AnonComponent.this); } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GUI API 2 | Minecraft API plugin for display of visual elements in the player's screen, such as text, images or interface components. 3 | 4 | Requires the usage of the Negative Space Resource Pack by AmberW: 5 | 6 | https://www.spigotmc.org/threads/negative-space-font-resource-pack.440952/ 7 | 8 | Reminder that you can include AmberW's resource pack in your own resource pack (see the link for details). 9 | You can also set a server resource pack in server.properties, or by using a plugin such as ForceResourcePack: 10 | 11 | https://www.spigotmc.org/resources/forceresourcepack.6097/ 12 | 13 | ## For server owners: 14 | If you are a server owner, you might want to add this plugin to your server for mainly two purposes: 15 | 16 | ### Resolve conflicting action bars coming from different plugins 17 | GUI API will store the conflicting action bar texts and cycle through them, displaying them one by one at a configurable rate. 18 | 19 | ### Make depending plugins work! 20 | Through the use of GUI API, other plugins can achieve amazing visual effects, displaying information relevant to their functionality or gameplay. 21 | 22 | ## For developers: 23 | You can use GUI API as a dependency to display information and other visual elements, further customizing the looks of your plugin. 24 | Possible GUI Components include text in different places of the screen, and images added through a resource pack. 25 | You get to choose the position and order in which each individual GUI component is rendered. 26 | -------------------------------------------------------------------------------- /src/main/java/com/sentropic/guiapi/PacketManager.java: -------------------------------------------------------------------------------- 1 | package com.sentropic.guiapi; 2 | 3 | import com.comphenix.protocol.PacketType; 4 | import com.comphenix.protocol.ProtocolLibrary; 5 | import com.comphenix.protocol.ProtocolManager; 6 | import com.comphenix.protocol.events.PacketAdapter; 7 | import com.comphenix.protocol.events.PacketContainer; 8 | import com.comphenix.protocol.events.PacketEvent; 9 | import com.comphenix.protocol.utility.MinecraftVersion; 10 | import com.comphenix.protocol.wrappers.EnumWrappers; 11 | import com.sentropic.guiapi.gui.GUI; 12 | import net.md_5.bungee.chat.ComponentSerializer; 13 | import org.bukkit.plugin.Plugin; 14 | 15 | public class PacketManager { 16 | private final ProtocolManager protocolManager; 17 | private final PacketAdapter packetAdapter; 18 | 19 | PacketManager(Plugin plugin) { 20 | protocolManager = ProtocolLibrary.getProtocolManager(); 21 | if (MinecraftVersion.atOrAbove(new MinecraftVersion("1.17"))) { 22 | packetAdapter = new Manager_1_17(plugin); 23 | } else { 24 | packetAdapter = new Manager_1_16(plugin); 25 | } 26 | protocolManager.addPacketListener(packetAdapter); 27 | } 28 | 29 | public void disable() { protocolManager.removePacketListener(packetAdapter); } 30 | 31 | private static class Manager_1_17 extends PacketAdapter { 32 | public Manager_1_17(Plugin plugin) { 33 | super(plugin, PacketType.Play.Server.SET_ACTION_BAR_TEXT); 34 | } 35 | 36 | @Override 37 | public void onPacketSending(PacketEvent event) { 38 | if (event.isCancelled() || GUI.isSending()) { return; } 39 | boolean success = GUIAPI.getGUIManager().getGUI(event.getPlayer()).addAnonComponent( 40 | ComponentSerializer.parse(event.getPacket().getChatComponents().read(0).getJson())[0]); 41 | event.setCancelled(success); 42 | } 43 | } 44 | 45 | private static class Manager_1_16 extends PacketAdapter { 46 | public Manager_1_16(Plugin plugin) { 47 | super(plugin, PacketType.Play.Server.TITLE); 48 | } 49 | 50 | @Override 51 | public void onPacketSending(PacketEvent event) { 52 | if (event.isCancelled() || GUI.isSending()) { return; } 53 | PacketContainer packet = event.getPacket(); 54 | if (!packet.getTitleActions().read(0).equals(EnumWrappers.TitleAction.ACTIONBAR)) { return; } 55 | boolean success = GUIAPI.getGUIManager().getGUI(event.getPlayer()).addAnonComponent( 56 | ComponentSerializer.parse(packet.getChatComponents().read(0).getJson())[0]); 57 | event.setCancelled(success); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/sentropic/guiapi/GUIManager.java: -------------------------------------------------------------------------------- 1 | package com.sentropic.guiapi; 2 | 3 | import com.sentropic.guiapi.gui.GUI; 4 | import org.bukkit.Bukkit; 5 | import org.bukkit.entity.Player; 6 | import org.bukkit.event.EventHandler; 7 | import org.bukkit.event.EventPriority; 8 | import org.bukkit.event.Listener; 9 | import org.bukkit.event.player.PlayerJoinEvent; 10 | import org.bukkit.event.player.PlayerQuitEvent; 11 | import org.bukkit.metadata.FixedMetadataValue; 12 | import org.bukkit.scheduler.BukkitRunnable; 13 | 14 | import java.util.HashSet; 15 | import java.util.Set; 16 | 17 | /** 18 | * Class in charge of storing, accessing and updating the {@link Player}s' {@link GUI} 19 | * To access the instance used by the plugin, use {@link GUIAPI#getGUIManager()} 20 | */ 21 | public class GUIManager implements Listener { 22 | private static final String METADATA_KEY = "guiapi:gui"; 23 | 24 | final Set GUIS = new HashSet<>(); 25 | private final Task task = new Task(); 26 | 27 | /** 28 | * For internal use only. Use {@link GUIAPI#getGUIManager()} to get the instance used by the plugin 29 | */ 30 | public GUIManager() { 31 | task.runTaskTimer(GUIAPI.getPlugin(), 0, 1); 32 | Bukkit.getServer().getOnlinePlayers().forEach(this::createGUI); 33 | } 34 | 35 | /** 36 | * For internal use only. Used to clear the stored {@link GUI}s from memory and cancel GUI updating 37 | */ 38 | void disable() { 39 | try { 40 | task.cancel(); 41 | for (GUI gui : GUIS) { 42 | gui.getPlayer().removeMetadata(METADATA_KEY, GUIAPI.getPlugin()); 43 | } 44 | GUIS.clear(); 45 | } catch (IllegalStateException ignored) { } 46 | } 47 | 48 | /** 49 | * Gets the {@link GUI} of a given {@link Player} 50 | * 51 | * @param player the {@link Player} to get the {@link GUI} for 52 | * @return the existing {@link GUI} of the {@link Player} 53 | * @throws IllegalStateException if the given player is offline 54 | */ 55 | public GUI getGUI(Player player) { 56 | if (player.isOnline()) { 57 | return (GUI) player.getMetadata(METADATA_KEY).get(0).value(); 58 | } else { 59 | throw new IllegalStateException(); 60 | } 61 | } 62 | 63 | @EventHandler(priority = EventPriority.LOWEST) 64 | public void onPlayerLeave(PlayerQuitEvent event) { 65 | Player player = event.getPlayer(); 66 | GUIS.remove(this.getGUI(player)); 67 | player.removeMetadata(METADATA_KEY, GUIAPI.getPlugin()); 68 | } 69 | 70 | @EventHandler(priority = EventPriority.LOWEST) 71 | public void onPlayerJoin(PlayerJoinEvent event) { 72 | createGUI(event.getPlayer()); 73 | } 74 | 75 | private void createGUI(Player player) { 76 | GUI gui = new GUI(player); 77 | player.setMetadata(METADATA_KEY, new FixedMetadataValue(GUIAPI.getPlugin(), gui)); 78 | GUIS.add(gui); 79 | } 80 | 81 | private class Task extends BukkitRunnable { 82 | @Override 83 | public void run() { 84 | GUIS.forEach(GUI::play); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.sentropic.guiapi 8 | GUI_API 9 | 1.2.2 10 | jar 11 | 12 | GUI API 13 | 14 | 15 | 1.8 16 | UTF-8 17 | 18 | 19 | 20 | clean package 21 | 22 | 23 | org.apache.maven.plugins 24 | maven-compiler-plugin 25 | 3.7.0 26 | 27 | ${java.version} 28 | ${java.version} 29 | 30 | 31 | 32 | org.apache.maven.plugins 33 | maven-shade-plugin 34 | 3.1.0 35 | 36 | 37 | package 38 | 39 | shade 40 | 41 | 42 | false 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | src/main/resources 51 | true 52 | 53 | 54 | 55 | 56 | 57 | spigotmc-repo 58 | https://hub.spigotmc.org/nexus/content/repositories/snapshots/ 59 | 60 | 61 | md_5-public 62 | http://repo.md-5.net/content/groups/public/ 63 | 64 | 65 | jitpack.io 66 | https://jitpack.io 67 | 68 | 69 | 70 | 71 | org.spigotmc 72 | spigot-api 73 | 1.16.5-R0.1-SNAPSHOT 74 | provided 75 | 76 | 77 | com.github.dmulloy2 78 | ProtocolLib 79 | 4.7.0 80 | provided 81 | 82 | 83 | org.jetbrains 84 | annotations-java5 85 | 22.0.0 86 | provided 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /src/main/java/com/sentropic/guiapi/GUIAPI.java: -------------------------------------------------------------------------------- 1 | package com.sentropic.guiapi; 2 | 3 | import com.sentropic.guiapi.command.ReloadCommand; 4 | import com.sentropic.guiapi.gui.GUI; 5 | import org.bukkit.entity.Player; 6 | import org.bukkit.event.EventHandler; 7 | import org.bukkit.event.HandlerList; 8 | import org.bukkit.event.Listener; 9 | import org.bukkit.event.server.PluginDisableEvent; 10 | import org.bukkit.event.server.PluginEnableEvent; 11 | import org.bukkit.plugin.PluginManager; 12 | import org.bukkit.plugin.java.JavaPlugin; 13 | 14 | import java.util.Arrays; 15 | import java.util.Objects; 16 | 17 | public final class GUIAPI extends JavaPlugin implements Listener { 18 | private static GUIAPI singleton; 19 | private static GUIManager guiManager; 20 | private static GUIConfig config; 21 | private static PacketManager packetManager = null; 22 | 23 | @Override 24 | public void onEnable() { 25 | if (singleton != null) { throw new IllegalStateException(); } 26 | singleton = this; 27 | PluginManager pluginManager = getServer().getPluginManager(); 28 | pluginManager.registerEvents(this, this); 29 | 30 | guiManager = new GUIManager(); 31 | pluginManager.registerEvents(guiManager, this); 32 | 33 | saveDefaultConfig(); 34 | config = new GUIConfig(); 35 | 36 | // TODO add config for character widths 37 | Objects.requireNonNull(this.getCommand("guiapi")).setExecutor(new ReloadCommand()); 38 | 39 | if (Arrays.stream(pluginManager.getPlugins()).anyMatch( 40 | plugin -> plugin.getName().equals("ProtocolLib") && plugin.isEnabled())) { 41 | setProtocolLib(true); 42 | } 43 | 44 | } 45 | 46 | @Override 47 | public void onDisable() { 48 | HandlerList.unregisterAll(guiManager); 49 | guiManager.disable(); 50 | guiManager = null; 51 | config = null; 52 | 53 | setProtocolLib(false); 54 | 55 | HandlerList.unregisterAll((Listener) this); 56 | singleton = null; 57 | } 58 | 59 | @EventHandler 60 | public void onPluginEnable(PluginEnableEvent event) { 61 | if (event.getPlugin().getName().equals("ProtocolLib")) { 62 | setProtocolLib(true); 63 | } 64 | } 65 | 66 | @EventHandler 67 | public void onPluginDisable(PluginDisableEvent event) { 68 | if (event.getPlugin().getName().equals("ProtocolLib")) { 69 | setProtocolLib(false); 70 | } 71 | } 72 | 73 | private void setProtocolLib(boolean enabled) { 74 | if ((packetManager != null) == enabled) { return; } 75 | if (enabled) { 76 | packetManager = new PacketManager(this); 77 | } else { 78 | packetManager.disable(); 79 | packetManager = null; 80 | } 81 | } 82 | 83 | /** 84 | * Gets the singleton {@link GUIAPI} object loaded by the server 85 | * 86 | * @return the singleton {@link GUIAPI} object 87 | */ 88 | public static GUIAPI getPlugin() { return singleton; } 89 | 90 | /** 91 | * Gets the singleton {@link GUIManager} loaded by the plugin. Use this to access a {@link Player}s' {@link GUI} 92 | * 93 | * @return the {@link GUIManager} instance loaded by the plugin 94 | */ 95 | public static GUIManager getGUIManager() { return guiManager; } 96 | 97 | /** 98 | * Gets the singleton {@link GUIConfig} loaded by the plugin 99 | * 100 | * @return the {@link GUIConfig} instance loaded by the plugin 101 | */ 102 | public static GUIConfig getGUIConfig() { return config; } 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/com/sentropic/guiapi/gui/TemporaryGUIComponent.java: -------------------------------------------------------------------------------- 1 | package com.sentropic.guiapi.gui; 2 | 3 | import com.sentropic.guiapi.GUIAPI; 4 | import net.md_5.bungee.api.chat.BaseComponent; 5 | import org.bukkit.scheduler.BukkitRunnable; 6 | 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | /** 11 | * A GUIComponent set to last only for a certain duration, after which, it will be removed from the {@link GUI} 12 | */ 13 | @SuppressWarnings("unused") 14 | public class TemporaryGUIComponent extends GUIComponent { 15 | private final int duration; 16 | private final Map tasks = new HashMap<>(); 17 | 18 | /** 19 | * @param id the identifier of this GUIComponent 20 | * @param component the {@link BaseComponent} containing the text and formatting of this GUIComponent 21 | * @param offset the lateral offset of this GUIComponent in the player's screen 22 | * @param alignment the alignment to set this GUIComponent to, 23 | * whether {@link Alignment#LEFT}, {@link Alignment#RIGHT} or {@link Alignment#CENTER} 24 | * @param scale whether the text in the provided {@link BaseComponent} should be scaled according to its fonts 25 | * @param duration the duration this component will last for in the {@link GUI}, in ticks 26 | * @throws IllegalArgumentException if the provided {@link BaseComponent} is not supported, 27 | * according to the criteria of {@link GUIComponent#check(BaseComponent)} 28 | */ 29 | public TemporaryGUIComponent(String id, BaseComponent component, int offset, Alignment alignment, boolean scale, int duration) { 30 | super(id, component, offset, alignment, scale); 31 | this.duration = duration; 32 | } 33 | 34 | /** 35 | * @param id the identifier of this GUIComponent 36 | * @param component the {@link BaseComponent} containing the text and formatting of this GUIComponent 37 | * @param width the total width of the provided {@link BaseComponent} in the player's screen 38 | * @param offset the lateral offset of this GUIComponent in the player's screen 39 | * @param alignment the alignment to set this GUIComponent to, 40 | * whether {@link Alignment#LEFT}, {@link Alignment#RIGHT} or {@link Alignment#CENTER} 41 | * @param duration the duration this component will last for in the {@link GUI}, in ticks 42 | * @throws IllegalArgumentException if the provided {@link BaseComponent} is not supported, 43 | * according to the criteria of {@link GUIComponent#check(BaseComponent)} 44 | */ 45 | public TemporaryGUIComponent(String id, BaseComponent component, int width, int offset, Alignment alignment, int duration) { 46 | super(id, component, width, offset, alignment); 47 | this.duration = duration; 48 | } 49 | 50 | 51 | @Override 52 | void onAdd(GUI gui) { 53 | RemoveTask task = new RemoveTask(gui); 54 | RemoveTask oldTask = tasks.put(gui, task); 55 | if (oldTask != null) { 56 | try { oldTask.cancel(); } catch (IllegalStateException ignore) { } 57 | } 58 | task.runTaskLater(GUIAPI.getPlugin(), duration); 59 | } 60 | 61 | @Override 62 | void onRemove(GUI gui) { 63 | RemoveTask task = tasks.remove(gui); 64 | if (task != null) { 65 | try { task.cancel(); } catch (IllegalStateException ignore) { } 66 | } 67 | } 68 | 69 | private class RemoveTask extends BukkitRunnable { 70 | private final GUI gui; 71 | 72 | private RemoveTask(GUI gui) { this.gui = gui; } 73 | 74 | @Override 75 | public void run() { gui.remove(getID()); } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/com/sentropic/guiapi/GUIConfig.java: -------------------------------------------------------------------------------- 1 | package com.sentropic.guiapi; 2 | 3 | import com.sentropic.guiapi.gui.Alignment; 4 | import com.sentropic.guiapi.gui.Font; 5 | import com.sentropic.guiapi.gui.GUI; 6 | import com.sentropic.guiapi.gui.GUIComponent; 7 | import net.md_5.bungee.api.chat.TextComponent; 8 | import org.bukkit.configuration.ConfigurationSection; 9 | import org.bukkit.configuration.file.FileConfiguration; 10 | 11 | import java.util.ArrayList; 12 | import java.util.Collections; 13 | import java.util.List; 14 | import java.util.Objects; 15 | 16 | /** 17 | * Represents the config options of the plugin 18 | * For efficiency, use instead of directly getting the config file from the plugin 19 | */ 20 | public class GUIConfig { 21 | private int sendPeriod; 22 | private int anonPeriod; 23 | private int anonDuration; 24 | private final List debugComponents = new ArrayList<>(); 25 | private final List debugComponentsRead = Collections.unmodifiableList(debugComponents); 26 | 27 | GUIConfig() { reload(); } 28 | 29 | /** 30 | * Reloads the plugin's config and stores all its values 31 | */ 32 | public void reload() { 33 | GUIAPI plugin = GUIAPI.getPlugin(); 34 | plugin.reloadConfig(); 35 | FileConfiguration configFile = plugin.getConfig(); 36 | sendPeriod = configFile.getInt("send_period_milis", 1900); 37 | anonPeriod = configFile.getInt("anon_period_ticks", 20); 38 | anonDuration = configFile.getInt("anon_duration_ticks", 40); 39 | 40 | // Debug components 41 | { 42 | debugComponents.clear(); 43 | ConfigurationSection debugSection = configFile.getConfigurationSection("debug"); 44 | if (debugSection != null) { 45 | for (String componentKey : debugSection.getKeys(false)) { 46 | try { 47 | ConfigurationSection componentSection = Objects.requireNonNull(debugSection.getConfigurationSection(componentKey)); 48 | ConfigurationSection fontSection = Objects.requireNonNull(componentSection.getConfigurationSection("font")); 49 | 50 | String id = GUI.ID_DEBUG+componentKey; 51 | int offset = componentSection.getInt("offset", 0); 52 | String text = Objects.requireNonNull(componentSection.getString("text")); 53 | int width = componentSection.getInt("width", -1); 54 | 55 | String fontID = Objects.requireNonNull(fontSection.getString("id")); 56 | Font font = Font.getRegistered(fontID); 57 | if (font == null) { font = new Font(fontID, fontSection.getInt("height", 8)); } 58 | Alignment alignment = Alignment.valueOf( 59 | Objects.requireNonNull(componentSection.getString("alignment")).toUpperCase()); 60 | boolean scale = componentSection.getBoolean("scale", true); 61 | 62 | TextComponent textComponent = GUIComponent.createTextComponent(text, font.getID()); 63 | GUIComponent component; 64 | if (width == -1) { 65 | component = new GUIComponent(id, textComponent, font.getWidth(text, scale), offset, alignment); 66 | } else { 67 | component = new GUIComponent(id, textComponent, width, offset, alignment); 68 | } 69 | debugComponents.add(component); 70 | } catch (NullPointerException | IllegalArgumentException ignored) { } 71 | } 72 | } 73 | GUIAPI.getGUIManager().GUIS.forEach(GUI::onReload); 74 | } 75 | } 76 | 77 | /** 78 | * @return the maximum delay between sending GUIs to players, in milisecons 79 | */ 80 | public int getSendPeriod() { return sendPeriod; } 81 | 82 | /** 83 | * Gets the amount of ticks that each anonymous action bar text (sent without the usage of GUIAPI) 84 | * is shown in the GUI before switching to a different one, if more than one is available 85 | * 86 | * @return the period at which anonymous action bar texts are cycled around in the GUI 87 | */ 88 | public int getAnonPeriod() { return anonPeriod; } 89 | 90 | /** 91 | * Gets the amount of ticks that each anonymous action bar text (sent without the usage of GUIAPI) 92 | * remains in memory before being removed (defaults to 40 in vanilla Minecraft) 93 | * 94 | * @return the duration of anonymous action bars 95 | */ 96 | public int getAnonDuration() { return anonDuration; } 97 | 98 | /** 99 | * Gets the debug {@link GUIComponent}s defined in the config 100 | * 101 | * @return a {@link List} containing the debug {@link GUIComponent}s defined in the config, in typing order 102 | */ 103 | public List getDebugComponents() { return debugComponentsRead; } 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/com/sentropic/guiapi/gui/Font.java: -------------------------------------------------------------------------------- 1 | package com.sentropic.guiapi.gui; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | /** 10 | * Represents a resource pack font, containing its ID and character widths 11 | */ 12 | public class Font { 13 | 14 | // Static code 15 | 16 | public static final Font DEFAULT = new Font("minecraft:default", 8); 17 | private static final Map registeredFonts = new HashMap<>(); 18 | 19 | static { 20 | // Register default char widths (width = horizontal pixels + 1) 21 | DEFAULT.registerWidth(' ', 4); 22 | DEFAULT.registerWidth('f', 5); 23 | DEFAULT.registerWidth('i', 2); 24 | DEFAULT.registerWidth('k', 5); 25 | DEFAULT.registerWidth('l', 3); 26 | DEFAULT.registerWidth('t', 4); 27 | DEFAULT.registerWidth('I', 4); 28 | DEFAULT.registerWidth('í', 3); 29 | DEFAULT.registerWidth('Í', 4); 30 | DEFAULT.registerWidth('´', 3); 31 | DEFAULT.registerWidth('.', 2); 32 | DEFAULT.registerWidth(',', 2); 33 | DEFAULT.registerWidth(';', 2); 34 | DEFAULT.registerWidth(':', 2); 35 | DEFAULT.registerWidth('[', 4); 36 | DEFAULT.registerWidth(']', 4); 37 | DEFAULT.registerWidth('{', 4); 38 | DEFAULT.registerWidth('}', 4); 39 | DEFAULT.registerWidth('*', 4); 40 | DEFAULT.registerWidth('!', 2); 41 | DEFAULT.registerWidth('¡', 2); 42 | DEFAULT.registerWidth('"', 4); 43 | DEFAULT.registerWidth('(', 4); 44 | DEFAULT.registerWidth(')', 4); 45 | DEFAULT.registerWidth('°', 5); 46 | DEFAULT.registerWidth('|', 2); 47 | DEFAULT.registerWidth('`', 3); 48 | DEFAULT.registerWidth('\'', 2); 49 | DEFAULT.registerWidth('<', 5); 50 | DEFAULT.registerWidth('>', 5); 51 | DEFAULT.registerWidth('@', 7); 52 | DEFAULT.registerWidth('~', 7); 53 | 54 | for (Map.Entry entry : GUI.POS_SPACES.entrySet()) { 55 | DEFAULT.registerWidth(entry.getValue(), entry.getKey()); 56 | } 57 | for (Map.Entry entry : GUI.NEG_SPACES.entrySet()) { 58 | DEFAULT.registerWidth(entry.getValue(), -entry.getKey()); 59 | } 60 | 61 | register(DEFAULT); 62 | } 63 | 64 | /** 65 | * Gets a registered font by its ID 66 | * 67 | * @param id the id of the font 68 | * @return the font for the given ID, or null if was not found 69 | */ 70 | @Nullable 71 | public static Font getRegistered(String id) { return registeredFonts.get(id); } 72 | 73 | /** 74 | * Registers a Font to be accessed statically later, through {@link Font#getRegistered(String)} 75 | * 76 | * @param font the Font to be registered 77 | * @throws IllegalArgumentException if a font with the same ID already is registered 78 | */ 79 | public static void register(Font font) { 80 | String id = font.getID(); 81 | if (registeredFonts.containsKey(id)) { 82 | throw new IllegalArgumentException("Font \""+id+"\" is already registered"); 83 | } 84 | registeredFonts.put(id, font); 85 | } 86 | 87 | /** 88 | * Unregisters a Font from the static context 89 | * 90 | * @param font the font to unregister 91 | * @return true if the font was unregistered, false if it was not previously registered 92 | */ 93 | @SuppressWarnings("unused") 94 | public static boolean unregister(Font font) { 95 | boolean success = false; 96 | if (registeredFonts != null) { success = registeredFonts.remove(font.toString()) != null; } 97 | return success; 98 | } 99 | 100 | // Instance code 101 | 102 | private final String id; 103 | private final int height; 104 | private Font parent; 105 | private Map widths; 106 | 107 | /** 108 | * @param id the namespaced ID of the font, as used by the resource pack (i.e. "minecraft:default") 109 | * @param height the default height of the characters in the font, as specified in the resource pack 110 | */ 111 | public Font(@NotNull String id, int height) { 112 | this.id = id; 113 | this.height = height; 114 | } 115 | 116 | /** 117 | * Creates a font that inherits its character widths from a parent font 118 | * Used for fonts that share the same textures with another one 119 | * 120 | * @param id the namespaced ID of the font, as used by the resource pack (i.e. "minecraft:default") 121 | * @param height the default height of the characters in the font, as specified in the resource pack 122 | * @param parent the font to inherit its character widths from 123 | */ 124 | public Font(@NotNull String id, int height, Font parent) { 125 | this.id = id; 126 | this.height = height; 127 | this.parent = parent; 128 | } 129 | 130 | /** 131 | * Registers the width of a character for this font, if different from the default of 6 132 | * 133 | * @param character the character to register the width for 134 | * @param width the width of the character 135 | */ 136 | public void registerWidth(char character, int width) { 137 | if (widths == null) { widths = new HashMap<>(); } 138 | widths.put(character, width); 139 | } 140 | 141 | /** 142 | * Gets the width of a given character for this font 143 | * 144 | * @param character the character to get the width for 145 | * @param scale whether to scale the width according to the font's height 146 | * @return the width of the character 147 | */ 148 | public int getWidth(char character, boolean scale) { 149 | Integer result = null; 150 | if (this == DEFAULT) { result = widths.getOrDefault(character, 6); } else { 151 | try { result = widths.get(character); } catch (NullPointerException ignored) { } 152 | if (result == null) { 153 | result = parent == null ? 154 | DEFAULT.getWidth(character, false) : 155 | parent.getWidth(character, false); 156 | } 157 | } 158 | if (scale && this != DEFAULT && character != ' ') { 159 | // Formula figured out experimentally (pain) 160 | result = (int) Math.round(1.1249999d+(result-1)*height/8d); 161 | } 162 | return result; 163 | } 164 | 165 | /** 166 | * Calculates the width of a given {@link String} for this font 167 | * 168 | * @param text the String to calculate the width for 169 | * @param scale whether to scale the width according to the font's height 170 | * @return the calculated width of the character 171 | */ 172 | public int getWidth(String text, boolean scale) { 173 | int total = 0; 174 | for (Character character : text.toCharArray()) { total += getWidth(character, scale); } 175 | return total; 176 | } 177 | 178 | /** 179 | * @return the namespaced ID of the font, as used by the resource pack (i.e. "minecraft:default") 180 | */ 181 | @Override 182 | public String toString() { return id; } 183 | 184 | /** 185 | * @return the namespaced ID of the font, as used by the resource pack (i.e. "minecraft:default") 186 | */ 187 | public String getID() { return id; } 188 | 189 | /** 190 | * @return the default height of the characters in the font, as specified in the resource pack 191 | */ 192 | @SuppressWarnings("unused") 193 | public int getHeight() { return height; } 194 | } 195 | -------------------------------------------------------------------------------- /src/main/java/com/sentropic/guiapi/gui/GUIComponent.java: -------------------------------------------------------------------------------- 1 | package com.sentropic.guiapi.gui; 2 | 3 | import net.md_5.bungee.api.chat.BaseComponent; 4 | import net.md_5.bungee.api.chat.TextComponent; 5 | import org.jetbrains.annotations.NotNull; 6 | import org.jetbrains.annotations.Nullable; 7 | 8 | import java.util.HashSet; 9 | import java.util.List; 10 | import java.util.Set; 11 | 12 | public class GUIComponent { 13 | private static final Set> validComponents = new HashSet>() {{ 14 | add(TextComponent.class); 15 | // TODO see if can include Score and Selector 16 | // TODO add support for bold text 17 | }}; 18 | 19 | protected final String id; 20 | protected final BaseComponent component; 21 | protected final int left; 22 | protected final int right; 23 | 24 | /** 25 | * @param id the identifier of this GUIComponent 26 | * @param component the {@link BaseComponent} containing the text and formatting of this GUIComponent 27 | * @param offset the lateral offset of this GUIComponent in the player's screen 28 | * @param alignment the alignment to set this GUIComponent to, 29 | * whether {@link Alignment#LEFT}, {@link Alignment#RIGHT} or {@link Alignment#CENTER} 30 | * @param scale whether the text in the provided {@link BaseComponent} should be scaled according to its fonts 31 | * @throws IllegalArgumentException if the provided {@link BaseComponent} is not supported, 32 | * according to the criteria of {@link GUIComponent#check(BaseComponent)} 33 | */ 34 | public GUIComponent(String id, BaseComponent component, int offset, Alignment alignment, boolean scale) { 35 | this(id, component, getWidth(component, scale), offset, alignment); 36 | } 37 | 38 | /** 39 | * @param id the identifier of this GUIComponent 40 | * @param component the {@link BaseComponent} containing the text and formatting of this GUIComponent 41 | * @param width the total width of the provided {@link BaseComponent} in the player's screen 42 | * @param offset the lateral offset of this GUIComponent in the player's screen 43 | * @param alignment the alignment to set this GUIComponent to, 44 | * whether {@link Alignment#LEFT}, {@link Alignment#RIGHT} or {@link Alignment#CENTER} 45 | * @throws IllegalArgumentException if the provided {@link BaseComponent} is not supported, 46 | * according to the criteria of {@link GUIComponent#check(BaseComponent)} 47 | */ 48 | public GUIComponent(String id, BaseComponent component, int width, int offset, Alignment alignment) { 49 | check(component); 50 | this.id = id; 51 | this.component = component.duplicate(); 52 | int[] spaces = spacesFor(width, offset, alignment); 53 | left = spaces[0]; 54 | right = spaces[1]; 55 | } 56 | 57 | /** 58 | * Checks whether the given {@link BaseComponent} is supported 59 | * (contains components only of type {@link TextComponent} and non-bold text) 60 | * 61 | * @param component the component to be checked 62 | */ 63 | public static void check(BaseComponent component) { 64 | if (component.isBold() || !validComponents.contains(component.getClass())) { 65 | throw new IllegalArgumentException(); 66 | } 67 | List extras = component.getExtra(); 68 | if (extras != null) { 69 | for (BaseComponent extra : extras) { 70 | if (extra == component) { throw new IllegalArgumentException(); } 71 | check(extra); 72 | } 73 | } 74 | } 75 | 76 | /** 77 | * Calculates the width the given {@link BaseComponent} would have in-screen 78 | * 79 | * @param component the component to calculate the width for 80 | * @param scale whether the component's text should be scaled according to its fonts 81 | * @return the calculated width the component would have in-screen 82 | */ 83 | public static int getWidth(BaseComponent component, boolean scale) { 84 | int result = 0; 85 | Font font = Font.getRegistered(component.getFont()); 86 | if (font == null) { font = Font.DEFAULT; } 87 | if (component instanceof TextComponent) { 88 | TextComponent textComponent = (TextComponent) component; 89 | result += font.getWidth(textComponent.getText(), scale); 90 | } 91 | List extras = component.getExtra(); 92 | if (extras != null) { 93 | for (BaseComponent extra : extras) { 94 | result += getWidth(extra, scale); 95 | } 96 | } 97 | return result; 98 | } 99 | 100 | /** 101 | * Calculates the amount of space to be placed before and after a {@link GUIComponent}, 102 | * given its width, lateral offset and {@link Alignment} 103 | * 104 | * @param width the width of the component 105 | * @param offset the lateral offset of the component 106 | * @param alignment the alignment of the component, 107 | * whether {@link Alignment#LEFT}, {@link Alignment#RIGHT} or {@link Alignment#CENTER} 108 | * @return an 2 int array containing the left side spaces in its first index, and the right side spaces in its second 109 | */ 110 | public static int[] spacesFor(int width, int offset, Alignment alignment) { 111 | int[] result = new int[2]; 112 | switch (alignment) { 113 | case LEFT: 114 | result[0] = offset; 115 | result[1] = -offset-width; 116 | break; 117 | case RIGHT: 118 | result[0] = offset-width; 119 | result[1] = -offset; 120 | break; 121 | case CENTER: 122 | result[0] = offset-width/2; 123 | result[1] = -result[0]-width; 124 | break; 125 | default: 126 | throw new IllegalArgumentException(); 127 | } 128 | return result; 129 | } 130 | 131 | /** 132 | * Gets the ID of this component 133 | * 134 | * @return the {@link String} representing the component's ID 135 | */ 136 | public String getID() { return id; } 137 | 138 | /** 139 | * For internal ose only. Must not be modified 140 | * 141 | * @return the {@link BaseComponent} contained by this GUIComponent 142 | */ 143 | BaseComponent getComponent() { return component; } 144 | 145 | /** 146 | * @return the space to be placed before this component 147 | */ 148 | public int getLeftSpaces() { return left; } 149 | 150 | /** 151 | * @return the space to be placed after this component 152 | */ 153 | public int getRightSpaces() { return right; } 154 | 155 | void onAdd(GUI gui) { } 156 | 157 | void onRemove(GUI gui) { } 158 | 159 | /** 160 | * Creates a text component using the given text and font. 161 | * Use inside the GUIComponent constructor to create it with a single statement. 162 | * 163 | * @param text the text contained by the TextComponent 164 | * @param font the namespaced font ID to use for the text, as used in the resource pack 165 | * @return a new TextComponent with the provided text and font 166 | */ 167 | @NotNull 168 | public static TextComponent createTextComponent(@Nullable String text, @Nullable String font) { 169 | if (text == null) { text = ""; } 170 | TextComponent textComponent = new TextComponent(text); 171 | textComponent.setFont(font); 172 | return textComponent; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/main/java/com/sentropic/guiapi/gui/GUI.java: -------------------------------------------------------------------------------- 1 | package com.sentropic.guiapi.gui; 2 | 3 | import com.sentropic.guiapi.GUIAPI; 4 | import com.sentropic.guiapi.GUIManager; 5 | import net.md_5.bungee.api.ChatMessageType; 6 | import net.md_5.bungee.api.chat.BaseComponent; 7 | import net.md_5.bungee.api.chat.TextComponent; 8 | import org.bukkit.entity.Player; 9 | import org.bukkit.scheduler.BukkitRunnable; 10 | import org.jetbrains.annotations.NotNull; 11 | 12 | import java.util.*; 13 | import java.util.function.Predicate; 14 | 15 | /** 16 | * Represents the GUI of a specific {@link Player}, and all the visual components that make it up 17 | */ 18 | public class GUI { 19 | 20 | // Storage 21 | 22 | public static final String ID_DEFAULT = "_default_"; 23 | public static final String ID_DEBUG = "debug:"; 24 | private static final GUIComponent defaultComponent = 25 | new GUIComponent(ID_DEFAULT, new TextComponent(), 0, Alignment.LEFT, false); 26 | 27 | public static final SortedMap POS_SPACES = Collections.unmodifiableSortedMap(new TreeMap() {{ 28 | put(1024, '\uF82F'); 29 | put(512, '\uF82E'); 30 | put(256, '\uF82D'); 31 | put(128, '\uF82C'); 32 | put(64, '\uF82B'); 33 | put(32, '\uF82A'); 34 | put(16, '\uF829'); 35 | put(8, '\uF828'); 36 | put(7, '\uF827'); 37 | put(6, '\uF826'); 38 | put(5, '\uF825'); 39 | put(4, '\uF824'); 40 | put(3, '\uF823'); 41 | put(2, '\uF822'); 42 | put(1, '\uF821'); 43 | }}); 44 | public static final SortedMap NEG_SPACES = Collections.unmodifiableSortedMap(new TreeMap() {{ 45 | put(1, '\uF801'); 46 | put(2, '\uF802'); 47 | put(3, '\uF803'); 48 | put(4, '\uF804'); 49 | put(5, '\uF805'); 50 | put(6, '\uF806'); 51 | put(7, '\uF807'); 52 | put(8, '\uF808'); 53 | put(16, '\uF809'); 54 | put(32, '\uF80A'); 55 | put(64, '\uF80B'); 56 | put(128, '\uF80C'); 57 | put(256, '\uF80D'); 58 | put(512, '\uF80E'); 59 | put(1024, '\uF80F'); 60 | }}); 61 | 62 | // API code 63 | 64 | private final Player player; 65 | private final List guiComponents = new ArrayList<>(); 66 | 67 | 68 | /** 69 | * Important: If you need to get a player's GUI, use {@link GUIManager#getGUI(Player)}, 70 | * since GUIs created through this constructor will not be processed. 71 | * 72 | * @param player the player the GUI belongs to. 73 | */ 74 | @Deprecated 75 | public GUI(Player player) { 76 | this.player = player; 77 | guiComponents.add(defaultComponent); 78 | } 79 | 80 | /** 81 | * Gets the owner of this GUI 82 | * 83 | * @return the {@link Player} to whom this GUI belongs to 84 | */ 85 | @SuppressWarnings("unused") 86 | public Player getPlayer() { return player; } 87 | 88 | // Editing methods 89 | 90 | /** 91 | * Adds the given {@link GUIComponent} to the GUI after all previously added GUIComponents, 92 | * and removes any other component with the same ID as the given one 93 | * 94 | * @param component the GUIComponent to add to the GUI 95 | */ 96 | public void putOnTop(@NotNull GUIComponent component) { 97 | remove(component.getID()); 98 | guiComponents.add(component); 99 | changed = true; 100 | component.onAdd(this); 101 | } 102 | 103 | /** 104 | * Adds the given {@link GUIComponent} to the GUI before all previously added GUIComponents, 105 | * and removes any other component with the same ID as the given one 106 | * 107 | * @param component the GUIComponent to add to the GUI 108 | */ 109 | @SuppressWarnings("unused") 110 | public void putUnderneath(@NotNull GUIComponent component) { 111 | remove(component.getID()); 112 | guiComponents.add(0, component); 113 | changed = true; 114 | component.onAdd(this); 115 | } 116 | 117 | /** 118 | * If a {@link GUIComponent} exists with an ID matching the one of the given component, 119 | * removes it and puts the given one in its place 120 | * 121 | * @param component the GUIComponent to add to the GUI 122 | * @return whether the component could be added 123 | */ 124 | @SuppressWarnings("unused") 125 | public boolean update(@NotNull GUIComponent component) { 126 | boolean success = false; 127 | String id = component.getID(); 128 | checkID(id); 129 | for (ListIterator iterator = guiComponents.listIterator(); iterator.hasNext(); ) { 130 | GUIComponent oldComponent = iterator.next(); 131 | if (oldComponent.getID().equals(id)) { 132 | oldComponent.onRemove(this); 133 | iterator.set(component); 134 | component.onAdd(this); 135 | changed = true; 136 | success = true; 137 | break; 138 | } 139 | } 140 | return success; 141 | } 142 | 143 | /** 144 | * If a {@link GUIComponent} exists with an ID matching the given id, 145 | * adds the provided component after it, and removes any other component with the same ID as it 146 | * 147 | * @param component the GUIComponent to add to the GUI 148 | * @return whether the component could be added 149 | */ 150 | @SuppressWarnings("unused") 151 | public boolean putAfter(String id, @NotNull GUIComponent component) { 152 | boolean success = false; 153 | remove(component.getID()); 154 | int i = 0; 155 | for (GUIComponent component1 : guiComponents) { 156 | i++; 157 | if (component1.getID().equals(id)) { 158 | guiComponents.add(i, component); 159 | component.onAdd(this); 160 | changed = true; 161 | success = true; 162 | break; 163 | } 164 | } 165 | return success; 166 | } 167 | 168 | /** 169 | * If a {@link GUIComponent} exists with an ID matching the given id, 170 | * adds the provided component} before it, and removes any other component with the same ID as it 171 | * 172 | * @param component the GUIComponent to add to the GUI 173 | * @return whether the component could be added 174 | */ 175 | @SuppressWarnings("unused") 176 | public boolean putBefore(String before, @NotNull GUIComponent component) { 177 | boolean success = false; 178 | remove(component.getID()); 179 | int i = -1; 180 | for (GUIComponent component1 : guiComponents) { 181 | i++; 182 | if (component1.getID().equals(before)) { 183 | guiComponents.add(i, component); 184 | component.onAdd(this); 185 | changed = true; 186 | success = true; 187 | break; 188 | } 189 | } 190 | return success; 191 | } 192 | 193 | /** 194 | * If a {@link GUIComponent} exists with an ID matching the given id, removes it from the GUI 195 | * 196 | * @return whether a component with a matching id was removed 197 | */ 198 | @SuppressWarnings("UnusedReturnValue") 199 | public boolean remove(String id) { 200 | checkID(id); 201 | return removeIf(component -> component.getID().equals(id)); 202 | } 203 | 204 | /** 205 | * Removes any {@link GUIComponent}s that meet a given predicate from the GUI 206 | * 207 | * @param predicate the predicate that to-be-removed {@link GUIComponent}s must meet 208 | * @return whether any {@link GUIComponent}s were removed 209 | */ 210 | public boolean removeIf(Predicate predicate) { 211 | boolean success = false; 212 | for (Iterator iterator = guiComponents.iterator(); iterator.hasNext(); ) { 213 | GUIComponent component = iterator.next(); 214 | if (isLegalID(component.getID()) && predicate.test(component)) { 215 | success = true; 216 | iterator.remove(); 217 | component.onRemove(this); 218 | } 219 | } 220 | changed = success || changed; 221 | return success; 222 | } 223 | 224 | private static boolean isLegalID(String id) { return !ID_DEFAULT.equals(id); } 225 | 226 | private static void checkID(String id) { 227 | if (!isLegalID(id)) { 228 | throw new IllegalArgumentException("Cannot remove nor change default component _default_"); 229 | } 230 | } 231 | 232 | // Debug 233 | 234 | private boolean debug = false; 235 | 236 | /** 237 | * Gets whether this GUI is in debug mode (displaying the debug {@link GUIComponent}s defined in the plugin's config) 238 | * 239 | * @return whether this GUI is in debug mode 240 | */ 241 | public boolean isDebugging() { return debug; } 242 | 243 | /** 244 | * Sets this GUI to debug mode if debug is true, or disables it otherwise 245 | * 246 | * @param debug whether to put the GUI in debug mode or not 247 | */ 248 | public void setDebug(boolean debug) { 249 | if (this.debug == debug) { return; } else { 250 | this.debug = debug; 251 | changed = true; 252 | } 253 | if (debug) { 254 | for (GUIComponent component : GUIAPI.getGUIConfig().getDebugComponents()) { putOnTop(component); } 255 | } else { removeIf(component -> component.getID().startsWith(ID_DEBUG)); } 256 | } 257 | 258 | // Display 259 | 260 | private BaseComponent gui; 261 | private boolean changed = true; 262 | private long lastSend = 0; 263 | 264 | private void build() { 265 | gui = new TextComponent(); 266 | int offset = 0; 267 | for (GUIComponent guiComponent : guiComponents) { 268 | if (debug && !guiComponent.getID().startsWith(ID_DEBUG)) { continue; } 269 | if (guiComponent.getID().equals(ID_DEFAULT)) { 270 | if (anonComponent == null) { continue; } 271 | guiComponent = anonComponent; 272 | } 273 | 274 | offset += guiComponent.getLeftSpaces(); 275 | if (offset != 0) { gui.addExtra(new TextComponent(spacesOf(offset))); } 276 | gui.addExtra(guiComponent.getComponent()); 277 | offset = guiComponent.getRightSpaces(); 278 | } 279 | if (offset != 0) { gui.addExtra(new TextComponent(spacesOf(offset))); } 280 | changed = false; 281 | } 282 | 283 | /** 284 | * Plays the GUI to its {@link Player}. This is already done automatically by {@link GUIAPI} 285 | * No need to call manually under normal usage conditions 286 | */ 287 | public void play() { 288 | long time = System.currentTimeMillis(); 289 | boolean play = changed || time-lastSend >= GUIAPI.getGUIConfig().getSendPeriod(); 290 | if (play) { 291 | lastSend = time; 292 | if (changed) { build(); } 293 | sending = true; 294 | player.spigot().sendMessage(ChatMessageType.ACTION_BAR, gui); 295 | sending = false; 296 | } 297 | } 298 | 299 | /** 300 | * For internal use only 301 | * Code ran when {@link GUIAPI} is reloaded 302 | */ 303 | public void onReload() { 304 | // Update anon period 305 | anonCycler.reschedule(); 306 | 307 | // Remove anonymous components 308 | for (AnonComponent component : anonComponents) { component.cancelTask(); } 309 | anonComponents.clear(); 310 | anonComponent = null; 311 | 312 | // Refresh debug components 313 | if (debug) { 314 | setDebug(false); 315 | setDebug(true); 316 | } 317 | } 318 | 319 | // Anonymous component handling 320 | 321 | private final LinkedHashSet anonComponents = new LinkedHashSet<>(); 322 | private int anonIndex = 0; 323 | private AnonComponent anonComponent = null; 324 | private final AnonCycler anonCycler = new AnonCycler(); 325 | 326 | /** 327 | * Adds an {@link AnonComponent} containing the given baseComponent 328 | * Because the content of other {@link BaseComponent} implementations are unknown to the server, 329 | * only supports {@link TextComponent} at this moment 330 | * 331 | * @param baseComponent the anonymous chat component to add to the GUI 332 | * @return whether the given baseComponent was of the supported types and could be added 333 | */ 334 | public boolean addAnonComponent(BaseComponent baseComponent) { 335 | String text = baseComponent.toPlainText(); 336 | for (AnonComponent otherComponent : anonComponents) { 337 | if (otherComponent.getComponent().toPlainText().equals(text)) { 338 | otherComponent.refresh(); 339 | return true; 340 | } 341 | } 342 | AnonComponent component; 343 | try { 344 | component = new AnonComponent(baseComponent, this); 345 | } catch (IllegalArgumentException e) { return false; } 346 | anonComponents.add(component); 347 | component.refresh(); 348 | anonComponent = component; 349 | changed = true; 350 | anonCycler.stop(); 351 | if (anonComponents.size() > 1) { anonCycler.schedule(); } 352 | return true; 353 | } 354 | 355 | /** 356 | * Removes the given anonymous component from the GUI 357 | * 358 | * @param component the anonymous component to remove 359 | */ 360 | public void removeAnonComponent(AnonComponent component) { 361 | anonComponents.remove(component); 362 | if (anonComponent == component) { 363 | nextAnonComponent(); 364 | anonCycler.reschedule(); 365 | } 366 | if (anonComponents.size() <= 1) { anonCycler.stop(); } 367 | } 368 | 369 | private void nextAnonComponent() { 370 | AnonComponent oldComponent = anonComponent; 371 | int size = anonComponents.size(); 372 | if (size == 0) { 373 | anonIndex = 0; 374 | anonComponent = null; 375 | } else { 376 | anonIndex = (anonIndex+1)%size; 377 | Iterator iterator = anonComponents.iterator(); 378 | AnonComponent component = null; 379 | for (int i = 0; i <= anonIndex; i++) { component = iterator.next(); } 380 | anonComponent = component; 381 | } 382 | if (anonComponent != oldComponent) { changed = true; } 383 | } 384 | 385 | private class AnonCycler { 386 | private BukkitRunnable runnable; 387 | 388 | private void schedule() { 389 | stop(); 390 | runnable = new BukkitRunnable() { 391 | @Override 392 | public void run() { 393 | nextAnonComponent(); 394 | } 395 | }; 396 | int period = GUIAPI.getGUIConfig().getAnonPeriod(); 397 | runnable.runTaskTimer(GUIAPI.getPlugin(), period, period); 398 | } 399 | 400 | private void stop() { 401 | if (runnable != null && !runnable.isCancelled()) { 402 | try { runnable.cancel(); } catch (IllegalStateException ignored) { } 403 | } 404 | } 405 | 406 | private void reschedule() { 407 | stop(); 408 | schedule(); 409 | } 410 | } 411 | 412 | // Static methods 413 | 414 | private static boolean sending = false; 415 | 416 | /** 417 | * Used to distinguish between action bar text sent by {@link GUIAPI} and those send anonymously 418 | * 419 | * @return whether {@link GUIAPI} is sending an action bar packet at the moment of calling 420 | */ 421 | public static boolean isSending() { return sending; } 422 | 423 | /** 424 | * Builds a {@link String} containing the specified amount of space, 425 | * from space characters provided by AmberW's Negative Space resource pack 426 | * 427 | * @param amount the amount of space to generate the string for, whether positive, negative or zero 428 | * @return the built {@link String} containing the specified amount of space 429 | */ 430 | public static String spacesOf(int amount) { 431 | Map spaces; 432 | if (amount == 0) { return ""; } else if (amount > 0) { spaces = POS_SPACES; } else { 433 | amount = -amount; 434 | spaces = NEG_SPACES; 435 | } 436 | StringBuilder builder = new StringBuilder(); 437 | while (amount > 1024) { 438 | builder.append(spaces.get(1024)); 439 | amount -= 1024; 440 | } 441 | int power = 1; 442 | while (amount/power >= 1) { 443 | power *= 2; 444 | } 445 | while (amount > 0) { 446 | if (amount > 8) { 447 | power /= 2; 448 | if (amount >= power) { 449 | builder.append(spaces.get(power)); 450 | amount -= power; 451 | } 452 | } else { 453 | builder.append(spaces.get(amount)); 454 | break; 455 | } 456 | } 457 | return builder.toString(); 458 | } 459 | } 460 | --------------------------------------------------------------------------------