diff --git a/src/main/java/dev/majek/hexnicks/HexNicks.java b/src/main/java/dev/majek/hexnicks/HexNicks.java index 83b1b1b..1fdaf55 100644 --- a/src/main/java/dev/majek/hexnicks/HexNicks.java +++ b/src/main/java/dev/majek/hexnicks/HexNicks.java @@ -32,6 +32,7 @@ import dev.majek.hexnicks.api.HexNicksApi; import dev.majek.hexnicks.command.*; import dev.majek.hexnicks.config.ConfigValues; +import dev.majek.hexnicks.gui.GuiManager; import dev.majek.hexnicks.storage.*; import dev.majek.hexnicks.event.PaperTabCompleteEvent; import dev.majek.hexnicks.event.PlayerChat; @@ -67,6 +68,7 @@ public final class HexNicks extends JavaPlugin { private static ConfigValues config; private static HookManager hooks; private static StorageMethod storage; + private static GuiManager guis; private final File jsonFile; private final Map nickMap; private final Metrics metrics; @@ -83,6 +85,7 @@ public HexNicks() { logging = new LoggingManager(this, new File(this.getDataFolder(), "logs")); config = new ConfigValues(); hooks = new HookManager(); + guis = new GuiManager(); this.jsonFile = new File(this.getDataFolder(), "nicknames.json"); this.nickMap = new HashMap<>(); // Track plugin metrics through bStats @@ -167,7 +170,7 @@ public void onEnable() { () -> String.valueOf(config.CHAT_FORMATTER))); // Register events - this.registerEvents(new PlayerJoin(), new PaperTabCompleteEvent(), new PlayerChat()); + this.registerEvents(new PlayerJoin(), new PaperTabCompleteEvent(), new PlayerChat(), guis); // Check for updates - prompt to update if there is one if (this.updateChecker.isBehindSpigot()) { @@ -285,6 +288,15 @@ public static LoggingManager logging() { return logging; } + /** + * Get the plugin's gui manager. + * + * @return gui manager + */ + public static GuiManager guis() { + return guis; + } + /** * Reload the plugin's configuration file. */ diff --git a/src/main/java/dev/majek/hexnicks/command/CommandNickColor.java b/src/main/java/dev/majek/hexnicks/command/CommandNickColor.java index f01f51f..60939f6 100644 --- a/src/main/java/dev/majek/hexnicks/command/CommandNickColor.java +++ b/src/main/java/dev/majek/hexnicks/command/CommandNickColor.java @@ -26,6 +26,7 @@ import dev.majek.hexnicks.HexNicks; import dev.majek.hexnicks.api.NickColorEvent; import dev.majek.hexnicks.config.Messages; +import dev.majek.hexnicks.gui.NickColorGui; import dev.majek.hexnicks.message.MiniMessageWrapper; import java.util.Collections; import java.util.List; @@ -53,7 +54,12 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command } if (args.length == 0) { - return false; + if (HexNicks.config().NICKCOLOR_GUI) { + new NickColorGui().openGui(player); + return true; + } else { + return false; + } } String nickInput = String.join(" ", args); diff --git a/src/main/java/dev/majek/hexnicks/config/ConfigValues.java b/src/main/java/dev/majek/hexnicks/config/ConfigValues.java index b1f8d05..9a4795e 100644 --- a/src/main/java/dev/majek/hexnicks/config/ConfigValues.java +++ b/src/main/java/dev/majek/hexnicks/config/ConfigValues.java @@ -58,6 +58,7 @@ public class ConfigValues { public Boolean PREVENT_DUPLICATE_NICKS; public Boolean PREVENT_DUPLICATE_NICKS_STRICT; public List BLOCKED_NICKNAMES; + public Boolean NICKCOLOR_GUI; public Boolean DEBUG; public ConfigValues() { @@ -84,6 +85,7 @@ public void reload() { PREVENT_DUPLICATE_NICKS = HexNicks.core().getConfig().getBoolean("prevent-duplicate-nicks", true); PREVENT_DUPLICATE_NICKS_STRICT = HexNicks.core().getConfig().getBoolean("prevent-duplicate-nicks-strict", false); BLOCKED_NICKNAMES = HexNicks.core().getConfig().getStringList("blocked-nicknames"); + NICKCOLOR_GUI = HexNicks.core().getConfig().getBoolean("nickcolor-gui", true); DEBUG = HexNicks.core().getConfig().getBoolean("debug", false); } diff --git a/src/main/java/dev/majek/hexnicks/config/Messages.java b/src/main/java/dev/majek/hexnicks/config/Messages.java index 809f63e..fab426a 100644 --- a/src/main/java/dev/majek/hexnicks/config/Messages.java +++ b/src/main/java/dev/majek/hexnicks/config/Messages.java @@ -27,6 +27,7 @@ import dev.majek.hexnicks.util.MiscUtils; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextReplacementConfig; +import net.kyori.adventure.text.format.TextColor; import net.kyori.adventure.text.minimessage.MiniMessage; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; @@ -120,6 +121,27 @@ public interface Messages { .replaceText(TextReplacementConfig.builder().matchLiteral("%player%").replacement(player.getName()).build()) .replaceText(TextReplacementConfig.builder().matchLiteral("%nick%").replacement(nickname).build()); + Args0 GUI_BACK = () -> MiscUtils.configString("messages.guiBack", "Back"); + + Args0 GUI_NICK_COLOR_TITLE = () -> MiscUtils.configString("messages.guiNickColorTitle", "Nick Color"); + + Args1 GUI_NICK_COLOR_HEX_TITLE = (col) -> MiniMessage.miniMessage().deserialize(HexNicks.core().getConfig().getString( + "messages.guiNickColorHexTitle", + "Nick Color - %color%" + ).replace("%color%", col.toString())); + + Args0 GUI_NICK_COLOR_RANDOM_HEX = () -> MiscUtils.configString("messages.guiNickColorRandomHex", "Random Hexadecimal Color"); + + Args1 GUI_NICK_COLOR_HEX_RED = (c) -> MiscUtils.configString("messages.guiNickColorHexRed", "Red %step%") + .replaceText(TextReplacementConfig.builder().matchLiteral("%step%").replacement("%+d".formatted(c)).build()); + Args1 GUI_NICK_COLOR_HEX_GREEN = (c) -> MiscUtils.configString("messages.guiNickColorHexGreen", "Green %step%") + .replaceText(TextReplacementConfig.builder().matchLiteral("%step%").replacement("%+d".formatted(c)).build()); + Args1 GUI_NICK_COLOR_HEX_BLUE = (c) -> MiscUtils.configString("messages.guiNickColorHexBlue", "Blue %step%") + .replaceText(TextReplacementConfig.builder().matchLiteral("%step%").replacement("%+d".formatted(c)).build()); + + Args0 GUI_NICK_COLOR_HEX_BUTTON = () -> MiscUtils.configString("messages.guiNickColorHexButton", "Custom Hexadecimal Color"); + Args0 GUI_NICK_COLOR_HEX_SAVE = () -> MiscUtils.configString("messages.guiNickColorHexSave", "Click to save nickname"); + /** * A message that has no arguments that need to be replaced. */ diff --git a/src/main/java/dev/majek/hexnicks/gui/Gui.java b/src/main/java/dev/majek/hexnicks/gui/Gui.java new file mode 100644 index 0000000..40115b2 --- /dev/null +++ b/src/main/java/dev/majek/hexnicks/gui/Gui.java @@ -0,0 +1,144 @@ +package dev.majek.hexnicks.gui; + +import dev.majek.hexnicks.HexNicks; +import net.kyori.adventure.text.Component; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryAction; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Range; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.function.Consumer; + +public abstract class Gui { + + private final Inventory inventory; + private Player user; + + // Map of slot : slot action -- slot action can either be Consumer or Runnable + private final HashMap actionMap; + + // The item to use as a background colour + private static final ItemStack BLANK_ITEM; + + static { + BLANK_ITEM = new ItemStack(Material.GRAY_STAINED_GLASS_PANE); + ItemMeta meta = BLANK_ITEM.getItemMeta(); + meta.displayName(Component.empty()); + BLANK_ITEM.setItemMeta(meta); + } + + /** + * Create a gui with a given size and a name + * + * @param size The amount of slots for the GUI -- must be a multiple of 9 + * @param name The name for the gui, shown in the top-left corner + */ + public Gui(@Range(from = 1, to = 54) int size, @NotNull Component name) { + this.inventory = Bukkit.createInventory(null, size, name); + this.user = null; + this.actionMap = new HashMap<>(); + } + + /** + * Open the gui for a player, this will register the gui in the GuiManager to handle all events, and intialise the inventory + * + * @param player the player to open this gui for + */ + public void openGui(Player player) { + this.user = player; + fillInventory(); + player.openInventory(inventory); + HexNicks.guis().registerOpenGui(this.user.getUniqueId(), this); + } + + /** + * Set the contents of the inventory to only {@link Gui#BLANK_ITEM}. + * + * Note: This overwrites all slots in the inventory and should be used before setting other items. + */ + protected void blankInventory() { + ItemStack[] contents = new ItemStack[this.inventory.getSize()]; + // Clone here so that the user may modify the inventory slots without changing it for _all_ guis + Arrays.fill(contents, BLANK_ITEM.clone()); + this.inventory.setContents(contents); + } + + /** + * Add a button into the gui that may run an action + * + * @param slot the slot in which to place the item stack + * @param stack the stack to be placed -- This is _not_ cloned + * @param action the action to be run + */ + protected void addActionButton(int slot, ItemStack stack, Runnable action) { + this.inventory.setItem(slot, stack); + this.actionMap.put(slot, action); + } + + /** + * Add a button into the gui that may run an action + * + * @param slot the slot in which to place the item stack + * @param stack the stack to be placed -- This is _not_ cloned + * @param action the action to be run, passed the inventory action that occurs on the click. + */ + protected void addActionButton(int slot, ItemStack stack, Consumer action) { + this.inventory.setItem(slot, stack); + this.actionMap.put(slot, action); + } + + /** + * Fill the inventory with items. This is run when the inventory is first opened for a player and may be run after to refresh the gui. + *

+ * It is recommended to call {@link Gui#blankInventory()} at the beginning if you want a background. + */ + protected abstract void fillInventory(); + + /** + * Handle the inventory click event for this gui + * + * @param event the event that occurred + */ + public void onInventoryClick(InventoryClickEvent event) { + if (this.inventory().equals(event.getClickedInventory())) { + this.onInventoryClick(event.getSlot(), event.getAction()); + event.setCancelled(true); + } + } + + /** + * Handle inventory click on a specific slot for this gui + *

+ * Default implementation is to call the action for that slot + * + * @param slot the slot which has been clicked + * @param invAction the InventoryAction that was called on the slot + */ + protected void onInventoryClick(int slot, InventoryAction invAction) { + Object action = this.actionMap.get(slot); + if (action != null) { + if (action instanceof Runnable runnable) { + runnable.run(); + } else if (action instanceof Consumer) { + // This cast is okay because we know that the only way to insert a Consumer into the map is to use `addActionButton(.., Consumer)`. + ((Consumer) action).accept(invAction); + } + } + } + + protected Inventory inventory() { + return this.inventory; + } + + protected Player user() { + return this.user; + } +} diff --git a/src/main/java/dev/majek/hexnicks/gui/GuiManager.java b/src/main/java/dev/majek/hexnicks/gui/GuiManager.java new file mode 100644 index 0000000..cd90fa4 --- /dev/null +++ b/src/main/java/dev/majek/hexnicks/gui/GuiManager.java @@ -0,0 +1,96 @@ +package dev.majek.hexnicks.gui; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryCloseEvent; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.UUID; + +/** + * A manager for Guis, this handles events such as {@link InventoryClickEvent} and determines which {@link Gui} to send the event to + */ +public class GuiManager implements Listener { + + private final HashMap openGuis; + + /** + * Create the new GUI Manager + */ + public GuiManager() { + this.openGuis = new HashMap<>(); + } + + /** + * Add a GUI for this manager to handle + * + * @param uuid the Player who opened this gui + * @param gui the gui which was opened + */ + public void registerOpenGui(@NotNull UUID uuid, @NotNull Gui gui) { + this.openGuis.put(uuid, gui); + } + + /** + * Get a gui that is open for a player + * + * @param uuid the player + * @return the gui that is open -- {@code null} if no gui is open + */ + public @Nullable Gui getOpenGuiFor(@NotNull UUID uuid) { + return this.openGuis.get(uuid); + } + + /** + * Close a specific gui for a player + * + * @param uuid the player + * @param gui the gui to close + * @return true if the gui was open, false otherwise + */ + public boolean closeGui(@NotNull UUID uuid, @NotNull Gui gui) { + if (this.openGuis.remove(uuid, gui)) { + Player player = Bukkit.getPlayer(uuid); + if (player != null) { + player.closeInventory(InventoryCloseEvent.Reason.PLUGIN); + } + return true; + } + return false; + } + + /** + * Close the open gui for a player, if the player had this gui, then it is + * + * @param uuid the player + * @return true if a gui was open, false otherwise + */ + public boolean closeGui(@NotNull UUID uuid) { + if (this.openGuis.remove(uuid) != null) { + Player player = Bukkit.getPlayer(uuid); + if (player != null) { + player.closeInventory(InventoryCloseEvent.Reason.PLUGIN); + } + return true; + } + return false; + } + + @EventHandler + public void onInventoryClick(InventoryClickEvent event) { + Gui gui = this.getOpenGuiFor(event.getWhoClicked().getUniqueId()); + if (gui != null) { + gui.onInventoryClick(event); + } + } + + @EventHandler + public void onInventoryClose(InventoryCloseEvent event) { + this.openGuis.remove(event.getPlayer().getUniqueId()); + } +} diff --git a/src/main/java/dev/majek/hexnicks/gui/NickColorGui.java b/src/main/java/dev/majek/hexnicks/gui/NickColorGui.java new file mode 100644 index 0000000..e07535e --- /dev/null +++ b/src/main/java/dev/majek/hexnicks/gui/NickColorGui.java @@ -0,0 +1,132 @@ +package dev.majek.hexnicks.gui; + +import dev.majek.hexnicks.command.CommandHexNicks; +import dev.majek.hexnicks.command.CommandNickColor; +import dev.majek.hexnicks.config.Messages; +import dev.majek.hexnicks.util.CustomHead; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.format.TextDecoration; +import org.bukkit.event.inventory.InventoryAction; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +/** + * A gui to be used for choosing a nick color. This shows the vanilla colours (black, dark blue, etc.) and a button to create a hexadecimal color. + */ +public class NickColorGui extends Gui { + + private static final ItemStack BLACK; + private static final ItemStack DARK_BLUE; + private static final ItemStack DARK_GREEN; + private static final ItemStack DARK_AQUA; + private static final ItemStack DARK_RED; + private static final ItemStack DARK_PURPLE; + private static final ItemStack GOLD; + private static final ItemStack GRAY; + private static final ItemStack DARK_GRAY; + private static final ItemStack BLUE; + private static final ItemStack GREEN; + private static final ItemStack AQUA; + private static final ItemStack RED; + private static final ItemStack LIGHT_PURPLE; + private static final ItemStack YELLOW; + private static final ItemStack WHITE; + + private static final ItemStack RAINBOW; + + /** + * Generate an item to represent a color choice + * + * @param name the name of the colour + * @param color the color to use for the title (probably the color that this item represents) + * @param base the base item stack to use -- this is _not_ cloned + * @return The {@link ItemStack} that can be used to represent this colour + */ + @Contract(value = "_, _, _ -> param3", mutates = "param3") + private static ItemStack generateItem(@NotNull String name, @NotNull TextColor color, @NotNull ItemStack base) { + Component nameComp = Component.text(name, color, TextDecoration.BOLD).decoration(TextDecoration.ITALIC, false); + ItemMeta meta = base.getItemMeta(); + meta.displayName(nameComp); + base.setItemMeta(meta); + return base; + } + + static { + // @formatter:off + // TODO: Language(?) + BLACK = generateItem("Black", NamedTextColor.BLACK, CustomHead.BLACK.asItemStack() ); + DARK_BLUE = generateItem("Dark Blue", NamedTextColor.DARK_BLUE, CustomHead.DARK_BLUE.asItemStack() ); + DARK_GREEN = generateItem("Dark Green", NamedTextColor.DARK_GREEN, CustomHead.DARK_GREEN.asItemStack() ); + DARK_AQUA = generateItem("Dark Aqua", NamedTextColor.DARK_AQUA, CustomHead.DARK_AQUA.asItemStack() ); + DARK_RED = generateItem("Dark Red", NamedTextColor.DARK_RED, CustomHead.DARK_RED.asItemStack() ); + DARK_PURPLE = generateItem("Dark Purple", NamedTextColor.DARK_PURPLE, CustomHead.DARK_PURPLE.asItemStack() ); + GOLD = generateItem("Gold", NamedTextColor.GOLD, CustomHead.GOLD.asItemStack() ); + GRAY = generateItem("Gray", NamedTextColor.GRAY, CustomHead.GRAY.asItemStack() ); + DARK_GRAY = generateItem("Dark Gray", NamedTextColor.DARK_GRAY, CustomHead.DARK_GRAY.asItemStack() ); + BLUE = generateItem("Blue", NamedTextColor.BLUE, CustomHead.BLUE.asItemStack() ); + GREEN = generateItem("Green", NamedTextColor.GREEN, CustomHead.GREEN.asItemStack() ); + AQUA = generateItem("Aqua", NamedTextColor.AQUA, CustomHead.AQUA.asItemStack() ); + RED = generateItem("Red", NamedTextColor.RED, CustomHead.RED.asItemStack() ); + LIGHT_PURPLE = generateItem("Light Purple", NamedTextColor.LIGHT_PURPLE, CustomHead.LIGHT_PURPLE.asItemStack() ); + YELLOW = generateItem("Yellow", NamedTextColor.YELLOW, CustomHead.YELLOW.asItemStack() ); + WHITE = generateItem("White", NamedTextColor.WHITE, CustomHead.WHITE.asItemStack() ); + // @formatter:on + + RAINBOW = CustomHead.RAINBOW.asItemStack(); + ItemMeta meta = RAINBOW.getItemMeta(); + meta.displayName(Messages.GUI_NICK_COLOR_HEX_BUTTON.build()); + RAINBOW.setItemMeta(meta); + } + + public NickColorGui() { + super(54, Messages.GUI_NICK_COLOR_TITLE.build()); + } + + /** + * Generate a function that may be used as an action for an item to set the nick color to a value. + * + * @param color The color to which the nickname should be set + * @return The Runnable that can be used as an action + */ + private Runnable setNickColor(TextColor color) { + return () -> { + // Use the command since it already handles the logic + this.user().performCommand("hexnicks:nickcolor <" + color + ">"); + }; + } + + @Override + protected void fillInventory() { + this.blankInventory(); + + addActionButton(10, DARK_BLUE, setNickColor(NamedTextColor.DARK_BLUE)); + addActionButton(11, DARK_GREEN, setNickColor(NamedTextColor.DARK_GREEN)); + addActionButton(12, DARK_AQUA, setNickColor(NamedTextColor.DARK_AQUA)); + addActionButton(13, DARK_RED, setNickColor(NamedTextColor.DARK_RED)); + addActionButton(14, DARK_PURPLE, setNickColor(NamedTextColor.DARK_PURPLE)); + addActionButton(15, GOLD, setNickColor(NamedTextColor.GOLD)); + addActionButton(16, GRAY, setNickColor(NamedTextColor.GRAY)); + + addActionButton(19, BLUE, setNickColor(NamedTextColor.BLUE)); + addActionButton(20, GREEN, setNickColor(NamedTextColor.GREEN)); + addActionButton(21, AQUA, setNickColor(NamedTextColor.AQUA)); + addActionButton(22, RED, setNickColor(NamedTextColor.RED)); + addActionButton(23, LIGHT_PURPLE, setNickColor(NamedTextColor.LIGHT_PURPLE)); + addActionButton(24, YELLOW, setNickColor(NamedTextColor.YELLOW)); + addActionButton(25, WHITE, setNickColor(NamedTextColor.WHITE)); + + addActionButton(29, BLACK, setNickColor(NamedTextColor.BLACK)); + addActionButton(33, DARK_GRAY, setNickColor(NamedTextColor.DARK_GRAY)); + + addActionButton(40, RAINBOW, (a) -> { + new NickColorHexGui().openGui(this.user()); + }); + } + +} diff --git a/src/main/java/dev/majek/hexnicks/gui/NickColorHexGui.java b/src/main/java/dev/majek/hexnicks/gui/NickColorHexGui.java new file mode 100644 index 0000000..ee13129 --- /dev/null +++ b/src/main/java/dev/majek/hexnicks/gui/NickColorHexGui.java @@ -0,0 +1,186 @@ +package dev.majek.hexnicks.gui; + +import dev.majek.hexnicks.HexNicks; +import dev.majek.hexnicks.config.Messages; +import dev.majek.hexnicks.message.MiniMessageWrapper; +import dev.majek.hexnicks.util.CustomHead; +import dev.majek.hexnicks.util.MiscUtils; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.format.TextDecoration; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import org.bukkit.Material; +import org.bukkit.event.inventory.InventoryAction; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.List; +import java.util.Random; + +/** + * Gui for choosing a hexadecimal colour to use for nickname + */ +public class NickColorHexGui extends Gui { + + // The steps to be used for each button + private static final int[] STEPS = {16, 8, 1, -1, -8, -16}; + + // Arrays that hold the r/g/b color buttons + private static final ItemStack[] RED; + private static final ItemStack[] GREEN; + private static final ItemStack[] BLUE; + + // Rainbow button for random color + private static final ItemStack RAINBOW; + // Back button for back to simple color codes + private static final ItemStack BACK_BUTTON; + + private static final Random RNG = new Random(); + + private int red, green, blue; + private String nickname; + + /** + * Generate the array of items that should be used for changing the r/g/b values + * + * @param msg The message that should be used for the name (passed the step) + * @param base The base {@link ItemStack} that should be used (this is cloned for each item in the array) + * @return An array with 6 elements that maps to the values in {@code STEPS} + */ + private static ItemStack[] generateItems(Messages.Args1 msg, ItemStack base) { + ItemStack[] out = new ItemStack[6]; + ItemMeta meta = base.getItemMeta(); + for (int i = 0; i < 6; i++) { + int step = STEPS[i]; + meta.displayName(msg.build(step)); + out[i] = base.clone(); + out[i].setAmount(Math.abs(step)); + out[i].setItemMeta(meta); + } + return out; + } + + static { + // @formatter:off + RED = generateItems(Messages.GUI_NICK_COLOR_HEX_RED, CustomHead.RED.asItemStack()); + GREEN = generateItems(Messages.GUI_NICK_COLOR_HEX_GREEN, CustomHead.GREEN.asItemStack()); + BLUE = generateItems(Messages.GUI_NICK_COLOR_HEX_BLUE, CustomHead.AQUA.asItemStack()); + // @formatter:on + + // Generate the rainbow button + RAINBOW = CustomHead.RAINBOW.asItemStack(); + ItemMeta meta = RAINBOW.getItemMeta(); + meta.displayName(Messages.GUI_NICK_COLOR_RANDOM_HEX.build()); + RAINBOW.setItemMeta(meta); + + BACK_BUTTON = new ItemStack(Material.NETHER_STAR); + meta = BACK_BUTTON.getItemMeta(); + meta.displayName(Messages.GUI_BACK.build()); + BACK_BUTTON.setItemMeta(meta); + } + + public NickColorHexGui() { + this(0x88, 0x88, 0x88); + } + + public NickColorHexGui(int r, int g, int b) { + super(54, Messages.GUI_NICK_COLOR_HEX_TITLE.build(TextColor.color(r, g, b))); + + this.red = r; + this.green = g; + this.blue = b; + } + + /** + * Update the name icon in the gui (name tag in slot 20) + */ + private void updateName(boolean newGui) { + TextColor col = TextColor.color(this.red, this.green, this.blue); + ItemStack s = new ItemStack(Material.NAME_TAG); + ItemMeta meta = s.getItemMeta(); + meta.displayName(Component.text(this.nickname).color(col).decoration(TextDecoration.ITALIC, false)); + meta.lore(List.of( + Component.text(col.toString()).decoration(TextDecoration.ITALIC, false), + Messages.GUI_NICK_COLOR_HEX_SAVE.build() + )); + s.setItemMeta(meta); + addActionButton(20, s, () -> { + // Use the command since it already handles the logic + this.user().performCommand("hexnicks:nickcolor <" + col + ">"); + }); + + if (newGui) { + // unfortunately, this seems to be the only way to change the title (without manual packets) + new NickColorHexGui(this.red, this.green, this.blue).openGui(this.user()); + } + } + + @Override + protected void fillInventory() { + this.blankInventory(); + // By this point, we have the player, so we can get their nickname + this.nickname = PlainTextComponentSerializer.plainText().serialize(HexNicks.core().getDisplayName(this.user())); + + // Remove nickname prefix if essentials is hooked + if (HexNicks.hooks().isEssentialsHooked()) { + String nickPrefix = HexNicks.hooks().getEssNickPrefix(); + if (nickPrefix != null && this.nickname.startsWith(nickPrefix)) { + this.nickname = this.nickname.substring(nickPrefix.length()); + } + } + + // Add the "random hex colour" button + addActionButton(49, RAINBOW, (a) -> { + byte[] bytes = new byte[3]; + RNG.nextBytes(bytes); + this.red = bytes[0]; + this.green = bytes[1]; + this.blue = bytes[2]; + updateName(true); + }); + + // Set the back button to the bottom-left + addActionButton(45, BACK_BUTTON, (a) -> { + new NickColorGui().openGui(this.user()); + }); + + // Generate the nametag item + updateName(false); + + // add all 18 buttons + for (int i = 0; i < 6; i++) { + final int finalI = i; + // Add red column + addActionButton(5 + i * 9, RED[i], (a) -> { + if ((finalI == 0 || finalI == 5) && a == InventoryAction.MOVE_TO_OTHER_INVENTORY) { // shift click on +/- 16 + this.red = STEPS[finalI] < 0 ? 0x00 : 0xff; + } else { + this.red = MiscUtils.constrain(this.red + STEPS[finalI], 0, 255); + } + updateName(true); + }); + + // Add green column + addActionButton(6 + i * 9, GREEN[i], (a) -> { + if ((finalI == 0 || finalI == 5) && a == InventoryAction.MOVE_TO_OTHER_INVENTORY) { // shift click on +/- 16 + this.green = STEPS[finalI] < 0 ? 0x00 : 0xff; + } else { + this.green = MiscUtils.constrain(this.green + STEPS[finalI], 0, 255); + } + updateName(true); + }); + + // Add blue column + addActionButton(7 + i * 9, BLUE[i], (a) -> { + if ((finalI == 0 || finalI == 5) && a == InventoryAction.MOVE_TO_OTHER_INVENTORY) { // shift click on +/- 16 + this.blue = STEPS[finalI] < 0 ? 0x00 : 0xff; + } else { + this.blue = MiscUtils.constrain(this.blue + STEPS[finalI], 0, 255); + } + updateName(true); + }); + } + } + +} diff --git a/src/main/java/dev/majek/hexnicks/gui/package-info.java b/src/main/java/dev/majek/hexnicks/gui/package-info.java new file mode 100644 index 0000000..783c5f0 --- /dev/null +++ b/src/main/java/dev/majek/hexnicks/gui/package-info.java @@ -0,0 +1,4 @@ +/** + * Guis used by the plugin + */ +package dev.majek.hexnicks.gui; \ No newline at end of file diff --git a/src/main/java/dev/majek/hexnicks/util/CustomHead.java b/src/main/java/dev/majek/hexnicks/util/CustomHead.java new file mode 100644 index 0000000..f5d508d --- /dev/null +++ b/src/main/java/dev/majek/hexnicks/util/CustomHead.java @@ -0,0 +1,55 @@ +package dev.majek.hexnicks.util; + +import com.destroystokyo.paper.profile.PlayerProfile; +import com.destroystokyo.paper.profile.ProfileProperty; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.SkullMeta; + +import java.util.UUID; + +public enum CustomHead { + BLACK("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOTc0ZmU5Y2I4MDAyOWQ2NjM0NTI3N2FhNTYwZDQxZWYxMDMwOTYyYjdmMjlhYmYyMzk2MWQ5ZWJhODQyNTBhMyJ9fX0="), + DARK_BLUE("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvN2U3YWI3MTJjODdmNjdkNDhiOThmNzA2MzRkMWRjZmNkNTk4MGMzZDZmMGQ2MjJjZGMzMjMwOTEyMzYxYjU0ZSJ9fX0="), + DARK_GREEN("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYTNlOWY0ZGJhZGRlMGY3MjdjNTgwM2Q3NWQ4YmIzNzhmYjlmY2I0YjYwZDMzYmVjMTkwOTJhM2EyZTdiMDdhOSJ9fX0="), + DARK_AQUA("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOTc1YjdhYzlmMGM3MTIzMDNjZDNiNjU0ZTY0NmNlMWM0YmYyNDNhYjM0OGE2YTI1MzcwZjI2MDNlNzlhNjJhMCJ9fX0="), + DARK_RED("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYzY1ZjNiYWUwZDIwM2JhMTZmZTFkYzNkMTMwN2E4NmE2MzhiZTkyNDQ3MWYyM2U4MmFiZDlkNzhmOGEzZmNhIn19fQ=="), + DARK_PURPLE("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNDY3ZjJiNTA2MzcwYzFlODRmOTBmYmYyOWM4MGUwY2I3ZTJhYzkzMjMwMzAxYjVkOGU0MmM2OGZkZGU4OWZlMCJ9fX0="), + GOLD("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNTE4OWYzNDdmNDI0NTBjZDJhMmU5YjhhNTM5ODgwN2QyOGM3ZjQyNTRiZDk5YThhNDk5Y2U1NDM1MzIwOTU1In19fQ=="), + GRAY("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYzMyOGRjZGUxNzNiZWZmOWYzZjQxYjkyMzIxM2ZjMWJiNzY3ODk2N2NjYjJlZGU3YTdjZjQwYjE4MzZiMWE3MyJ9fX0="), + DARK_GRAY("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvN2FmNmZhYjc2N2NhNGQ3ZGY2MjE3Yjg5NWI2NjdiY2FjYzUyNGQ0MDcwNjg2MTlmODE5YTA3MGYzZjYyOWNlMCJ9fX0="), + BLUE("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvM2I1MTA2YjA2MGVhZjM5ODIxNzM0OWYzY2ZiNGYyYzdjNGZkOWEwYjAzMDdhMTdlYmE2YWY3ODg5YmUwZmJlNiJ9fX0="), + GREEN("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYjk4NWEyOTk1N2Q0MGZhNTY0ZDVlMzFjYmQ5MDVlMzY5NGE2MTYzOTNjZTEzNzEwYmZjMzFiMWI4YjBhNTIyZCJ9fX0="), + AQUA("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZjllMTY5NzkzMDliNWE5YjY3M2Q2MGQxMzkwYmJhYjBkMDM4NWVhYzcyNTRkODI4YWRhMmEzNmE0NmY3M2E1OSJ9fX0="), + RED("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMjA2MmQ4ZDcyZjU4OTFjNzFmYWIzMGQ1MmUwNDgxNzk1YjNkMmQzZDJlZDJmOGI5YjUxN2Q3ZDI4MjFlMzVkNiJ9fX0="), + LIGHT_PURPLE("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvN2E5ZWE2ZTM2ZjllNTc5ZjU4NmFkYjE5MzdiYjE0Mzc3YjBkNzQwMzRmZmNiMjU1NmEyYWNiNDM1NjcxNDQ4ZiJ9fX0="), + YELLOW("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMjAwYmY0YmYxNGM4Njk5YzBmOTIwOWNhNzlmZTE4MjUzZTkwMWU5ZWMzODc2YTJiYTA5NWRhMDUyZjY5ZWJhNyJ9fX0="), + WHITE("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOGUwZThhY2FiYWQyN2Q0NjE2ZmFlOWU0NzJjMGRlNjA4NTNkMjAzYzFjNmYzMTM2N2M5MzliNjE5ZjNlMzgzMSJ9fX0="), + + RAINBOW("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYjg3ZmQyM2E3ODM2OWJkMzg3NWRhODg5NmYxNTBjNGFmOWYyMzM3NGUwNDhlMzA5MTM5MDBlM2ZkZDc3ODU5YSJ9fX0="), + ; + + private final ItemStack itemStack; + + CustomHead(String base64) { + this.itemStack = getHead(base64); + } + + public ItemStack asItemStack() { + return this.itemStack.clone(); + } + + private static ItemStack getHead(String base64) { + ItemStack head = new ItemStack(Material.PLAYER_HEAD); + if (base64 == null || base64.isEmpty()) { + return head; + } + SkullMeta headMeta = (SkullMeta) head.getItemMeta(); + PlayerProfile profile = Bukkit.createProfile(UUID.randomUUID()); + profile.setProperty(new ProfileProperty("textures", base64)); + headMeta.setPlayerProfile(profile); + head.setItemMeta(headMeta); + return head; + } +} diff --git a/src/main/java/dev/majek/hexnicks/util/MiscUtils.java b/src/main/java/dev/majek/hexnicks/util/MiscUtils.java index 8bd3fa1..943ced6 100644 --- a/src/main/java/dev/majek/hexnicks/util/MiscUtils.java +++ b/src/main/java/dev/majek/hexnicks/util/MiscUtils.java @@ -249,4 +249,17 @@ public static void announceMessage(final @NotNull Component message) { } } } + + + /** + * Constrain a number to between two other numbers (inclusive) + * + * @param value The number to constrain + * @param min The minimum value (inclusive) + * @param max The maximum value (inclusive) + * @return value, or the closest bound + */ + public static int constrain(int value, int min, int max) { + return value > max ? max : Math.max(value, min); + } } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index f99051a..fbfff81 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -63,6 +63,9 @@ blocked-nicknames: - "replace these lines" - "what to block" +# Whether a gui should open when running `/nickcolor` with no arguments -- `/nickcolor ` works either way. +nickcolor-gui: true + # Database settings # Only enable this if you know what you're doing and have an existing database database-enabled: false @@ -105,4 +108,13 @@ messages: latestLog: "View the latest log here." working: "Working..." notAllowed: "That nickname is not allowed!" - joinAnnouncement: "%player% has the nickname %nick%" \ No newline at end of file + joinAnnouncement: "%player% has the nickname %nick%" + guiBack: "Back" + guiNickColorTitle: "Nick Color" + guiNickColorHexTitle: "Nick Color - %color%" + guiNickColorRandomHex: "Random Hexadecimal Color" + guiNickColorHexRed: "Red %step%" + guiNickColorHexGreen: "Green %step%" + guiNickColorHexBlue: "Blue %step%" + guiNickColorHexButton: "Custom Hexadecimal Color" + guiNickColorHexSave: "Click to save nickname"