From 5c2cf35387dc9e157efab5b1411eca21a4ebed25 Mon Sep 17 00:00:00 2001 From: unknown <1tranducnhan.2021@gmail.com> Date: Fri, 15 May 2026 17:58:37 +0700 Subject: [PATCH 01/12] Add inventory sub menus and CommandPanels converter --- build.gradle.kts | 7 +- gradle/libs.versions.toml | 12 +- .../deluxemenus/action/ActionType.java | 8 +- .../deluxemenus/action/ClickActionTask.java | 195 +++-- .../command/DeluxeMenusCommand.java | 1 + .../ConvertCommandPanelsCommand.java | 129 +++ .../CommandPanelsConversionResult.java | 47 + .../commandpanels/CommandPanelsConverter.java | 813 ++++++++++++++++++ .../deluxemenus/config/DeluxeMenusConfig.java | 125 ++- .../deluxemenus/listener/PlayerListener.java | 58 +- .../extendedclip/deluxemenus/menu/Menu.java | 376 +++++--- .../deluxemenus/menu/MenuHolder.java | 294 +++++-- .../deluxemenus/menu/MenuItem.java | 12 +- .../deluxemenus/menu/options/MenuOptions.java | 39 + .../deluxemenus/utils/Messages.java | 16 +- .../deluxemenus/utils/StringUtils.java | 73 +- src/main/resources/plugin.yml | 11 +- 17 files changed, 1828 insertions(+), 388 deletions(-) create mode 100644 src/main/java/com/extendedclip/deluxemenus/command/subcommand/ConvertCommandPanelsCommand.java create mode 100644 src/main/java/com/extendedclip/deluxemenus/commandpanels/CommandPanelsConversionResult.java create mode 100644 src/main/java/com/extendedclip/deluxemenus/commandpanels/CommandPanelsConverter.java diff --git a/build.gradle.kts b/build.gradle.kts index fffd5a9a..c2a2fe34 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -45,9 +45,10 @@ dependencies { compileOnly(libs.papi) implementation(libs.nashorn) - implementation(libs.adventure.platform) - implementation(libs.adventure.minimessage) - implementation(libs.bstats) + implementation(libs.adventure.platform) + implementation(libs.adventure.minimessage) + implementation(libs.adventure.legacy) + implementation(libs.bstats) compileOnly("org.jetbrains:annotations:23.0.0") } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 06e791dd..fab80a35 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,8 +17,9 @@ bstats = "3.1.0" # Implementation nashorn = "15.6" -adventure-platform = "4.4.1" -adventure-minimessage = "4.24.0" +adventure-platform = "4.4.1" +adventure-minimessage = "4.24.0" +adventure-legacy = "4.24.0" [libraries] # Compile only @@ -39,6 +40,7 @@ sig = { module = "io.github.valerashimchuck:simpleitemgenerator-api", version.re # Implementation nashorn = { module = "org.openjdk.nashorn:nashorn-core", version.ref = "nashorn" } -adventure-platform = { module = "net.kyori:adventure-platform-bukkit", version.ref = "adventure-platform" } -adventure-minimessage = { module = "net.kyori:adventure-text-minimessage", version.ref = "adventure-minimessage" } -bstats = { module = "org.bstats:bstats-bukkit", version.ref = "bstats" } +adventure-platform = { module = "net.kyori:adventure-platform-bukkit", version.ref = "adventure-platform" } +adventure-minimessage = { module = "net.kyori:adventure-text-minimessage", version.ref = "adventure-minimessage" } +adventure-legacy = { module = "net.kyori:adventure-text-serializer-legacy", version.ref = "adventure-legacy" } +bstats = { module = "org.bstats:bstats-bukkit", version.ref = "bstats" } diff --git a/src/main/java/com/extendedclip/deluxemenus/action/ActionType.java b/src/main/java/com/extendedclip/deluxemenus/action/ActionType.java index e970f76f..dc7ca50d 100644 --- a/src/main/java/com/extendedclip/deluxemenus/action/ActionType.java +++ b/src/main/java/com/extendedclip/deluxemenus/action/ActionType.java @@ -26,9 +26,11 @@ public enum ActionType { "- [message] "), LOG("[log]", "Log a message to the console", "- [log] "), BROADCAST("[broadcast]", "Broadcast a message to the server", "- '[broadcast] "), - CHAT("[chat]", "Send a chat message as the player performing the action", "- '[chat] "), - OPEN_GUI_MENU("[openguimenu]", "Open a GUI menu", "- '[openguimenu] '"), - OPEN_MENU("[openmenu]", "Open a GUI menu", "- '[openmenu] '"), + CHAT("[chat]", "Send a chat message as the player performing the action", "- '[chat] "), + OPEN_GUI_MENU("[openguimenu]", "Open a GUI menu", "- '[openguimenu] '"), + OPEN_GUI_INVENTORY("[open_gui_inventory]", "Open a GUI menu in the player inventory area", + "- '[open_gui_inventory] '"), + OPEN_MENU("[openmenu]", "Open a GUI menu", "- '[openmenu] '"), CONNECT("[connect]", "Connect to the specified bungee server", "- '[connect] '"), CLOSE("[close]", "Close the viewers open menu", "- '[close]"), REFRESH("[refresh]", "Refresh items in the current menu view", "- '[refresh]"), diff --git a/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java b/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java index dfb3592d..b0e24937 100644 --- a/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java +++ b/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java @@ -30,13 +30,12 @@ public class ClickActionTask extends UniversalRunnable { private final DeluxeMenus plugin; private final TaskScheduler scheduler; - private final UUID uuid; - private final ActionType actionType; - private final String exec; - // Ugly hack to get around the fact that arguments are not available at task execution time - private final Map arguments; - private final boolean parsePlaceholdersInArguments; - private final boolean parsePlaceholdersAfterArguments; + private final UUID uuid; + private final ActionType actionType; + private final String exec; + private final Map arguments; + private final boolean parsePlaceholdersInArguments; + private final boolean parsePlaceholdersAfterArguments; public ClickActionTask( @NotNull final DeluxeMenus plugin, @@ -168,10 +167,10 @@ public void run() { Menu.closeMenu(plugin, player, true, true); break; - case OPEN_GUI_MENU: - case OPEN_MENU: - final String temporaryExecutable = executable.replaceAll("\\s+", " ").replace(" ", " "); - final String[] executableParts = temporaryExecutable.split(" ", 2); + case OPEN_GUI_MENU: + case OPEN_MENU: + final String temporaryExecutable = executable.replaceAll("\\s+", " ").replace(" ", " "); + final String[] executableParts = temporaryExecutable.split(" ", 2); if (executableParts.length == 0) { plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Could not find and open menu " + executable); @@ -187,9 +186,10 @@ public void run() { break; } - final Menu menuToOpen = optionalMenuToOpen.get(); - - final List menuArgumentNames = menuToOpen.options().arguments(); + final Menu menuToOpen = optionalMenuToOpen.get(); + final Menu playerInventoryMenuToKeep = getPlayerInventoryMenuToKeep(holder, menuToOpen); + + final List menuArgumentNames = menuToOpen.options().arguments(); String[] passedArgumentValues = null; if (executableParts.length > 1) { @@ -204,26 +204,25 @@ public void run() { "Arguments were given for menu " + menuName + " in action [openguimenu] or [openmenu], but the menu does not support arguments!" ); } - - if (holder.isEmpty()) { - menuToOpen.openMenu(player); - break; - } - - menuToOpen.openMenu(player, holder.get().getTypedArgs(), holder.get().getPlaceholderPlayer()); - break; - } - - if (passedArgumentValues == null || passedArgumentValues.length == 0) { - // Replicate old behavior: If no arguments are given, open the menu with the arguments from the current menu - if (holder.isEmpty()) { - menuToOpen.openMenu(player); - break; - } - - menuToOpen.openMenu(player, holder.get().getTypedArgs(), holder.get().getPlaceholderPlayer()); - break; - } + + if (holder.isEmpty()) { + menuToOpen.openMenu(player, null, null, null); + break; + } + + menuToOpen.openMenu(player, holder.get().getTypedArgs(), holder.get().getPlaceholderPlayer(), playerInventoryMenuToKeep); + break; + } + + if (passedArgumentValues == null || passedArgumentValues.length == 0) { + if (holder.isEmpty()) { + menuToOpen.openMenu(player, null, null, null); + break; + } + + menuToOpen.openMenu(player, holder.get().getTypedArgs(), holder.get().getPlaceholderPlayer(), playerInventoryMenuToKeep); + break; + } if (passedArgumentValues.length < menuArgumentNames.size()) { plugin.debug( @@ -234,47 +233,69 @@ public void run() { break; } - final Map argumentsMap = new HashMap<>(); - if (holder.isPresent() && holder.get().getTypedArgs() != null) { - // Pass the arguments from the current menu to the new menu. If the new menu has arguments with the - // same name, they will be overwritten - argumentsMap.putAll(holder.get().getTypedArgs()); - } + final Map argumentsMap = new HashMap<>(); + if (holder.isPresent() && holder.get().getTypedArgs() != null) { + argumentsMap.putAll(holder.get().getTypedArgs()); + } for (int index = 0; index < menuArgumentNames.size(); index++) { - final String argumentName = menuArgumentNames.get(index); - - if (passedArgumentValues.length <= index) { - // This should never be the case! - plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, + final String argumentName = menuArgumentNames.get(index); + + if (passedArgumentValues.length <= index) { + plugin.debug( + DebugLevel.HIGHEST, + Level.WARNING, "Not enough arguments given for menu " + menuName + " when opening using the [openguimenu] or [openmenu] action!" ); break; - } - - if (menuArgumentNames.size() == index + 1) { - // If this is the last argument, get all remaining values and join them - final String lastArgumentValue = String.join(" ", Arrays.asList(passedArgumentValues).subList(index, passedArgumentValues.length)); - argumentsMap.put(argumentName, lastArgumentValue); - break; + } + + if (menuArgumentNames.size() == index + 1) { + final String lastArgumentValue = String.join(" ", Arrays.asList(passedArgumentValues).subList(index, passedArgumentValues.length)); + argumentsMap.put(argumentName, lastArgumentValue); + break; } argumentsMap.put(argumentName, passedArgumentValues[index]); } - - if (holder.isEmpty()) { - menuToOpen.openMenu(player, argumentsMap, null); - break; - } - - menuToOpen.openMenu(player, argumentsMap, holder.get().getPlaceholderPlayer()); - break; - - case CONNECT: - plugin.connect(player, executable); - break; + + if (holder.isEmpty()) { + menuToOpen.openMenu(player, argumentsMap, null, null); + break; + } + + menuToOpen.openMenu(player, argumentsMap, holder.get().getPlaceholderPlayer(), playerInventoryMenuToKeep); + break; + + case OPEN_GUI_INVENTORY: + if (holder.isEmpty()) { + plugin.debug( + DebugLevel.HIGHEST, + Level.WARNING, + "Cannot open player inventory menu " + executable + " because no DeluxeMenus menu is open." + ); + break; + } + + final String inventoryExecutable = executable.replaceAll("\\s+", " ").trim(); + if (inventoryExecutable.isBlank()) { + plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Could not find and open player inventory menu " + executable); + break; + } + + final String inventoryMenuName = inventoryExecutable.split(" ", 2)[0]; + final Optional optionalInventoryMenu = Menu.getSubMenuByName(inventoryMenuName); + if (optionalInventoryMenu.isEmpty()) { + plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Could not find and open player inventory menu " + executable); + break; + } + + holder.get().openPlayerInventoryMenu(optionalInventoryMenu.get()); + break; + + case CONNECT: + plugin.connect(player, executable); + break; case JSON_MESSAGE: AdventureUtils.sendJson(plugin, player, executable); @@ -508,26 +529,34 @@ public void run() { } break; - case PLAY_SOUND: - if (sound == null) { - plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Sound name given for sound action: " + executable + ", is not a valid sound!" - ); - break; - } - player.playSound(player.getLocation(), sound, volume, pitch); - break; - } - break; + case PLAY_SOUND: + if (sound == null) { + plugin.debug( + DebugLevel.HIGHEST, + Level.WARNING, + "Sound name given for sound action: " + executable + ", is not a valid sound!" + ); + break; + } + player.playSound(player.getLocation(), sound, volume, pitch); + break; + } + break; default: break; } } - private boolean isRaw(ActionType actionType) { - return actionType == ActionType.PLAY_RAW_SOUND || actionType == ActionType.BROADCAST_RAW_SOUND || actionType == ActionType.BROADCAST_WORLD_RAW_SOUND; - } -} + private boolean isRaw(ActionType actionType) { + return actionType == ActionType.PLAY_RAW_SOUND || actionType == ActionType.BROADCAST_RAW_SOUND || actionType == ActionType.BROADCAST_WORLD_RAW_SOUND; + } + + private Menu getPlayerInventoryMenuToKeep(final @NotNull Optional holder, final @NotNull Menu menuToOpen) { + if (menuToOpen.options().playerInventoryMenu().isPresent()) { + return null; + } + + return holder.flatMap(MenuHolder::getRenderedPlayerInventoryMenu).orElse(null); + } +} diff --git a/src/main/java/com/extendedclip/deluxemenus/command/DeluxeMenusCommand.java b/src/main/java/com/extendedclip/deluxemenus/command/DeluxeMenusCommand.java index e360bde8..2c3d9236 100644 --- a/src/main/java/com/extendedclip/deluxemenus/command/DeluxeMenusCommand.java +++ b/src/main/java/com/extendedclip/deluxemenus/command/DeluxeMenusCommand.java @@ -91,6 +91,7 @@ private void registerSubCommands() { final List commands = List.of( new DumpCommand(plugin), new ExecuteCommand(plugin), + new ConvertCommandPanelsCommand(plugin), new HelpCommand(plugin), new ListCommand(plugin), new MetaCommand(plugin), diff --git a/src/main/java/com/extendedclip/deluxemenus/command/subcommand/ConvertCommandPanelsCommand.java b/src/main/java/com/extendedclip/deluxemenus/command/subcommand/ConvertCommandPanelsCommand.java new file mode 100644 index 00000000..24fc1df4 --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/command/subcommand/ConvertCommandPanelsCommand.java @@ -0,0 +1,129 @@ +package com.extendedclip.deluxemenus.command.subcommand; + +import com.extendedclip.deluxemenus.DeluxeMenus; +import com.extendedclip.deluxemenus.commandpanels.CommandPanelsConversionResult; +import com.extendedclip.deluxemenus.commandpanels.CommandPanelsConverter; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.logging.Level; + +import static net.kyori.adventure.text.Component.text; + +public class ConvertCommandPanelsCommand extends SubCommand { + + private static final String CONVERT_PERMISSION = "deluxemenus.convertcommandpanels"; + + public ConvertCommandPanelsCommand(final @NotNull DeluxeMenus plugin) { + super(plugin); + } + + @Override + public @NotNull String getName() { + return "convertcommandpanels"; + } + + @Override + public void execute(final @NotNull CommandSender sender, final @NotNull List arguments) { + if (!sender.hasPermission(CONVERT_PERMISSION)) { + plugin.sms(sender, com.extendedclip.deluxemenus.utils.Messages.NO_PERMISSION); + return; + } + + final ParsedArguments parsedArguments = parseArguments(arguments); + + try { + final CommandPanelsConverter converter = new CommandPanelsConverter(plugin); + final CommandPanelsConversionResult result = parsedArguments.sourcePath.isBlank() + ? converter.convertDefault(parsedArguments.outputFolder) + : converter.convert(new File(stripQuotes(parsedArguments.sourcePath)), parsedArguments.outputFolder); + + plugin.sms(sender, text("Converted ", NamedTextColor.GREEN) + .append(text(result.menusConverted(), NamedTextColor.WHITE)) + .append(text(" CommandPanels menus from ", NamedTextColor.GREEN)) + .append(text(result.filesRead(), NamedTextColor.WHITE)) + .append(text(" files.", NamedTextColor.GREEN))); + + if (result.menusSkipped() > 0) { + plugin.sms(sender, text("Skipped " + result.menusSkipped() + " menus. Check console for details.", NamedTextColor.YELLOW)); + } + + for (final String warning : result.warnings()) { + plugin.getLogger().warning("[CommandPanels converter] " + warning); + } + } catch (final IOException exception) { + plugin.sms(sender, Component.text("CommandPanels conversion failed: " + exception.getMessage(), NamedTextColor.RED)); + } catch (final RuntimeException exception) { + plugin.getLogger().log(Level.SEVERE, "Unexpected error while converting CommandPanels menus.", exception); + plugin.sms(sender, Component.text("CommandPanels conversion failed. Check console for details.", NamedTextColor.RED)); + } + } + + @Override + public @Nullable List onTabComplete(final @NotNull CommandSender sender, final @NotNull List arguments) { + if (!sender.hasPermission(CONVERT_PERMISSION)) { + return null; + } + + if (arguments.isEmpty()) { + return List.of(getName()); + } + + if (arguments.size() == 1 && getName().startsWith(arguments.get(0).toLowerCase())) { + return List.of(getName()); + } + + if (arguments.size() >= 2 && getName().equalsIgnoreCase(arguments.get(0)) && "--output".startsWith(arguments.get(arguments.size() - 1).toLowerCase())) { + return List.of("--output"); + } + + return null; + } + + private @NotNull ParsedArguments parseArguments(final @NotNull List arguments) { + String outputFolder = null; + final StringBuilder sourcePath = new StringBuilder(); + + for (int i = 0; i < arguments.size(); i++) { + final String argument = arguments.get(i); + + if (argument.equalsIgnoreCase("--output")) { + if (i + 1 < arguments.size()) { + outputFolder = arguments.get(i + 1); + } + i++; + continue; + } + + if (sourcePath.length() > 0) { + sourcePath.append(' '); + } + sourcePath.append(argument); + } + + return new ParsedArguments(sourcePath.toString(), outputFolder); + } + + private @NotNull String stripQuotes(final @NotNull String value) { + if (value.length() >= 2 && ((value.startsWith("\"") && value.endsWith("\"")) || (value.startsWith("'") && value.endsWith("'")))) { + return value.substring(1, value.length() - 1); + } + return value; + } + + private static class ParsedArguments { + private final String sourcePath; + private final String outputFolder; + + private ParsedArguments(final @NotNull String sourcePath, final @Nullable String outputFolder) { + this.sourcePath = sourcePath; + this.outputFolder = outputFolder; + } + } +} diff --git a/src/main/java/com/extendedclip/deluxemenus/commandpanels/CommandPanelsConversionResult.java b/src/main/java/com/extendedclip/deluxemenus/commandpanels/CommandPanelsConversionResult.java new file mode 100644 index 00000000..70b9014f --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/commandpanels/CommandPanelsConversionResult.java @@ -0,0 +1,47 @@ +package com.extendedclip.deluxemenus.commandpanels; + +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class CommandPanelsConversionResult { + + private int filesRead; + private int menusConverted; + private int menusSkipped; + private final List warnings = new ArrayList<>(); + + public void fileRead() { + filesRead++; + } + + public void menuConverted() { + menusConverted++; + } + + public void menuSkipped() { + menusSkipped++; + } + + public void warn(final @NotNull String warning) { + warnings.add(warning); + } + + public int filesRead() { + return filesRead; + } + + public int menusConverted() { + return menusConverted; + } + + public int menusSkipped() { + return menusSkipped; + } + + public @NotNull List warnings() { + return Collections.unmodifiableList(warnings); + } +} diff --git a/src/main/java/com/extendedclip/deluxemenus/commandpanels/CommandPanelsConverter.java b/src/main/java/com/extendedclip/deluxemenus/commandpanels/CommandPanelsConverter.java new file mode 100644 index 00000000..14445962 --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/commandpanels/CommandPanelsConverter.java @@ -0,0 +1,813 @@ +package com.extendedclip.deluxemenus.commandpanels; + +import com.extendedclip.deluxemenus.DeluxeMenus; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +public class CommandPanelsConverter { + + private static final String DEFAULT_OUTPUT_FOLDER = "commandpanels"; + + private final DeluxeMenus plugin; + + public CommandPanelsConverter(final @NotNull DeluxeMenus plugin) { + this.plugin = plugin; + } + + public @NotNull CommandPanelsConversionResult convertDefault(final @Nullable String outputFolder) throws IOException { + return convert(resolveDefaultSource(), outputFolder); + } + + public @NotNull CommandPanelsConversionResult convert(final @NotNull File source, final @Nullable String outputFolder) throws IOException { + final File panelsFolder = resolvePanelsFolder(source); + final File[] files = panelsFolder.listFiles((dir, name) -> name.toLowerCase(Locale.ROOT).endsWith(".yml")); + if (files == null || files.length == 0) { + throw new IOException("No .yml panel files were found in " + panelsFolder.getPath()); + } + + final String targetFolder = sanitizeFolder(outputFolder == null || outputFolder.isBlank() ? DEFAULT_OUTPUT_FOLDER : outputFolder); + final File outputDirectory = new File(plugin.getConfiguration().getMenuDirector(), targetFolder); + if (!outputDirectory.exists() && !outputDirectory.mkdirs()) { + throw new IOException("Could not create output folder " + outputDirectory.getPath()); + } + + final CommandPanelsConversionResult result = new CommandPanelsConversionResult(); + final YamlConfiguration deluxeConfig = (YamlConfiguration) plugin.getConfig(); + final List sortedFiles = new ArrayList<>(List.of(files)); + sortedFiles.sort(Comparator.comparing(File::getName)); + + for (final File file : sortedFiles) { + result.fileRead(); + final YamlConfiguration sourceConfig = YamlConfiguration.loadConfiguration(file); + convertFile(sourceConfig, file, outputDirectory, targetFolder, deluxeConfig, result); + } + + plugin.saveConfig(); + return result; + } + + private void convertFile( + final @NotNull YamlConfiguration sourceConfig, + final @NotNull File sourceFile, + final @NotNull File outputDirectory, + final @NotNull String outputFolder, + final @NotNull YamlConfiguration deluxeConfig, + final @NotNull CommandPanelsConversionResult result + ) throws IOException { + final ConfigurationSection legacyPanels = sourceConfig.getConfigurationSection("panels"); + if (legacyPanels != null) { + for (final String panelName : legacyPanels.getKeys(false)) { + final ConfigurationSection panel = legacyPanels.getConfigurationSection(panelName); + if (panel == null) { + result.menuSkipped(); + continue; + } + try { + saveConvertedPanel(panelName, panel, outputDirectory, outputFolder, deluxeConfig, result); + } catch (final RuntimeException exception) { + result.menuSkipped(); + result.warn("Skipped " + panelName + " in " + sourceFile.getName() + ": " + exception.getMessage()); + } + } + return; + } + + final String panelName = sourceConfig.getString("panel", stripExtension(sourceFile.getName())); + try { + saveConvertedPanel(panelName, sourceConfig, outputDirectory, outputFolder, deluxeConfig, result); + } catch (final RuntimeException exception) { + result.menuSkipped(); + result.warn("Skipped " + panelName + " in " + sourceFile.getName() + ": " + exception.getMessage()); + } + } + + private void saveConvertedPanel( + final @NotNull String rawPanelName, + final @NotNull ConfigurationSection panel, + final @NotNull File outputDirectory, + final @NotNull String outputFolder, + final @NotNull YamlConfiguration deluxeConfig, + final @NotNull CommandPanelsConversionResult result + ) throws IOException { + final String menuName = sanitizeMenuName(rawPanelName); + final YamlConfiguration converted = convertPanel(menuName, panel, result); + if (converted.getConfigurationSection("items") == null) { + result.menuSkipped(); + result.warn("Skipped " + rawPanelName + " because it did not contain convertible items."); + return; + } + + final File outputFile = new File(outputDirectory, menuName + ".yml"); + converted.save(outputFile); + deluxeConfig.set("gui_menus." + menuName + ".file", outputFolder + "/" + outputFile.getName()); + result.menuConverted(); + } + + private @NotNull YamlConfiguration convertPanel( + final @NotNull String menuName, + final @NotNull ConfigurationSection panel, + final @NotNull CommandPanelsConversionResult result + ) { + final YamlConfiguration out = new YamlConfiguration(); + out.options().indent(2); + + out.set("menu_title", translateText(panel.getString("title", panel.getString("menu_title", menuName)))); + + final int size = resolveSize(panel, result, menuName); + out.set("size", size); + + final List openCommands = getOpenCommands(panel); + if (!openCommands.isEmpty()) { + out.set("open_command", openCommands); + } + + final int refreshInterval = panel.getInt("refresh-delay", panel.getInt("refresh_delay", panel.getInt("update-delay", panel.getInt("update_delay", -1)))); + if (refreshInterval > 0) { + out.set("refresh", true); + out.set("refresh_interval", refreshInterval); + out.set("update_interval", refreshInterval); + } + + if (panel.getBoolean("hide-player-inventory", panel.getBoolean("hide_player_inventory", false))) { + out.set("hide_player_inventory", true); + } + + final ConfigurationSection legacyItems = panel.getConfigurationSection("item"); + final ConfigurationSection modernItems = panel.getConfigurationSection("items"); + final List layout = getStringList(panel, "layout"); + + if (legacyItems != null) { + convertLegacyItems(out, legacyItems, result, menuName); + } else if (modernItems != null && !layout.isEmpty()) { + convertLayoutItems(out, modernItems, layout, result, menuName); + } else if (modernItems != null) { + convertNamedItems(out, modernItems, result, menuName); + } + + return out; + } + + private int resolveSize( + final @NotNull ConfigurationSection panel, + final @NotNull CommandPanelsConversionResult result, + final @NotNull String menuName + ) { + if (panel.isInt("rows")) { + return Math.max(1, Math.min(6, panel.getInt("rows"))) * 9; + } + + if (panel.isInt("size")) { + final int size = panel.getInt("size"); + return Math.max(9, Math.min(54, size - (size % 9))); + } + + final String rows = panel.getString("rows"); + if (rows != null) { + try { + return Math.max(1, Math.min(6, Integer.parseInt(rows))) * 9; + } catch (final NumberFormatException ignored) { + result.warn("Menu " + menuName + " uses non-chest rows value '" + rows + "'. It was converted as a 54-slot chest."); + } + } + + return 54; + } + + private void convertLegacyItems( + final @NotNull YamlConfiguration out, + final @NotNull ConfigurationSection items, + final @NotNull CommandPanelsConversionResult result, + final @NotNull String menuName + ) { + for (final String slotKey : items.getKeys(false)) { + final int slot = parseSlot(slotKey, result, menuName); + if (slot < 0) { + continue; + } + + final ConfigurationSection item = items.getConfigurationSection(slotKey); + if (item == null) { + continue; + } + + int priority = 1; + for (final String key : item.getKeys(false)) { + if (!key.toLowerCase(Locale.ROOT).matches("has\\d+")) { + continue; + } + + final ConfigurationSection variant = item.getConfigurationSection(key); + if (variant == null) { + continue; + } + + final String itemPath = "items." + itemName(slot, key) + "."; + convertItem(out, itemPath, variant, slot, priority++, result, menuName); + addLegacyHasRequirements(out, itemPath + "view_requirement", variant); + } + + convertItem(out, "items." + itemName(slot, "base") + ".", item, slot, priority, result, menuName); + } + } + + private void convertLayoutItems( + final @NotNull YamlConfiguration out, + final @NotNull ConfigurationSection items, + final @NotNull List layout, + final @NotNull CommandPanelsConversionResult result, + final @NotNull String menuName + ) { + final Set occupiedSlots = new HashSet<>(); + int slot = 0; + for (final String line : layout) { + for (final String token : tokenizeLayout(line)) { + if (token.equalsIgnoreCase("empty") || token.equalsIgnoreCase("air") || token.equals("-")) { + slot++; + continue; + } + + final ConfigurationSection item = items.getConfigurationSection(token); + if (item == null) { + result.warn("Menu " + menuName + " references unknown CommandPanels layout item '" + token + "'."); + slot++; + continue; + } + + convertItem(out, "items." + itemName(slot, token) + ".", item, slot, 1, result, menuName); + occupiedSlots.add(slot); + slot++; + } + } + + final String fillItemName = getFillItemName(items); + if (fillItemName == null) { + return; + } + + final ConfigurationSection fillItem = items.getConfigurationSection(fillItemName); + if (fillItem == null) { + return; + } + + for (int index = 0; index < slot; index++) { + if (occupiedSlots.contains(index)) { + continue; + } + + convertItem(out, "items." + itemName(index, "fill") + ".", fillItem, index, 1, result, menuName); + } + } + + private void convertNamedItems( + final @NotNull YamlConfiguration out, + final @NotNull ConfigurationSection items, + final @NotNull CommandPanelsConversionResult result, + final @NotNull String menuName + ) { + for (final String key : items.getKeys(false)) { + final ConfigurationSection item = items.getConfigurationSection(key); + if (item == null || !item.isInt("slot")) { + continue; + } + + final int slot = item.getInt("slot"); + convertItem(out, "items." + itemName(slot, key) + ".", item, slot, 1, result, menuName); + } + } + + private void convertItem( + final @NotNull YamlConfiguration out, + final @NotNull String path, + final @NotNull ConfigurationSection item, + final int slot, + final int priority, + final @NotNull CommandPanelsConversionResult result, + final @NotNull String menuName + ) { + out.set(path + "material", convertMaterial(item.getString("material", "STONE"))); + out.set(path + "slot", slot); + out.set(path + "priority", priority); + + setIfPresent(out, path + "display_name", translateText(item.getString("name", item.getString("display-name", null)))); + setIfPresent(out, path + "lore", translateTextList(getStringList(item, "lore"))); + setIfPresent(out, path + "model_data", item.getString("customdata", item.getString("custom-model-data", item.getString("custom_model_data", null)))); + setIfPresent(out, path + "amount", item.getString("stack", item.getString("amount", null))); + setIfPresent(out, path + "damage", item.getString("damage", null)); + + if (item.getBoolean("tooltip", true) == false) { + out.set(path + "hide_tooltip", "true"); + } + + final List enchantments = convertEnchantments(getStringList(item, "enchantments")); + if (!enchantments.isEmpty()) { + out.set(path + "enchantments", enchantments); + } + + final List commands = convertCommands(getStringList(item, "commands")); + if (!commands.isEmpty()) { + out.set(path + "click_commands", commands); + } + + final List actions = convertCommands(getStringList(item, "actions.commands")); + if (!actions.isEmpty()) { + out.set(path + "click_commands", actions); + } + + final List leftActions = convertCommands(getStringList(item, "left-click.commands")); + if (!leftActions.isEmpty()) { + out.set(path + "left_click_commands", leftActions); + } + + final List rightActions = convertCommands(getStringList(item, "right-click.commands")); + if (!rightActions.isEmpty()) { + out.set(path + "right_click_commands", rightActions); + } + + final String conditions = item.getString("conditions", null); + if (conditions != null && !conditions.isBlank()) { + if (!addExpressionRequirements(out, path + "view_requirement", conditions)) { + result.warn("Menu " + menuName + " item at slot " + slot + " has an unsupported condition expression: " + conditions); + } + } + + final boolean update = containsPlaceholder(item.getString("name", "")) || getStringList(item, "lore").stream().anyMatch(this::containsPlaceholder); + if (update) { + out.set(path + "update", true); + } + } + + private boolean addExpressionRequirements(final @NotNull YamlConfiguration out, final @NotNull String path, final @NotNull String expression) { + final String connector; + if (expression.contains("$AND") && !expression.contains("$OR")) { + connector = "\\$AND"; + } else if (expression.contains("$OR") && !expression.contains("$AND")) { + connector = "\\$OR"; + out.set(path + ".minimum_requirements", 1); + } else { + connector = null; + } + + final String[] parts = connector == null ? new String[]{expression} : expression.split(connector); + int requirement = 1; + for (final String part : parts) { + if (!addExpressionRequirement(out, path + ".requirements.condition_" + requirement, part.trim())) { + return false; + } + requirement++; + } + return true; + } + + private boolean addExpressionRequirement(final @NotNull YamlConfiguration out, final @NotNull String path, final @NotNull String expression) { + final String cleanExpression = expression.replace("(", "").replace(")", "").trim(); + final boolean inverted = cleanExpression.startsWith("$NOT "); + final String normalized = inverted ? cleanExpression.substring(5).trim() : cleanExpression; + final String[] parts = normalized.split("\\s+", 3); + if (parts.length < 3) { + return false; + } + + final String input = translateText(parts[0]); + final String operator = parts[1].toUpperCase(Locale.ROOT); + final String output = translateText(parts[2]); + + switch (operator) { + case "$HASPERM": + out.set(path + ".type", inverted ? "!has permission" : "has permission"); + out.set(path + ".permission", output); + return true; + case "$EQUALS": + out.set(path + ".type", inverted ? "string does not equal ignorecase" : "string equals ignorecase"); + out.set(path + ".input", input); + out.set(path + ".output", output); + return true; + case "$ATLEAST": + out.set(path + ".type", inverted ? "<" : ">="); + out.set(path + ".input", input); + out.set(path + ".output", output); + return true; + default: + return false; + } + } + + private void addLegacyHasRequirements(final @NotNull YamlConfiguration out, final @NotNull String path, final @NotNull ConfigurationSection variant) { + int requirement = 1; + for (int index = 0; index < 20; index++) { + final String value = variant.getString("value" + index, null); + final String compare = variant.getString("compare" + index, null); + if (value == null || compare == null) { + continue; + } + + addLegacyComparison(out, path + ".requirements.condition_" + requirement, value, compare); + requirement++; + } + } + + private void addLegacyComparison( + final @NotNull YamlConfiguration out, + final @NotNull String path, + final @NotNull String rawValue, + final @NotNull String rawCompare + ) { + String value = translateText(rawValue.trim()); + String compare = translateText(rawCompare.trim()); + String type = "string equals ignorecase"; + String input; + String output; + + if (value.toUpperCase(Locale.ROOT).startsWith("NOT ")) { + type = "string does not equal ignorecase"; + input = compare; + output = value.substring(4).trim(); + } else if (value.toUpperCase(Locale.ROOT).endsWith(" ISGREATER")) { + type = ">"; + input = value.substring(0, value.length() - " ISGREATER".length()).trim(); + output = compare; + } else if (value.toUpperCase(Locale.ROOT).endsWith(" ISLESS")) { + type = "<"; + input = value.substring(0, value.length() - " ISLESS".length()).trim(); + output = compare; + } else if (value.toUpperCase(Locale.ROOT).endsWith(" ISEQUAL")) { + type = "string equals ignorecase"; + input = value.substring(0, value.length() - " ISEQUAL".length()).trim(); + output = compare; + } else if (value.toUpperCase(Locale.ROOT).endsWith(" ISNOTEQUAL")) { + type = "string does not equal ignorecase"; + input = value.substring(0, value.length() - " ISNOTEQUAL".length()).trim(); + output = compare; + } else if (containsPlaceholder(value) || !containsPlaceholder(compare)) { + input = value; + output = compare; + } else { + input = compare; + output = value; + } + + out.set(path + ".type", type); + out.set(path + ".input", input); + out.set(path + ".output", output); + } + + private @NotNull List convertCommands(final @NotNull List sourceCommands) { + final List commands = new ArrayList<>(); + + for (final String sourceCommand : sourceCommands) { + if (sourceCommand == null || sourceCommand.isBlank()) { + continue; + } + + final String command = translateText(sourceCommand.trim()); + final int equalsIndex = command.indexOf('='); + + if (command.equalsIgnoreCase("cpc") || command.equalsIgnoreCase("close") || command.equalsIgnoreCase("[close]")) { + commands.add("[close]"); + continue; + } + + if (command.startsWith("[")) { + commands.add(convertBracketCommand(command)); + continue; + } + + if (equalsIndex > 0) { + final String type = command.substring(0, equalsIndex).trim().toLowerCase(Locale.ROOT); + final String executable = command.substring(equalsIndex + 1).trim(); + + switch (type) { + case "open": + commands.add("[openguimenu] " + executable); + continue; + case "open_gui_inventory": + case "openguiinventory": + case "open_inventory": + case "openinventory": + commands.add("[open_gui_inventory] " + executable); + continue; + case "sound": + commands.add("[sound] " + executable); + continue; + case "console": + commands.add("[console] " + executable); + continue; + case "msg": + case "message": + commands.add((looksMiniMessage(executable) ? "[minimessage] " : "[message] ") + executable); + continue; + case "minimessage": + case "mini_message": + commands.add("[minimessage] " + executable); + continue; + case "server": + case "connect": + commands.add("[connect] " + executable); + continue; + case "chat": + commands.add("[chat] " + executable); + continue; + case "close": + commands.add("[close]"); + continue; + case "refresh": + commands.add("[refresh]"); + continue; + default: + commands.add("[player] " + stripSlash(command)); + continue; + } + } + + commands.add("[player] " + stripSlash(command)); + } + + return commands; + } + + private @NotNull String convertBracketCommand(final @NotNull String command) { + final int endIndex = command.indexOf(']'); + if (endIndex <= 1) { + return "[player] " + stripSlash(command); + } + + final String type = command.substring(1, endIndex).trim().toLowerCase(Locale.ROOT); + final String executable = command.substring(endIndex + 1).trim(); + + switch (type) { + case "open": + return "[openguimenu] " + executable; + case "open_gui_inventory": + case "openguiinventory": + case "open_inventory": + case "openinventory": + return "[open_gui_inventory] " + executable; + case "msg": + case "message": + return (looksMiniMessage(executable) ? "[minimessage] " : "[message] ") + executable; + case "server": + return "[connect] " + executable; + case "sound": + case "console": + case "chat": + case "close": + case "refresh": + return "[" + type + "]" + (executable.isBlank() ? "" : " " + executable); + default: + return "[player] " + stripSlash(command); + } + } + + private @NotNull String convertMaterial(final @NotNull String sourceMaterial) { + final String material = translateText(sourceMaterial.trim()); + final String lowerMaterial = material.toLowerCase(Locale.ROOT); + + if (containsPlaceholder(material)) { + return "placeholder-" + material; + } + + if (lowerMaterial.startsWith("cps=")) { + final String value = material.substring(material.indexOf('=') + 1).trim(); + return value.equalsIgnoreCase("self") ? "head-%player_name%" : "head-" + value; + } + + if (lowerMaterial.startsWith("head=") || lowerMaterial.startsWith("skull=")) { + return "head-" + material.substring(material.indexOf('=') + 1).trim(); + } + + if (lowerMaterial.startsWith("basehead=")) { + return "basehead-" + material.substring(material.indexOf('=') + 1).trim(); + } + + if (lowerMaterial.startsWith("hdb=")) { + return "hdb-" + material.substring(material.indexOf('=') + 1).trim(); + } + + if (lowerMaterial.startsWith("itemsadder=")) { + return "itemsadder-" + material.substring(material.indexOf('=') + 1).trim(); + } + + if (lowerMaterial.startsWith("nexo=")) { + return "nexo-" + material.substring(material.indexOf('=') + 1).trim(); + } + + if (lowerMaterial.startsWith("oraxen=")) { + return "oraxen-" + material.substring(material.indexOf('=') + 1).trim(); + } + + if (lowerMaterial.startsWith("craftengine=")) { + return "craftengine-" + material.substring(material.indexOf('=') + 1).trim(); + } + + if (lowerMaterial.startsWith("mmo=") || lowerMaterial.startsWith("mmoitems=")) { + final String value = material.substring(material.indexOf('=') + 1).trim().replace(' ', ':'); + return "mmoitems-" + value; + } + + return material.toUpperCase(Locale.ROOT); + } + + private @NotNull List convertEnchantments(final @NotNull List sourceEnchantments) { + final List enchantments = new ArrayList<>(); + for (final String enchantment : sourceEnchantments) { + final String[] parts = enchantment.trim().split("\\s+", 2); + if (parts.length == 0 || parts[0].isBlank()) { + continue; + } + + final String level = parts.length > 1 ? parts[1].trim() : "1"; + enchantments.add(parts[0].toUpperCase(Locale.ROOT) + ";" + level); + } + return enchantments; + } + + private @NotNull List getOpenCommands(final @NotNull ConfigurationSection panel) { + final List commands = new ArrayList<>(); + + if (panel.isList("commands")) { + commands.addAll(getStringList(panel, "commands")); + } + + if (panel.isString("command")) { + commands.add(panel.getString("command", "")); + } + + commands.addAll(getStringList(panel, "aliases")); + commands.removeIf(String::isBlank); + return commands; + } + + private @Nullable String getFillItemName(final @NotNull ConfigurationSection items) { + for (final String key : items.getKeys(false)) { + final ConfigurationSection item = items.getConfigurationSection(key); + if (item != null && item.getBoolean("fill", false)) { + return key; + } + } + return null; + } + + private @NotNull List tokenizeLayout(final @NotNull String line) { + final String trimmed = line.trim(); + if (trimmed.contains(" ")) { + return List.of(trimmed.split("\\s+")); + } + + final List tokens = new ArrayList<>(); + for (int i = 0; i < trimmed.length(); i++) { + tokens.add(String.valueOf(trimmed.charAt(i))); + } + return tokens; + } + + private @NotNull File resolvePanelsFolder(final @NotNull File source) throws IOException { + if (!source.exists()) { + throw new IOException("Source path does not exist: " + source.getPath()); + } + + if (source.isFile()) { + return source.getParentFile(); + } + + final File nestedPanels = new File(source, "panels"); + if (nestedPanels.isDirectory()) { + return nestedPanels; + } + + return source; + } + + private @NotNull File resolveDefaultSource() throws IOException { + final File converterDirectory = plugin.getConfiguration().getConverterDirectory(); + if (!converterDirectory.exists() && !converterDirectory.mkdirs()) { + throw new IOException("Could not create converter folder " + converterDirectory.getPath()); + } + + final List candidates = List.of( + new File(converterDirectory, "CommandPanels"), + new File(converterDirectory, "commandpanels"), + new File(converterDirectory, "panels"), + converterDirectory + ); + + for (final File candidate : candidates) { + if (!candidate.exists()) { + continue; + } + + final File panelsFolder = resolvePanelsFolder(candidate); + final File[] files = panelsFolder.listFiles((dir, name) -> name.toLowerCase(Locale.ROOT).endsWith(".yml")); + if (files != null && files.length > 0) { + return candidate; + } + } + + throw new IOException("Put your CommandPanels folder into " + converterDirectory.getPath() + " and run /dm convertcommandpanels again."); + } + + private int parseSlot(final @NotNull String key, final @NotNull CommandPanelsConversionResult result, final @NotNull String menuName) { + try { + return Integer.parseInt(key); + } catch (final NumberFormatException exception) { + result.warn("Menu " + menuName + " contains non-numeric legacy item slot '" + key + "'."); + return -1; + } + } + + private @NotNull String itemName(final int slot, final @NotNull String suffix) { + return "slot_" + slot + "_" + sanitizeMenuName(suffix); + } + + private @NotNull String stripExtension(final @NotNull String filename) { + final int index = filename.lastIndexOf('.'); + return index == -1 ? filename : filename.substring(0, index); + } + + private @NotNull String sanitizeFolder(final @NotNull String name) { + return name.replace("\\", "/").replace("..", "").replaceAll("^/+", "").replaceAll("/+$", ""); + } + + private @NotNull String sanitizeMenuName(final @NotNull String name) { + final String sanitized = name.trim().toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9_-]", "_"); + return sanitized.isBlank() ? "menu" : sanitized; + } + + private void setIfPresent(final @NotNull YamlConfiguration out, final @NotNull String path, final @Nullable Object value) { + if (value == null) { + return; + } + + if (value instanceof List && ((List) value).isEmpty()) { + return; + } + + out.set(path, value); + } + + private @NotNull List getStringList(final @NotNull ConfigurationSection section, final @NotNull String path) { + if (section.isList(path)) { + return section.getStringList(path); + } + + if (section.isString(path)) { + return List.of(section.getString(path, "")); + } + + return List.of(); + } + + private @NotNull List translateTextList(final @NotNull List source) { + final List translated = new ArrayList<>(); + for (final String line : source) { + if (line == null) { + continue; + } + + translated.add(translateText(line)); + } + return translated; + } + + private @Nullable String translateText(final @Nullable String source) { + if (source == null) { + return null; + } + + return source + .replace("%cp-player-name%", "%player_name%") + .replace("%cp-player-uuid%", "%player_uuid%") + .replace("%cp-player-world%", "%player_world%"); + } + + private boolean containsPlaceholder(final @NotNull String input) { + return input.contains("%"); + } + + private boolean looksMiniMessage(final @NotNull String input) { + final String lowerInput = input.toLowerCase(Locale.ROOT); + return lowerInput.contains("<#") + || lowerInput.contains("") + || lowerInput.contains("") + || lowerInput.contains("") + || lowerInput.contains("") + || lowerInput.contains("") + || lowerInput.contains("") + || lowerInput.contains(""); + } + + private @NotNull String stripSlash(final @NotNull String command) { + return command.startsWith("/") ? command.substring(1) : command; + } +} diff --git a/src/main/java/com/extendedclip/deluxemenus/config/DeluxeMenusConfig.java b/src/main/java/com/extendedclip/deluxemenus/config/DeluxeMenusConfig.java index 0baa68f5..83414dcd 100644 --- a/src/main/java/com/extendedclip/deluxemenus/config/DeluxeMenusConfig.java +++ b/src/main/java/com/extendedclip/deluxemenus/config/DeluxeMenusConfig.java @@ -96,6 +96,8 @@ public class DeluxeMenusConfig { private final String separator = File.separator; private final File menuDirectory; + private final File subMenuDirectory; + private final File converterDirectory; private final DeluxeMenus plugin; private final List exampleMenus = Arrays.asList("basics_menu", "advanced_menu", "requirements_menu" // more example menus here @@ -106,12 +108,18 @@ public DeluxeMenusConfig(@NotNull final DeluxeMenus plugin) { this.plugin = plugin; menuDirectory = new File(this.plugin.getDataFolder() + separator + "gui_menus"); + subMenuDirectory = new File(this.plugin.getDataFolder() + separator + "sub_menu"); + converterDirectory = new File(this.plugin.getDataFolder() + separator + "converter"); try { if (menuDirectory.mkdirs()) { plugin.debug(DebugLevel.HIGH, Level.INFO, "Individual menus directory did not exist.", "Created directory: plugins" + separator + "DeluxeMenus" + separator + "gui_menus"); } + if (subMenuDirectory.mkdirs()) { + plugin.debug(DebugLevel.HIGH, Level.INFO, "Sub menu directory did not exist.", "Created directory: plugins" + separator + "DeluxeMenus" + separator + "sub_menu"); + } + converterDirectory.mkdirs(); } catch (SecurityException e) { - plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Something went wrong while creating directory: plugins" + separator + "DeluxeMenus" + separator + "gui_menus"); + plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Something went wrong while creating DeluxeMenus directories."); } } @@ -169,6 +177,7 @@ public boolean loadDefConfig() { c.addDefault("check_updates", true); c.addDefault("use_admin_commands_in_menus_list", false); c.addDefault("menus_list_page_size", 10); + c.addDefault("sub_menus", new HashMap<>()); c.options().copyDefaults(true); if (!c.contains("gui_menus")) { @@ -331,19 +340,64 @@ public int loadGUIMenus() { loadMenu(c, key, true, "config"); } } + loadSubMenus(); return Menu.getLoadedMenuSize(); } + public int loadSubMenus() { + + if (checkConfig(null, "config.yml", false) == null) { + return 0; + } + + FileConfiguration c = plugin.getConfig(); + + if (!c.contains("sub_menus")) { + return 0; + } + + if (!c.isConfigurationSection("sub_menus")) { + return 0; + } + + Set keys = c.getConfigurationSection("sub_menus").getKeys(false); + + if (keys.isEmpty()) { + return 0; + } + + for (String key : keys) { + + if (c.contains("sub_menus." + key + ".file")) { + loadSubMenuFromFile(key); + } else { + loadMenu(c, key, true, "config", true); + } + } + + return Menu.getLoadedSubMenuSize(); + } + public boolean loadMenuFromFile(String menuName) { + return loadMenuFromFile(menuName, false); + } - String fileName = plugin.getConfig().getString("gui_menus." + menuName + ".file"); + public boolean loadSubMenuFromFile(String menuName) { + return loadMenuFromFile(menuName, true); + } + + private boolean loadMenuFromFile(String menuName, final boolean subMenu) { + + final String configRoot = subMenu ? "sub_menus" : "gui_menus"; + final File directory = subMenu ? subMenuDirectory : menuDirectory; + String fileName = plugin.getConfig().getString(configRoot + "." + menuName + ".file"); if (!fileName.endsWith(".yml")) { plugin.debug(DebugLevel.HIGHEST, Level.SEVERE, "Filename specified for menu: " + menuName + " is not a .yml file!", "Make sure that the file name to load this menu from is specified as a .yml file!", "Skipping loading of menu: " + menuName); return false; } - File f = new File(menuDirectory.getPath(), fileName); + File f = new File(directory.getPath(), fileName); if (!f.exists()) { plugin.debug(DebugLevel.HIGHEST, Level.INFO, f.getName() + " does not exist!"); @@ -359,7 +413,7 @@ public boolean loadMenuFromFile(String menuName) { } plugin.debug(DebugLevel.HIGHEST, Level.INFO, f.getName() + " created! Add your menu options to this file and use /dm reload to load it!"); } catch (IOException e) { - plugin.debug(DebugLevel.HIGHEST, Level.SEVERE, "Could not create menu file: plugins" + separator + "DeluxeMenus" + separator + "gui_menus" + separator + fileName); + plugin.debug(DebugLevel.HIGHEST, Level.SEVERE, "Could not create menu file: plugins" + separator + "DeluxeMenus" + separator + directory.getName() + separator + fileName); return false; } } @@ -377,31 +431,35 @@ public boolean loadMenuFromFile(String menuName) { return false; } - final Path guiMenusPath = menuDirectory.toPath(); + final Path guiMenusPath = directory.toPath(); final Path menuPath = f.toPath(); final Path relativePath = guiMenusPath.relativize(menuPath); - loadMenu(cfg, menuName, false, relativePath.toString()); - return Menu.getMenuByName(menuName).isPresent(); + loadMenu(cfg, menuName, false, relativePath.toString(), subMenu); + return subMenu ? Menu.getSubMenuByName(menuName).isPresent() : Menu.getMenuByName(menuName).isPresent(); } public void loadMenu(FileConfiguration c, String key, boolean mainConfig, final @NotNull String path) { + loadMenu(c, key, mainConfig, path, false); + } + + private void loadMenu(FileConfiguration c, String key, boolean mainConfig, final @NotNull String path, final boolean subMenu) { if (mainConfig) { plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Menu: " + key + " does not have a file specified in config.yml! Creating menus in the " + "config.yml file is deprecated and will be removed in a future version! Please migrate your " + "menus to individual files in the gui_menus directory! For more information see: " + "https://wiki.helpch.at/clips-plugins/deluxemenus/external-menus"); } - String pre = "gui_menus." + key + "."; + String pre = (subMenu ? "sub_menus." : "gui_menus.") + key + "."; if (!mainConfig) { pre = ""; } - if (!c.contains(pre + "menu_title")) { + if (!subMenu && !c.contains(pre + "menu_title")) { plugin.debug(DebugLevel.HIGHEST, Level.SEVERE, "Menu title for menu: " + key + " is not present!", "Skipping menu: " + key); return; } - String title = null; + String title = subMenu ? key : null; if (c.isString(pre + "menu_title")) { title = c.getString(pre + "menu_title"); @@ -418,7 +476,7 @@ public void loadMenu(FileConfiguration c, String key, boolean mainConfig, final InventoryType type = InventoryType.CHEST; - if (c.contains(pre + "inventory_type")) { + if (!subMenu && c.contains(pre + "inventory_type")) { try { final InventoryType inventoryType = InventoryType.valueOf(c.getString(pre + "inventory_type").toUpperCase()); type = !VALID_INVENTORY_TYPES.contains(inventoryType) ? InventoryType.CHEST : inventoryType; @@ -428,10 +486,11 @@ public void loadMenu(FileConfiguration c, String key, boolean mainConfig, final } builder.type(type); + builder.subMenu(subMenu); final List openCommands = new ArrayList<>(); - if (c.contains(pre + "open_command")) { + if (!subMenu && c.contains(pre + "open_command")) { if (c.isString(pre + "open_command") && !c.getString(pre + "open_command").isEmpty()) { String cmd = c.getString(pre + "open_command"); @@ -498,10 +557,10 @@ public void loadMenu(FileConfiguration c, String key, boolean mainConfig, final builder.argumentRequirements(argumentRequirements); builder.argumentsUsageMessage(c.getString(pre + "args_usage_message", null)); - int size = 54; + int size = subMenu ? 36 : 54; if (type == InventoryType.CHEST) { if (!c.contains(pre + "size")) { - plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Menu size for menu: " + key + " is not present!", "Using default size of 54"); + plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Menu size for menu: " + key + " is not present!", "Using default size of " + size); } else { size = c.getInt(pre + "size"); @@ -514,14 +573,16 @@ public void loadMenu(FileConfiguration c, String key, boolean mainConfig, final size = 9; } - if (size > 54) { - plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Menu size for menu: " + key + " is higher than 54", "Defaulting to 54."); - size = 54; + final int maxSize = subMenu ? 36 : 54; + if (size > maxSize) { + plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Menu size for menu: " + key + " is higher than " + maxSize, "Defaulting to " + maxSize + "."); + size = maxSize; } if (size % 9 != 0) { - plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Menu size for menu: " + key + " is not a multiple of 9", "Defaulting to 54."); - size = 54; + final int defaultSize = subMenu ? 36 : 54; + plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Menu size for menu: " + key + " is not a multiple of 9", "Defaulting to " + defaultSize + "."); + size = defaultSize; } } } else { @@ -555,7 +616,12 @@ public void loadMenu(FileConfiguration c, String key, boolean mainConfig, final final boolean refresh = c.getBoolean(pre + "refresh", false); builder.refresh(refresh); - Map> items = loadMenuItems(c, key, mainConfig); + builder.hidePlayerInventory(!subMenu && c.getBoolean(pre + "hide_player_inventory", false)); + if (!subMenu) { + builder.playerInventoryMenu(c.getString(pre + "player_inventory_menu", c.getString(pre + "bottom_menu", null))); + } + + Map> items = loadMenuItems(c, key, mainConfig, subMenu); if (items == null || items.isEmpty()) { plugin.debug(DebugLevel.HIGHEST, Level.SEVERE, "Failed to load menu items for menu: " + key, "Skipping menu: " + key); @@ -571,7 +637,11 @@ public void loadMenu(FileConfiguration c, String key, boolean mainConfig, final } private Map> loadMenuItems(FileConfiguration c, String name, boolean mainConfig) { - String itemsPath = "gui_menus." + name + ".items"; + return loadMenuItems(c, name, mainConfig, false); + } + + private Map> loadMenuItems(FileConfiguration c, String name, boolean mainConfig, final boolean subMenu) { + String itemsPath = (subMenu ? "sub_menus." : "gui_menus.") + name + ".items"; if (!mainConfig) { itemsPath = "items"; @@ -829,6 +899,11 @@ private Map> loadMenuItems(FileConfiguration final MenuItem menuItem = new MenuItem(plugin, builder.build()); for (int slot : slots) { + if (subMenu && (slot < 0 || slot >= 36)) { + plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Slot " + slot + " for item: " + key + " in sub menu: " + name + " is outside 0-35.", "Skipping slot: " + slot); + continue; + } + TreeMap slotPriorityMap; if ((!menuItems.containsKey(slot)) || menuItems.get(slot) == null) { slotPriorityMap = new TreeMap<>(); @@ -1284,6 +1359,14 @@ public File getMenuDirector() { return menuDirectory; } + public File getSubMenuDirectory() { + return subMenuDirectory; + } + + public File getConverterDirectory() { + return converterDirectory; + } + public void addEnchantmentsOptionToBuilder( final FileConfiguration c, final String currentPath, diff --git a/src/main/java/com/extendedclip/deluxemenus/listener/PlayerListener.java b/src/main/java/com/extendedclip/deluxemenus/listener/PlayerListener.java index f2d7d5f3..224f07f8 100644 --- a/src/main/java/com/extendedclip/deluxemenus/listener/PlayerListener.java +++ b/src/main/java/com/extendedclip/deluxemenus/listener/PlayerListener.java @@ -15,22 +15,23 @@ import org.bukkit.event.inventory.ClickType; import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.inventory.InventoryCloseEvent; -import org.bukkit.event.inventory.InventoryOpenEvent; -import org.bukkit.event.player.PlayerCommandPreprocessEvent; -import org.bukkit.event.player.PlayerQuitEvent; -import org.jetbrains.annotations.NotNull; - -import java.util.Optional; -import java.util.UUID; -import java.util.concurrent.TimeUnit; +import org.bukkit.event.inventory.InventoryOpenEvent; +import org.bukkit.event.player.PlayerCommandPreprocessEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.event.entity.PlayerDeathEvent; +import org.jetbrains.annotations.NotNull; + +import org.bukkit.inventory.ItemStack; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.TimeUnit; public class PlayerListener extends Listener { private final TaskScheduler scheduler; private final Cache cache = CacheBuilder.newBuilder().expireAfterWrite(75, TimeUnit.MILLISECONDS).build(); - // This is so dumb. Mojang fix your shit. - private final Cache shiftCache = CacheBuilder.newBuilder().expireAfterWrite(200, TimeUnit.MILLISECONDS).build(); + private final Cache shiftCache = CacheBuilder.newBuilder().expireAfterWrite(200, TimeUnit.MILLISECONDS).build(); public PlayerListener(@NotNull final DeluxeMenus plugin) { super(plugin); @@ -59,13 +60,36 @@ public void onCommandExecute(PlayerCommandPreprocessEvent event) { } @EventHandler - public void onLeave(PlayerQuitEvent event) { - Player player = event.getPlayer(); - - if (Menu.isInMenu(player)) { - Menu.closeMenu(plugin, player, false); - } - } + public void onLeave(PlayerQuitEvent event) { + Player player = event.getPlayer(); + + if (Menu.isInMenu(player)) { + Menu.closeMenu(plugin, player, false); + } + } + + @EventHandler(priority = EventPriority.LOWEST) + public void onDeath(PlayerDeathEvent event) { + final Player player = event.getEntity(); + final Optional optionalHolder = Menu.getMenuHolder(player); + + if (optionalHolder.isEmpty()) { + return; + } + + final MenuHolder holder = optionalHolder.get(); + if (!holder.isPlayerInventoryHidden()) { + return; + } + + event.getDrops().removeIf(itemStack -> itemStack != null && plugin.getMenuItemMarker().isMarked(itemStack)); + if (!event.getKeepInventory()) { + for (final ItemStack itemStack : holder.getHiddenPlayerInventoryDrops()) { + event.getDrops().add(itemStack); + } + } + holder.restorePlayerInventory(); + } @EventHandler public void onOpen(InventoryOpenEvent event) { diff --git a/src/main/java/com/extendedclip/deluxemenus/menu/Menu.java b/src/main/java/com/extendedclip/deluxemenus/menu/Menu.java index 5056aecc..8426f2ff 100644 --- a/src/main/java/com/extendedclip/deluxemenus/menu/Menu.java +++ b/src/main/java/com/extendedclip/deluxemenus/menu/Menu.java @@ -11,9 +11,10 @@ import com.extendedclip.deluxemenus.utils.StringUtils; import java.util.ArrayList; import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Map; +import java.util.HashSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import java.util.Set; @@ -23,17 +24,19 @@ import java.util.logging.Level; import org.bukkit.Bukkit; import org.bukkit.entity.Player; -import org.bukkit.event.inventory.InventoryType; -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.ItemStack; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryView; +import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -public class Menu { - - private static final Map menus = new ConcurrentHashMap<>(); - private static final Set menuHolders = ConcurrentHashMap.newKeySet(); - private static final Map lastOpenedMenus = new ConcurrentHashMap<>(); +public class Menu { + + private static final Map menus = new ConcurrentHashMap<>(); + private static final Map subMenus = new ConcurrentHashMap<>(); + private static final Set menuHolders = ConcurrentHashMap.newKeySet(); + private static final Map lastOpenedMenus = new ConcurrentHashMap<>(); private final DeluxeMenus plugin; private final TaskScheduler scheduler; @@ -56,13 +59,17 @@ public Menu( this.items = items; this.path = path; - if (this.options.registerCommands()) { - this.command = new RegistrableMenuCommand(plugin, this); - this.command.register(); - } - - menus.put(this.options.name(), this); - } + if (!this.options.subMenu() && this.options.registerCommands()) { + this.command = new RegistrableMenuCommand(plugin, this); + this.command.register(); + } + + if (this.options.subMenu()) { + subMenus.put(this.options.name(), this); + } else { + menus.put(this.options.name(), this); + } + } public static void unload(final @NotNull DeluxeMenus plugin, final @NotNull String name) { for (Player p : Bukkit.getOnlinePlayers()) { @@ -71,10 +78,11 @@ public static void unload(final @NotNull DeluxeMenus plugin, final @NotNull Stri } } - Optional optionalMenu = Menu.getMenuByName(name); - if (optionalMenu.isEmpty()) { - return; - } + Optional optionalMenu = Menu.getMenuByName(name); + if (optionalMenu.isEmpty()) { + subMenus.remove(name); + return; + } optionalMenu.get().unregisterCommand(); menus.remove(name); @@ -86,12 +94,16 @@ public static void unload(final @NotNull DeluxeMenus plugin) { closeMenu(plugin, p, true); } } - for (Menu menu : Menu.getAllMenus()) { - menu.unregisterCommand(); - } - menus.clear(); - menuHolders.clear(); - lastOpenedMenus.clear(); + for (Menu menu : Menu.getAllMenus()) { + menu.unregisterCommand(); + } + for (Menu menu : Menu.getAllSubMenus()) { + menu.unregisterCommand(); + } + menus.clear(); + subMenus.clear(); + menuHolders.clear(); + lastOpenedMenus.clear(); } private void unregisterCommand() { @@ -109,21 +121,30 @@ public static void unloadForShutdown(final @NotNull DeluxeMenus plugin) { if (isInMenu(player)) { closeMenuForShutdown(plugin, player); } - } - menus.clear(); - } - - public static int getLoadedMenuSize() { - return menus.size(); - } + } + menus.clear(); + subMenus.clear(); + } + + public static int getLoadedMenuSize() { + return menus.size(); + } + + public static int getLoadedSubMenuSize() { + return subMenus.size(); + } public static @NotNull Set getAllMenuNames() { return menus.keySet(); } - public static @NotNull Collection getAllMenus() { - return menus.values(); - } + public static @NotNull Collection getAllMenus() { + return menus.values(); + } + + public static @NotNull Collection getAllSubMenus() { + return subMenus.values(); + } // Menus need to be stored in a list because config.yml can contain multiple menus. // This can be changed once we remove support for menus inside the config file. @@ -144,9 +165,13 @@ public static int getLoadedMenuSize() { ); } - public static @NotNull Optional getMenuByName(final @NotNull String name) { - return menus.entrySet().stream().filter(e -> e.getKey().equalsIgnoreCase(name)).findFirst().map(Entry::getValue); - } + public static @NotNull Optional getMenuByName(final @NotNull String name) { + return menus.entrySet().stream().filter(e -> e.getKey().equalsIgnoreCase(name)).findFirst().map(Entry::getValue); + } + + public static @NotNull Optional getSubMenuByName(final @NotNull String name) { + return subMenus.entrySet().stream().filter(e -> e.getKey().equalsIgnoreCase(name)).findFirst().map(Entry::getValue); + } public static @NotNull Optional getMenuByCommand(final @NotNull String command) { return menus.values().stream().filter(m -> m.getMenuCommandUsed(command).isPresent()).findFirst(); @@ -200,27 +225,33 @@ public static void closeMenu(final @NotNull DeluxeMenus plugin, final @NotNull P MenuHolder holder = optionalHolder.get(); holder.stopPlaceholderUpdate(); - holder.stopRefreshTask(); - - if (executeCloseActions) { - holder.getMenu().map(Menu::options).map(MenuOptions::closeHandler).flatMap(h -> h).ifPresent(h -> h.onClick(holder)); - } - - if (close) { - plugin.getScheduler().runTask(player, () -> { - player.closeInventory(); - cleanInventory(plugin, player); - }); - } - menuHolders.remove(holder); - lastOpenedMenus.put(player.getUniqueId(), holder.getMenu().orElse(null)); - } - - public static void closeMenuForShutdown(final @NotNull DeluxeMenus plugin, final @NotNull Player player) { - getMenuHolder(player).ifPresent(MenuHolder::stopPlaceholderUpdate); - - player.closeInventory(); - cleanInventory(plugin, player); + holder.stopRefreshTask(); + + if (executeCloseActions) { + holder.getMenu().map(Menu::options).map(MenuOptions::closeHandler).flatMap(h -> h).ifPresent(h -> h.onClick(holder)); + } + + if (close) { + plugin.getScheduler().runTask(player, () -> { + holder.restorePlayerInventory(); + player.closeInventory(); + cleanInventory(plugin, player); + }); + } else { + holder.restorePlayerInventory(); + } + menuHolders.remove(holder); + lastOpenedMenus.put(player.getUniqueId(), holder.getMenu().orElse(null)); + } + + public static void closeMenuForShutdown(final @NotNull DeluxeMenus plugin, final @NotNull Player player) { + getMenuHolder(player).ifPresent(holder -> { + holder.stopPlaceholderUpdate(); + holder.restorePlayerInventory(); + }); + + player.closeInventory(); + cleanInventory(plugin, player); } public static void closeMenu(final @NotNull DeluxeMenus plugin, final @NotNull Player player, final boolean close) { @@ -272,14 +303,27 @@ private boolean handleArgRequirements(final @NotNull MenuHolder holder) { return true; } - public void openMenu(final @NotNull Player viewer) { - openMenu(viewer, null, null); - } - - public void openMenu(final @NotNull Player viewer, final @Nullable Map args, final @Nullable Player placeholderPlayer) { - if (items == null || items.isEmpty()) { - return; - } + public void openMenu(final @NotNull Player viewer) { + openMenu(viewer, null, null); + } + + public void openMenu(final @NotNull Player viewer, final @Nullable Map args, final @Nullable Player placeholderPlayer) { + openMenu(viewer, args, placeholderPlayer, null); + } + + public void openMenu( + final @NotNull Player viewer, + final @Nullable Map args, + final @Nullable Player placeholderPlayer, + final @Nullable Menu playerInventoryMenu + ) { + if (this.options.subMenu()) { + return; + } + + if (items == null || items.isEmpty()) { + return; + } DeluxeMenusPreOpenMenuEvent preOpenEvent = new DeluxeMenusPreOpenMenuEvent(viewer); Bukkit.getPluginManager().callEvent(preOpenEvent); @@ -292,12 +336,15 @@ public void openMenu(final @NotNull Player viewer, final @Nullable Map { - Set activeItems = new HashSet<>(); - - for (Entry> entry : items.entrySet()) { - - for (MenuItem item : entry.getValue().values()) { - - int slot = item.options().slot(); - - if (slot >= this.options.size()) { - plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Item set to slot " + slot + " for menu: " + this.options.name() + " exceeds the inventory size!", - "This item will not be added to the menu!" - ); - continue; - } - - if (item.options().viewRequirements().isPresent()) { - - if (item.options().viewRequirements().get().evaluate(holder)) { - - activeItems.add(item); - break; - } - } else { - - activeItems.add(item); - break; - } - } - } - - if (activeItems.isEmpty()) { - return; - } + Set activeItems = getActiveItems(holder); + + if (activeItems.isEmpty()) { + return; + } holder.setMenuName(this.options.name()); holder.setActiveItems(activeItems); this.options.openHandler().ifPresent(h -> h.onClick(holder)); - String title = StringUtils.color(holder.setPlaceholdersAndArguments(this.options.title())); - - Inventory inventory; + String title = StringUtils.color(holder.setPlaceholdersAndArguments(this.options.title())); + + Inventory inventory; if (this.options.type() != InventoryType.CHEST) { inventory = Bukkit.createInventory(holder, this.options.type(), title); @@ -358,11 +374,13 @@ public void openMenu(final @NotNull Player viewer, final @Nullable Map playerInventoryItems = new HashMap<>(); + final boolean renderPlayerInventory = rendersPlayerInventory(holder); + + for (MenuItem item : activeItems) { ItemStack iStack = item.getItemStack(holder); @@ -372,24 +390,28 @@ public void openMenu(final @NotNull Player viewer, final @Nullable Map= this.options.size()) { - plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, + int slot = item.options().slot(); + + if (slot >= this.options.size() + (renderPlayerInventory ? 36 : 0)) { + plugin.debug( + DebugLevel.HIGHEST, + Level.WARNING, "Item set to slot " + slot + " for menu: " + this.options.name() + " exceeds the inventory size!", "This item will not be added to the menu!" ); continue; } - if (item.options().updatePlaceholders()) { - update = true; - } - - inventory.setItem(item.options().slot(), iStack); - } + if (item.options().updatePlaceholders()) { + update = true; + } + + if (slot < this.options.size()) { + inventory.setItem(item.options().slot(), iStack); + } else { + playerInventoryItems.put(slot, iStack); + } + } final boolean updatePlaceholders = update; @@ -398,12 +420,19 @@ public void openMenu(final @NotNull Player viewer, final @Nullable Map menuHolder.getMenuName().equalsIgnoreCase(options.name())).forEach(MenuHolder::refreshMenu); - } + public void refreshForAll() { + menuHolders.stream().filter(menuHolder -> menuHolder.getMenuName().equalsIgnoreCase(options.name())).forEach(MenuHolder::refreshMenu); + } + + public @NotNull Set getActiveItems(final @NotNull MenuHolder holder) { + final Set activeItems = new HashSet<>(getActiveItems(holder, 0, this.options.size(), 0)); + final Optional openPlayerInventoryMenu = holder.getPlayerInventoryMenu(); + + if (!this.options.hidePlayerInventory() + && openPlayerInventoryMenu.isEmpty() + && this.options.playerInventoryMenu().isEmpty()) { + return activeItems; + } + + if (openPlayerInventoryMenu.isPresent()) { + activeItems.addAll(openPlayerInventoryMenu.get().getActiveItems(holder, 0, 36, this.options.size())); + return activeItems; + } + + if (this.options.playerInventoryMenu().isPresent()) { + final String playerInventoryMenuName = this.options.playerInventoryMenu().get(); + final Optional bottomMenu = Menu.getSubMenuByName(playerInventoryMenuName); + if (bottomMenu.isPresent()) { + activeItems.addAll(bottomMenu.get().getActiveItems(holder, 0, 36, this.options.size())); + } else { + plugin.debug( + DebugLevel.HIGHEST, + Level.WARNING, + "Player inventory menu " + playerInventoryMenuName + " for menu " + this.options.name() + " was not found." + ); + } + return activeItems; + } + + return activeItems; + } + + private boolean rendersPlayerInventory(final @NotNull MenuHolder holder) { + return this.options.hidePlayerInventory() + || holder.getPlayerInventoryMenu().isPresent() + || this.options.playerInventoryMenu().flatMap(Menu::getSubMenuByName).isPresent(); + } + + private @NotNull Set getActiveItems(final @NotNull MenuHolder holder, final int minimumSlot, final int maximumSlot, final int slotOffset) { + final Set activeItems = new HashSet<>(); + + for (Entry> entry : items.entrySet()) { + final int configuredSlot = entry.getKey(); + + if (configuredSlot < minimumSlot || configuredSlot >= maximumSlot) { + continue; + } + + for (MenuItem item : entry.getValue().values()) { + if (item.options().viewRequirements().isPresent()) { + if (!item.options().viewRequirements().get().evaluate(holder)) { + continue; + } + } + + final int renderedSlot = configuredSlot + slotOffset; + activeItems.add(new MenuItem(plugin, item.options().asBuilder().slot(renderedSlot).build())); + break; + } + } + + return activeItems; + } public @NotNull Map> getMenuItems() { return this.items; diff --git a/src/main/java/com/extendedclip/deluxemenus/menu/MenuHolder.java b/src/main/java/com/extendedclip/deluxemenus/menu/MenuHolder.java index 82c9be24..76b3ca19 100644 --- a/src/main/java/com/extendedclip/deluxemenus/menu/MenuHolder.java +++ b/src/main/java/com/extendedclip/deluxemenus/menu/MenuHolder.java @@ -5,17 +5,25 @@ import com.extendedclip.deluxemenus.scheduler.scheduling.schedulers.TaskScheduler; import com.extendedclip.deluxemenus.scheduler.scheduling.tasks.MyScheduledTask; import com.extendedclip.deluxemenus.utils.StringUtils; -import org.bukkit.entity.Player; -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.InventoryHolder; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.ItemMeta; -import org.jetbrains.annotations.NotNull; -import java.util.HashSet; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.TreeMap; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryHolder; +import org.bukkit.inventory.InventoryView; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.TreeMap; +import java.util.stream.Collectors; public class MenuHolder implements InventoryHolder { @@ -30,9 +38,11 @@ public class MenuHolder implements InventoryHolder { private MyScheduledTask refreshTask = null; private Inventory inventory; private boolean updating; - private boolean parsePlaceholdersInArguments; - private boolean parsePlaceholdersAfterArguments; - private Map typedArgs; + private boolean parsePlaceholdersInArguments; + private boolean parsePlaceholdersAfterArguments; + private Map typedArgs; + private ItemStack[] hiddenPlayerStorageContents; + private Menu playerInventoryMenu; public MenuHolder(final @NotNull DeluxeMenus plugin, final @NotNull Player viewer) { this.plugin = plugin; @@ -122,7 +132,7 @@ public Optional getMenu() { ); } - public void refreshMenu() { + public void refreshMenu() { Optional optionalMenu = getMenu(); if (optionalMenu.isEmpty()) { @@ -139,38 +149,24 @@ public void refreshMenu() { scheduler.runTaskAsynchronously(() -> { - final Set active = new HashSet<>(); - final Set slotsToClear = new HashSet<>(); - - for (int i = 0; i < getInventory().getSize(); i++) { - TreeMap e = menu.getMenuItems().get(i); - - if (e == null) { - slotsToClear.add(i); - continue; - } - - boolean matched = false; - for (MenuItem item : e.values()) { - - if (item.options().viewRequirements().isPresent()) { - - if (item.options().viewRequirements().get().evaluate(this)) { - matched = true; - active.add(item); - break; - } - } else { - matched = true; - active.add(item); - break; - } - } - - if (!matched) { - slotsToClear.add(i); - } - } + final Set active = menu.getActiveItems(this); + final Set slotsToClear = new HashSet<>(); + + for (int i = 0; i < getInventory().getSize(); i++) { + final int slot = i; + if (active.stream().noneMatch(item -> item.options().slot() == slot)) { + slotsToClear.add(i); + } + } + + if (isPlayerInventoryHidden()) { + for (int i = getInventory().getSize(); i < getInventory().getSize() + 36; i++) { + final int slot = i; + if (active.stream().noneMatch(item -> item.options().slot() == slot)) { + slotsToClear.add(i); + } + } + } if (active.isEmpty()) { scheduler.runTask(viewer, () -> Menu.closeMenu(plugin, viewer, true)); @@ -179,9 +175,9 @@ public void refreshMenu() { scheduler.runTask(viewer, () -> { - for (int slot : slotsToClear) { - getInventory().setItem(slot, null); - } + for (int slot : slotsToClear) { + setRenderedItem(slot, null); + } boolean update = false; @@ -197,16 +193,16 @@ public void refreshMenu() { int slot = item.options().slot(); - if (slot >= menu.options().size()) { - continue; - } + if (!isRenderedSlot(slot)) { + continue; + } if (item.options().updatePlaceholders()) { update = true; } - getInventory().setItem(item.options().slot(), iStack); - } + setRenderedItem(item.options().slot(), iStack); + } setActiveItems(active); @@ -289,11 +285,11 @@ public void startUpdatePlaceholdersTask() { if (item.options().updatePlaceholders()) { - ItemStack i = inventory.getItem(item.options().slot()); - - if (i == null) { - continue; - } + ItemStack i = getRenderedItem(item.options().slot()); + + if (i == null) { + continue; + } int amt = i.getAmount(); @@ -312,20 +308,21 @@ public void startUpdatePlaceholdersTask() { } } - ItemMeta meta = i.getItemMeta(); - - if (item.options().displayNameHasPlaceholders() && item.options().displayName().isPresent()) { - meta.setDisplayName(StringUtils.color(setPlaceholdersAndArguments(item.options().displayName().get()))); - } - - if (item.options().loreHasPlaceholders()) { - meta.setLore(item.getMenuItemLore(getHolder(), item.options().lore())); - } - - i.setItemMeta(meta); - i.setAmount(amt); - } - } + ItemMeta meta = i.getItemMeta(); + + if (item.options().displayNameHasPlaceholders() && item.options().displayName().isPresent()) { + meta.setDisplayName(StringUtils.color(setPlaceholdersAndArguments(item.options().displayName().get()))); + } + + if (item.options().loreHasPlaceholders()) { + meta.setLore(item.getMenuItemLore(getHolder(), item.options().lore())); + } + + i.setItemMeta(meta); + i.setAmount(amt); + setRenderedItem(item.options().slot(), i); + } + } }, initialDelay, period ); @@ -344,9 +341,152 @@ public void setUpdating(boolean updating) { return this.inventory; } - public void setInventory(Inventory i) { - this.inventory = i; - } + public void setInventory(Inventory i) { + this.inventory = i; + } + + public @NotNull Optional getPlayerInventoryMenu() { + return Optional.ofNullable(this.playerInventoryMenu); + } + + public @NotNull Optional getRenderedPlayerInventoryMenu() { + if (this.playerInventoryMenu != null) { + return Optional.of(this.playerInventoryMenu); + } + + return getMenu() + .flatMap(menu -> menu.options().playerInventoryMenu()) + .flatMap(Menu::getSubMenuByName); + } + + public void setPlayerInventoryMenu(final @Nullable Menu menu) { + this.playerInventoryMenu = menu; + } + + public void openPlayerInventoryMenu(final @NotNull Menu menu) { + if (this.inventory == null) { + return; + } + + if (!menu.options().subMenu()) { + return; + } + + this.playerInventoryMenu = menu; + if (!isPlayerInventoryHidden()) { + hidePlayerInventory(); + } + refreshMenu(); + } + + public boolean isPlayerInventoryHidden() { + return this.hiddenPlayerStorageContents != null; + } + + public void hidePlayerInventory() { + if (isPlayerInventoryHidden()) { + return; + } + + final ItemStack[] storageContents = viewer.getInventory().getStorageContents(); + this.hiddenPlayerStorageContents = cloneContents(storageContents); + viewer.getInventory().setStorageContents(new ItemStack[storageContents.length]); + viewer.updateInventory(); + } + + public void restorePlayerInventory() { + if (!isPlayerInventoryHidden()) { + return; + } + + viewer.getInventory().setStorageContents(cloneContents(this.hiddenPlayerStorageContents)); + this.hiddenPlayerStorageContents = null; + viewer.updateInventory(); + } + + public @NotNull List getHiddenPlayerInventoryDrops() { + if (!isPlayerInventoryHidden()) { + return List.of(); + } + + return Arrays.stream(this.hiddenPlayerStorageContents) + .filter(Objects::nonNull) + .map(ItemStack::clone) + .collect(Collectors.toList()); + } + + public void applyPlayerInventoryItems(final @NotNull Map items) { + if (!isPlayerInventoryHidden()) { + return; + } + + for (Map.Entry entry : items.entrySet()) { + setRenderedItem(entry.getKey(), entry.getValue()); + } + viewer.updateInventory(); + } + + public @Nullable ItemStack getRenderedItem(final int rawSlot) { + if (rawSlot < 0) { + return null; + } + + if (rawSlot < inventory.getSize()) { + return inventory.getItem(rawSlot); + } + + if (!isPlayerInventoryHidden()) { + return null; + } + + return viewer.getOpenInventory().getItem(rawSlot); + } + + public void setRenderedItem(final int rawSlot, final @Nullable ItemStack itemStack) { + if (rawSlot < 0) { + return; + } + + if (rawSlot < inventory.getSize()) { + inventory.setItem(rawSlot, itemStack); + return; + } + + if (!isPlayerInventoryHidden()) { + return; + } + + final InventoryView view = viewer.getOpenInventory(); + if (view == null || view.getType() == InventoryType.CRAFTING) { + return; + } + + if (rawSlot >= inventory.getSize() + 36) { + return; + } + + view.setItem(rawSlot, itemStack); + } + + public boolean isRenderedSlot(final int rawSlot) { + if (rawSlot < 0) { + return false; + } + + if (rawSlot < inventory.getSize()) { + return true; + } + + return isPlayerInventoryHidden() && rawSlot < inventory.getSize() + 36; + } + + private @NotNull ItemStack[] cloneContents(final @NotNull ItemStack[] contents) { + final ItemStack[] clone = new ItemStack[contents.length]; + for (int i = 0; i < contents.length; i++) { + clone[i] = contents[i] == null ? null : contents[i].clone(); + } + return clone; + } public Map getTypedArgs() { return typedArgs; diff --git a/src/main/java/com/extendedclip/deluxemenus/menu/MenuItem.java b/src/main/java/com/extendedclip/deluxemenus/menu/MenuItem.java index f106161a..a132eeb5 100644 --- a/src/main/java/com/extendedclip/deluxemenus/menu/MenuItem.java +++ b/src/main/java/com/extendedclip/deluxemenus/menu/MenuItem.java @@ -269,10 +269,10 @@ public ItemStack getItemStack(@NotNull final MenuHolder holder) { itemMeta.setCustomModelDataComponent(parseCustomModelDataComponent(this.options.customModelDataComponent().get(), itemMeta.getCustomModelDataComponent(), holder)); } - if (this.options.displayName().isPresent()) { - final String displayName = holder.setPlaceholdersAndArguments(this.options.displayName().get()); - itemMeta.setDisplayName(StringUtils.color(displayName)); - } + if (this.options.displayName().isPresent()) { + final String displayName = holder.setPlaceholdersAndArguments(this.options.displayName().get()); + itemMeta.setDisplayName(StringUtils.color(displayName)); + } List lore = new ArrayList<>(); // This checks if a lore should be kept from the hooked item, and then if a lore exists on the item @@ -571,8 +571,8 @@ private boolean isHeadItem(@NotNull final String material) { protected List getMenuItemLore(@NotNull final MenuHolder holder, @NotNull final List lore) { return lore.stream() - .map(holder::setPlaceholdersAndArguments) - .map(StringUtils::color) + .map(holder::setPlaceholdersAndArguments) + .map(StringUtils::color) .map(line -> line.split("\n")) .flatMap(Arrays::stream) .map(line -> line.split("\\\\n")) diff --git a/src/main/java/com/extendedclip/deluxemenus/menu/options/MenuOptions.java b/src/main/java/com/extendedclip/deluxemenus/menu/options/MenuOptions.java index c5c4df0a..9dadf291 100644 --- a/src/main/java/com/extendedclip/deluxemenus/menu/options/MenuOptions.java +++ b/src/main/java/com/extendedclip/deluxemenus/menu/options/MenuOptions.java @@ -18,9 +18,12 @@ public class MenuOptions { private final int updateInterval; private final int refreshInterval; private final boolean refresh; + private final boolean hidePlayerInventory; + private final boolean subMenu; private final boolean parsePlaceholdersInArguments; private final boolean parsePlaceholdersAfterArguments; private final boolean enableBypassPerm; + private final String playerInventoryMenu; private final List commands; private final boolean registerCommands; @@ -40,9 +43,12 @@ private MenuOptions(final @NotNull MenuOptionsBuilder builder) { this.updateInterval = builder.updateInterval; this.refreshInterval = builder.refreshInterval; this.refresh = builder.refresh; + this.hidePlayerInventory = builder.hidePlayerInventory; + this.subMenu = builder.subMenu; this.parsePlaceholdersInArguments = builder.parsePlaceholdersInArguments; this.parsePlaceholdersAfterArguments = builder.parsePlaceholdersAfterArguments; this.enableBypassPerm = builder.enableBypassPerm; + this.playerInventoryMenu = builder.playerInventoryMenu; this.commands = builder.commands; this.registerCommands = builder.registerCommands; @@ -87,6 +93,14 @@ public boolean refresh() { return this.refresh; } + public boolean hidePlayerInventory() { + return this.hidePlayerInventory; + } + + public boolean subMenu() { + return this.subMenu; + } + public boolean parsePlaceholdersInArguments() { return this.parsePlaceholdersInArguments; } @@ -99,6 +113,10 @@ public boolean enableBypassPerm() { return this.enableBypassPerm; } + public @NotNull Optional playerInventoryMenu() { + return Optional.ofNullable(this.playerInventoryMenu); + } + public @NotNull List<@NotNull String> commands() { return this.commands; } @@ -138,9 +156,12 @@ public boolean registerCommands() { .updateInterval(this.updateInterval) .refreshInterval(this.refreshInterval) .refresh(this.refresh) + .hidePlayerInventory(this.hidePlayerInventory) + .subMenu(this.subMenu) .parsePlaceholdersInArguments(this.parsePlaceholdersInArguments) .parsePlaceholdersAfterArguments(this.parsePlaceholdersAfterArguments) .enableBypassPerm(this.enableBypassPerm) + .playerInventoryMenu(this.playerInventoryMenu) .commands(this.commands) .registerCommands(this.registerCommands) .arguments(this.arguments) @@ -160,9 +181,12 @@ public static class MenuOptionsBuilder { private int updateInterval = 10; private int refreshInterval = 10; private boolean refresh; + private boolean hidePlayerInventory; + private boolean subMenu; private boolean parsePlaceholdersInArguments = false; private boolean parsePlaceholdersAfterArguments = false; private boolean enableBypassPerm = false; + private String playerInventoryMenu; private List commands = List.of(); private boolean registerCommands = false; @@ -214,6 +238,16 @@ public MenuOptionsBuilder refresh(final boolean refresh) { return this; } + public MenuOptionsBuilder hidePlayerInventory(final boolean hidePlayerInventory) { + this.hidePlayerInventory = hidePlayerInventory; + return this; + } + + public MenuOptionsBuilder subMenu(final boolean subMenu) { + this.subMenu = subMenu; + return this; + } + public MenuOptionsBuilder parsePlaceholdersInArguments(final boolean parsePlaceholdersInArguments) { this.parsePlaceholdersInArguments = parsePlaceholdersInArguments; return this; @@ -229,6 +263,11 @@ public MenuOptionsBuilder enableBypassPerm(final boolean enableBypassPerm) { return this; } + public MenuOptionsBuilder playerInventoryMenu(final @Nullable String playerInventoryMenu) { + this.playerInventoryMenu = playerInventoryMenu; + return this; + } + public MenuOptionsBuilder commands(final @NotNull List<@NotNull String> commands) { this.commands = commands; return this; diff --git a/src/main/java/com/extendedclip/deluxemenus/utils/Messages.java b/src/main/java/com/extendedclip/deluxemenus/utils/Messages.java index 01919b25..bcf8eeaf 100644 --- a/src/main/java/com/extendedclip/deluxemenus/utils/Messages.java +++ b/src/main/java/com/extendedclip/deluxemenus/utils/Messages.java @@ -73,12 +73,16 @@ public enum Messages { .append(text("/dm dump ", NamedTextColor.WHITE)) .append(newline()) .append(text(">", NamedTextColor.AQUA)) - .append(space().append(space())) - .append(text("/dm meta ", NamedTextColor.WHITE)) - .append(newline()) - .append(text(">", NamedTextColor.AQUA)) - .append(space().append(space())) - .append(text("/dm reload [menu-name]", NamedTextColor.WHITE))), + .append(space().append(space())) + .append(text("/dm meta ", NamedTextColor.WHITE)) + .append(newline()) + .append(text(">", NamedTextColor.AQUA)) + .append(space().append(space())) + .append(text("/dm convertcommandpanels [folder] [--output folder]", NamedTextColor.WHITE)) + .append(newline()) + .append(text(">", NamedTextColor.AQUA)) + .append(space().append(space())) + .append(text("/dm reload [menu-name]", NamedTextColor.WHITE))), NO_PERMISSION(text("You don't have permission to do that!", NamedTextColor.RED)), NO_PERMISSION_PLAYER_ARGUMENT(text("You don't have permission to use the argument -p:!", NamedTextColor.RED)), diff --git a/src/main/java/com/extendedclip/deluxemenus/utils/StringUtils.java b/src/main/java/com/extendedclip/deluxemenus/utils/StringUtils.java index 81fd30f8..f0536274 100644 --- a/src/main/java/com/extendedclip/deluxemenus/utils/StringUtils.java +++ b/src/main/java/com/extendedclip/deluxemenus/utils/StringUtils.java @@ -1,19 +1,27 @@ package com.extendedclip.deluxemenus.utils; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import me.clip.placeholderapi.PlaceholderAPI; -import net.md_5.bungee.api.ChatColor; -import org.bukkit.Color; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import me.clip.placeholderapi.PlaceholderAPI; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import net.md_5.bungee.api.ChatColor; +import org.bukkit.Color; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; public class StringUtils { - private final static Pattern HEX_PATTERN = Pattern - .compile("&(#[a-f0-9]{6})", Pattern.CASE_INSENSITIVE); + private final static Pattern HEX_PATTERN = Pattern + .compile("&(#[a-f0-9]{6})", Pattern.CASE_INSENSITIVE); + private final static Pattern MINI_MESSAGE_PATTERN = Pattern.compile( + "<(/?(#[a-f0-9]{6}|black|dark_blue|dark_green|dark_aqua|dark_red|dark_purple|gold|gray|dark_gray|blue|green|aqua|red|light_purple|yellow|white|bold|b|italic|i|underlined|u|strikethrough|st|obfuscated|obf|reset)|gradient:|rainbow|transition:|color:)", + Pattern.CASE_INSENSITIVE + ); + private final static MiniMessage MINI_MESSAGE = MiniMessage.miniMessage(); + private final static LegacyComponentSerializer LEGACY_SECTION = LegacyComponentSerializer.legacySection(); /** * Translates the ampersand color codes like '&7' to their section symbol counterparts like '§7'. @@ -23,18 +31,39 @@ public class StringUtils { * @param input The string in which to translate the color codes. * @return The string with the translated colors. */ - @NotNull - public static String color(@NotNull String input) { - // Hex Support for 1.16.1+ - Matcher m = HEX_PATTERN.matcher(input); - if (VersionHelper.IS_HEX_VERSION) { - while (m.find()) { - input = input.replace(m.group(), ChatColor.of(m.group(1)).toString()); - } + @NotNull + public static String color(@NotNull String input) { + if (hasMiniMessageFormat(input)) { + return miniMessage(input); + } + + return legacyColor(input); + } + + @NotNull + public static String miniMessage(@NotNull final String input) { + try { + return legacyColor(LEGACY_SECTION.serialize(MINI_MESSAGE.deserialize(input))); + } catch (final Exception ignored) { + return legacyColor(input); + } + } + + private static boolean hasMiniMessageFormat(@NotNull final String input) { + return MINI_MESSAGE_PATTERN.matcher(input).find(); + } + + @NotNull + private static String legacyColor(@NotNull String input) { + Matcher m = HEX_PATTERN.matcher(input); + if (VersionHelper.IS_HEX_VERSION) { + while (m.find()) { + input = input.replace(m.group(), ChatColor.of(m.group(1)).toString()); + } } - - return ChatColor.translateAlternateColorCodes('&', input); - } + + return ChatColor.translateAlternateColorCodes('&', input); + } @NotNull public static String replacePlaceholdersAndArguments(@NotNull String input, final @Nullable Map arguments, diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index ec50f892..74eab4af 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -20,10 +20,13 @@ permissions: deluxemenus.open.others: description: open a menu with /dm open default: op - deluxemenus.open.bypass: - description: attempt to open a menu for a viewer skipping view requirement checking for the player - default: op - deluxemenus.menu.*: + deluxemenus.open.bypass: + description: attempt to open a menu for a viewer skipping view requirement checking for the player + default: op + deluxemenus.convertcommandpanels: + description: convert CommandPanels panel files into DeluxeMenus menus + default: op + deluxemenus.menu.*: description: permission for all menus default: op deluxemenus.openrequirement.bypass.*: From 7c98a1cdc92b4052e25d03a0427ed2ebf58807b7 Mon Sep 17 00:00:00 2001 From: unknown <1tranducnhan.2021@gmail.com> Date: Fri, 15 May 2026 21:54:28 +0700 Subject: [PATCH 02/12] Remove out-of-scope converter changes --- build.gradle.kts | 7 +- gradle/libs.versions.toml | 12 +- .../command/DeluxeMenusCommand.java | 1 - .../ConvertCommandPanelsCommand.java | 129 --- .../CommandPanelsConversionResult.java | 47 - .../commandpanels/CommandPanelsConverter.java | 813 ------------------ .../deluxemenus/utils/Messages.java | 16 +- .../deluxemenus/utils/StringUtils.java | 73 +- src/main/resources/plugin.yml | 11 +- 9 files changed, 40 insertions(+), 1069 deletions(-) delete mode 100644 src/main/java/com/extendedclip/deluxemenus/command/subcommand/ConvertCommandPanelsCommand.java delete mode 100644 src/main/java/com/extendedclip/deluxemenus/commandpanels/CommandPanelsConversionResult.java delete mode 100644 src/main/java/com/extendedclip/deluxemenus/commandpanels/CommandPanelsConverter.java diff --git a/build.gradle.kts b/build.gradle.kts index c2a2fe34..fffd5a9a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -45,10 +45,9 @@ dependencies { compileOnly(libs.papi) implementation(libs.nashorn) - implementation(libs.adventure.platform) - implementation(libs.adventure.minimessage) - implementation(libs.adventure.legacy) - implementation(libs.bstats) + implementation(libs.adventure.platform) + implementation(libs.adventure.minimessage) + implementation(libs.bstats) compileOnly("org.jetbrains:annotations:23.0.0") } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fab80a35..06e791dd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,9 +17,8 @@ bstats = "3.1.0" # Implementation nashorn = "15.6" -adventure-platform = "4.4.1" -adventure-minimessage = "4.24.0" -adventure-legacy = "4.24.0" +adventure-platform = "4.4.1" +adventure-minimessage = "4.24.0" [libraries] # Compile only @@ -40,7 +39,6 @@ sig = { module = "io.github.valerashimchuck:simpleitemgenerator-api", version.re # Implementation nashorn = { module = "org.openjdk.nashorn:nashorn-core", version.ref = "nashorn" } -adventure-platform = { module = "net.kyori:adventure-platform-bukkit", version.ref = "adventure-platform" } -adventure-minimessage = { module = "net.kyori:adventure-text-minimessage", version.ref = "adventure-minimessage" } -adventure-legacy = { module = "net.kyori:adventure-text-serializer-legacy", version.ref = "adventure-legacy" } -bstats = { module = "org.bstats:bstats-bukkit", version.ref = "bstats" } +adventure-platform = { module = "net.kyori:adventure-platform-bukkit", version.ref = "adventure-platform" } +adventure-minimessage = { module = "net.kyori:adventure-text-minimessage", version.ref = "adventure-minimessage" } +bstats = { module = "org.bstats:bstats-bukkit", version.ref = "bstats" } diff --git a/src/main/java/com/extendedclip/deluxemenus/command/DeluxeMenusCommand.java b/src/main/java/com/extendedclip/deluxemenus/command/DeluxeMenusCommand.java index 2c3d9236..e360bde8 100644 --- a/src/main/java/com/extendedclip/deluxemenus/command/DeluxeMenusCommand.java +++ b/src/main/java/com/extendedclip/deluxemenus/command/DeluxeMenusCommand.java @@ -91,7 +91,6 @@ private void registerSubCommands() { final List commands = List.of( new DumpCommand(plugin), new ExecuteCommand(plugin), - new ConvertCommandPanelsCommand(plugin), new HelpCommand(plugin), new ListCommand(plugin), new MetaCommand(plugin), diff --git a/src/main/java/com/extendedclip/deluxemenus/command/subcommand/ConvertCommandPanelsCommand.java b/src/main/java/com/extendedclip/deluxemenus/command/subcommand/ConvertCommandPanelsCommand.java deleted file mode 100644 index 24fc1df4..00000000 --- a/src/main/java/com/extendedclip/deluxemenus/command/subcommand/ConvertCommandPanelsCommand.java +++ /dev/null @@ -1,129 +0,0 @@ -package com.extendedclip.deluxemenus.command.subcommand; - -import com.extendedclip.deluxemenus.DeluxeMenus; -import com.extendedclip.deluxemenus.commandpanels.CommandPanelsConversionResult; -import com.extendedclip.deluxemenus.commandpanels.CommandPanelsConverter; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; -import org.bukkit.command.CommandSender; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.io.File; -import java.io.IOException; -import java.util.List; -import java.util.logging.Level; - -import static net.kyori.adventure.text.Component.text; - -public class ConvertCommandPanelsCommand extends SubCommand { - - private static final String CONVERT_PERMISSION = "deluxemenus.convertcommandpanels"; - - public ConvertCommandPanelsCommand(final @NotNull DeluxeMenus plugin) { - super(plugin); - } - - @Override - public @NotNull String getName() { - return "convertcommandpanels"; - } - - @Override - public void execute(final @NotNull CommandSender sender, final @NotNull List arguments) { - if (!sender.hasPermission(CONVERT_PERMISSION)) { - plugin.sms(sender, com.extendedclip.deluxemenus.utils.Messages.NO_PERMISSION); - return; - } - - final ParsedArguments parsedArguments = parseArguments(arguments); - - try { - final CommandPanelsConverter converter = new CommandPanelsConverter(plugin); - final CommandPanelsConversionResult result = parsedArguments.sourcePath.isBlank() - ? converter.convertDefault(parsedArguments.outputFolder) - : converter.convert(new File(stripQuotes(parsedArguments.sourcePath)), parsedArguments.outputFolder); - - plugin.sms(sender, text("Converted ", NamedTextColor.GREEN) - .append(text(result.menusConverted(), NamedTextColor.WHITE)) - .append(text(" CommandPanels menus from ", NamedTextColor.GREEN)) - .append(text(result.filesRead(), NamedTextColor.WHITE)) - .append(text(" files.", NamedTextColor.GREEN))); - - if (result.menusSkipped() > 0) { - plugin.sms(sender, text("Skipped " + result.menusSkipped() + " menus. Check console for details.", NamedTextColor.YELLOW)); - } - - for (final String warning : result.warnings()) { - plugin.getLogger().warning("[CommandPanels converter] " + warning); - } - } catch (final IOException exception) { - plugin.sms(sender, Component.text("CommandPanels conversion failed: " + exception.getMessage(), NamedTextColor.RED)); - } catch (final RuntimeException exception) { - plugin.getLogger().log(Level.SEVERE, "Unexpected error while converting CommandPanels menus.", exception); - plugin.sms(sender, Component.text("CommandPanels conversion failed. Check console for details.", NamedTextColor.RED)); - } - } - - @Override - public @Nullable List onTabComplete(final @NotNull CommandSender sender, final @NotNull List arguments) { - if (!sender.hasPermission(CONVERT_PERMISSION)) { - return null; - } - - if (arguments.isEmpty()) { - return List.of(getName()); - } - - if (arguments.size() == 1 && getName().startsWith(arguments.get(0).toLowerCase())) { - return List.of(getName()); - } - - if (arguments.size() >= 2 && getName().equalsIgnoreCase(arguments.get(0)) && "--output".startsWith(arguments.get(arguments.size() - 1).toLowerCase())) { - return List.of("--output"); - } - - return null; - } - - private @NotNull ParsedArguments parseArguments(final @NotNull List arguments) { - String outputFolder = null; - final StringBuilder sourcePath = new StringBuilder(); - - for (int i = 0; i < arguments.size(); i++) { - final String argument = arguments.get(i); - - if (argument.equalsIgnoreCase("--output")) { - if (i + 1 < arguments.size()) { - outputFolder = arguments.get(i + 1); - } - i++; - continue; - } - - if (sourcePath.length() > 0) { - sourcePath.append(' '); - } - sourcePath.append(argument); - } - - return new ParsedArguments(sourcePath.toString(), outputFolder); - } - - private @NotNull String stripQuotes(final @NotNull String value) { - if (value.length() >= 2 && ((value.startsWith("\"") && value.endsWith("\"")) || (value.startsWith("'") && value.endsWith("'")))) { - return value.substring(1, value.length() - 1); - } - return value; - } - - private static class ParsedArguments { - private final String sourcePath; - private final String outputFolder; - - private ParsedArguments(final @NotNull String sourcePath, final @Nullable String outputFolder) { - this.sourcePath = sourcePath; - this.outputFolder = outputFolder; - } - } -} diff --git a/src/main/java/com/extendedclip/deluxemenus/commandpanels/CommandPanelsConversionResult.java b/src/main/java/com/extendedclip/deluxemenus/commandpanels/CommandPanelsConversionResult.java deleted file mode 100644 index 70b9014f..00000000 --- a/src/main/java/com/extendedclip/deluxemenus/commandpanels/CommandPanelsConversionResult.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.extendedclip.deluxemenus.commandpanels; - -import org.jetbrains.annotations.NotNull; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -public class CommandPanelsConversionResult { - - private int filesRead; - private int menusConverted; - private int menusSkipped; - private final List warnings = new ArrayList<>(); - - public void fileRead() { - filesRead++; - } - - public void menuConverted() { - menusConverted++; - } - - public void menuSkipped() { - menusSkipped++; - } - - public void warn(final @NotNull String warning) { - warnings.add(warning); - } - - public int filesRead() { - return filesRead; - } - - public int menusConverted() { - return menusConverted; - } - - public int menusSkipped() { - return menusSkipped; - } - - public @NotNull List warnings() { - return Collections.unmodifiableList(warnings); - } -} diff --git a/src/main/java/com/extendedclip/deluxemenus/commandpanels/CommandPanelsConverter.java b/src/main/java/com/extendedclip/deluxemenus/commandpanels/CommandPanelsConverter.java deleted file mode 100644 index 14445962..00000000 --- a/src/main/java/com/extendedclip/deluxemenus/commandpanels/CommandPanelsConverter.java +++ /dev/null @@ -1,813 +0,0 @@ -package com.extendedclip.deluxemenus.commandpanels; - -import com.extendedclip.deluxemenus.DeluxeMenus; -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.configuration.file.YamlConfiguration; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Set; - -public class CommandPanelsConverter { - - private static final String DEFAULT_OUTPUT_FOLDER = "commandpanels"; - - private final DeluxeMenus plugin; - - public CommandPanelsConverter(final @NotNull DeluxeMenus plugin) { - this.plugin = plugin; - } - - public @NotNull CommandPanelsConversionResult convertDefault(final @Nullable String outputFolder) throws IOException { - return convert(resolveDefaultSource(), outputFolder); - } - - public @NotNull CommandPanelsConversionResult convert(final @NotNull File source, final @Nullable String outputFolder) throws IOException { - final File panelsFolder = resolvePanelsFolder(source); - final File[] files = panelsFolder.listFiles((dir, name) -> name.toLowerCase(Locale.ROOT).endsWith(".yml")); - if (files == null || files.length == 0) { - throw new IOException("No .yml panel files were found in " + panelsFolder.getPath()); - } - - final String targetFolder = sanitizeFolder(outputFolder == null || outputFolder.isBlank() ? DEFAULT_OUTPUT_FOLDER : outputFolder); - final File outputDirectory = new File(plugin.getConfiguration().getMenuDirector(), targetFolder); - if (!outputDirectory.exists() && !outputDirectory.mkdirs()) { - throw new IOException("Could not create output folder " + outputDirectory.getPath()); - } - - final CommandPanelsConversionResult result = new CommandPanelsConversionResult(); - final YamlConfiguration deluxeConfig = (YamlConfiguration) plugin.getConfig(); - final List sortedFiles = new ArrayList<>(List.of(files)); - sortedFiles.sort(Comparator.comparing(File::getName)); - - for (final File file : sortedFiles) { - result.fileRead(); - final YamlConfiguration sourceConfig = YamlConfiguration.loadConfiguration(file); - convertFile(sourceConfig, file, outputDirectory, targetFolder, deluxeConfig, result); - } - - plugin.saveConfig(); - return result; - } - - private void convertFile( - final @NotNull YamlConfiguration sourceConfig, - final @NotNull File sourceFile, - final @NotNull File outputDirectory, - final @NotNull String outputFolder, - final @NotNull YamlConfiguration deluxeConfig, - final @NotNull CommandPanelsConversionResult result - ) throws IOException { - final ConfigurationSection legacyPanels = sourceConfig.getConfigurationSection("panels"); - if (legacyPanels != null) { - for (final String panelName : legacyPanels.getKeys(false)) { - final ConfigurationSection panel = legacyPanels.getConfigurationSection(panelName); - if (panel == null) { - result.menuSkipped(); - continue; - } - try { - saveConvertedPanel(panelName, panel, outputDirectory, outputFolder, deluxeConfig, result); - } catch (final RuntimeException exception) { - result.menuSkipped(); - result.warn("Skipped " + panelName + " in " + sourceFile.getName() + ": " + exception.getMessage()); - } - } - return; - } - - final String panelName = sourceConfig.getString("panel", stripExtension(sourceFile.getName())); - try { - saveConvertedPanel(panelName, sourceConfig, outputDirectory, outputFolder, deluxeConfig, result); - } catch (final RuntimeException exception) { - result.menuSkipped(); - result.warn("Skipped " + panelName + " in " + sourceFile.getName() + ": " + exception.getMessage()); - } - } - - private void saveConvertedPanel( - final @NotNull String rawPanelName, - final @NotNull ConfigurationSection panel, - final @NotNull File outputDirectory, - final @NotNull String outputFolder, - final @NotNull YamlConfiguration deluxeConfig, - final @NotNull CommandPanelsConversionResult result - ) throws IOException { - final String menuName = sanitizeMenuName(rawPanelName); - final YamlConfiguration converted = convertPanel(menuName, panel, result); - if (converted.getConfigurationSection("items") == null) { - result.menuSkipped(); - result.warn("Skipped " + rawPanelName + " because it did not contain convertible items."); - return; - } - - final File outputFile = new File(outputDirectory, menuName + ".yml"); - converted.save(outputFile); - deluxeConfig.set("gui_menus." + menuName + ".file", outputFolder + "/" + outputFile.getName()); - result.menuConverted(); - } - - private @NotNull YamlConfiguration convertPanel( - final @NotNull String menuName, - final @NotNull ConfigurationSection panel, - final @NotNull CommandPanelsConversionResult result - ) { - final YamlConfiguration out = new YamlConfiguration(); - out.options().indent(2); - - out.set("menu_title", translateText(panel.getString("title", panel.getString("menu_title", menuName)))); - - final int size = resolveSize(panel, result, menuName); - out.set("size", size); - - final List openCommands = getOpenCommands(panel); - if (!openCommands.isEmpty()) { - out.set("open_command", openCommands); - } - - final int refreshInterval = panel.getInt("refresh-delay", panel.getInt("refresh_delay", panel.getInt("update-delay", panel.getInt("update_delay", -1)))); - if (refreshInterval > 0) { - out.set("refresh", true); - out.set("refresh_interval", refreshInterval); - out.set("update_interval", refreshInterval); - } - - if (panel.getBoolean("hide-player-inventory", panel.getBoolean("hide_player_inventory", false))) { - out.set("hide_player_inventory", true); - } - - final ConfigurationSection legacyItems = panel.getConfigurationSection("item"); - final ConfigurationSection modernItems = panel.getConfigurationSection("items"); - final List layout = getStringList(panel, "layout"); - - if (legacyItems != null) { - convertLegacyItems(out, legacyItems, result, menuName); - } else if (modernItems != null && !layout.isEmpty()) { - convertLayoutItems(out, modernItems, layout, result, menuName); - } else if (modernItems != null) { - convertNamedItems(out, modernItems, result, menuName); - } - - return out; - } - - private int resolveSize( - final @NotNull ConfigurationSection panel, - final @NotNull CommandPanelsConversionResult result, - final @NotNull String menuName - ) { - if (panel.isInt("rows")) { - return Math.max(1, Math.min(6, panel.getInt("rows"))) * 9; - } - - if (panel.isInt("size")) { - final int size = panel.getInt("size"); - return Math.max(9, Math.min(54, size - (size % 9))); - } - - final String rows = panel.getString("rows"); - if (rows != null) { - try { - return Math.max(1, Math.min(6, Integer.parseInt(rows))) * 9; - } catch (final NumberFormatException ignored) { - result.warn("Menu " + menuName + " uses non-chest rows value '" + rows + "'. It was converted as a 54-slot chest."); - } - } - - return 54; - } - - private void convertLegacyItems( - final @NotNull YamlConfiguration out, - final @NotNull ConfigurationSection items, - final @NotNull CommandPanelsConversionResult result, - final @NotNull String menuName - ) { - for (final String slotKey : items.getKeys(false)) { - final int slot = parseSlot(slotKey, result, menuName); - if (slot < 0) { - continue; - } - - final ConfigurationSection item = items.getConfigurationSection(slotKey); - if (item == null) { - continue; - } - - int priority = 1; - for (final String key : item.getKeys(false)) { - if (!key.toLowerCase(Locale.ROOT).matches("has\\d+")) { - continue; - } - - final ConfigurationSection variant = item.getConfigurationSection(key); - if (variant == null) { - continue; - } - - final String itemPath = "items." + itemName(slot, key) + "."; - convertItem(out, itemPath, variant, slot, priority++, result, menuName); - addLegacyHasRequirements(out, itemPath + "view_requirement", variant); - } - - convertItem(out, "items." + itemName(slot, "base") + ".", item, slot, priority, result, menuName); - } - } - - private void convertLayoutItems( - final @NotNull YamlConfiguration out, - final @NotNull ConfigurationSection items, - final @NotNull List layout, - final @NotNull CommandPanelsConversionResult result, - final @NotNull String menuName - ) { - final Set occupiedSlots = new HashSet<>(); - int slot = 0; - for (final String line : layout) { - for (final String token : tokenizeLayout(line)) { - if (token.equalsIgnoreCase("empty") || token.equalsIgnoreCase("air") || token.equals("-")) { - slot++; - continue; - } - - final ConfigurationSection item = items.getConfigurationSection(token); - if (item == null) { - result.warn("Menu " + menuName + " references unknown CommandPanels layout item '" + token + "'."); - slot++; - continue; - } - - convertItem(out, "items." + itemName(slot, token) + ".", item, slot, 1, result, menuName); - occupiedSlots.add(slot); - slot++; - } - } - - final String fillItemName = getFillItemName(items); - if (fillItemName == null) { - return; - } - - final ConfigurationSection fillItem = items.getConfigurationSection(fillItemName); - if (fillItem == null) { - return; - } - - for (int index = 0; index < slot; index++) { - if (occupiedSlots.contains(index)) { - continue; - } - - convertItem(out, "items." + itemName(index, "fill") + ".", fillItem, index, 1, result, menuName); - } - } - - private void convertNamedItems( - final @NotNull YamlConfiguration out, - final @NotNull ConfigurationSection items, - final @NotNull CommandPanelsConversionResult result, - final @NotNull String menuName - ) { - for (final String key : items.getKeys(false)) { - final ConfigurationSection item = items.getConfigurationSection(key); - if (item == null || !item.isInt("slot")) { - continue; - } - - final int slot = item.getInt("slot"); - convertItem(out, "items." + itemName(slot, key) + ".", item, slot, 1, result, menuName); - } - } - - private void convertItem( - final @NotNull YamlConfiguration out, - final @NotNull String path, - final @NotNull ConfigurationSection item, - final int slot, - final int priority, - final @NotNull CommandPanelsConversionResult result, - final @NotNull String menuName - ) { - out.set(path + "material", convertMaterial(item.getString("material", "STONE"))); - out.set(path + "slot", slot); - out.set(path + "priority", priority); - - setIfPresent(out, path + "display_name", translateText(item.getString("name", item.getString("display-name", null)))); - setIfPresent(out, path + "lore", translateTextList(getStringList(item, "lore"))); - setIfPresent(out, path + "model_data", item.getString("customdata", item.getString("custom-model-data", item.getString("custom_model_data", null)))); - setIfPresent(out, path + "amount", item.getString("stack", item.getString("amount", null))); - setIfPresent(out, path + "damage", item.getString("damage", null)); - - if (item.getBoolean("tooltip", true) == false) { - out.set(path + "hide_tooltip", "true"); - } - - final List enchantments = convertEnchantments(getStringList(item, "enchantments")); - if (!enchantments.isEmpty()) { - out.set(path + "enchantments", enchantments); - } - - final List commands = convertCommands(getStringList(item, "commands")); - if (!commands.isEmpty()) { - out.set(path + "click_commands", commands); - } - - final List actions = convertCommands(getStringList(item, "actions.commands")); - if (!actions.isEmpty()) { - out.set(path + "click_commands", actions); - } - - final List leftActions = convertCommands(getStringList(item, "left-click.commands")); - if (!leftActions.isEmpty()) { - out.set(path + "left_click_commands", leftActions); - } - - final List rightActions = convertCommands(getStringList(item, "right-click.commands")); - if (!rightActions.isEmpty()) { - out.set(path + "right_click_commands", rightActions); - } - - final String conditions = item.getString("conditions", null); - if (conditions != null && !conditions.isBlank()) { - if (!addExpressionRequirements(out, path + "view_requirement", conditions)) { - result.warn("Menu " + menuName + " item at slot " + slot + " has an unsupported condition expression: " + conditions); - } - } - - final boolean update = containsPlaceholder(item.getString("name", "")) || getStringList(item, "lore").stream().anyMatch(this::containsPlaceholder); - if (update) { - out.set(path + "update", true); - } - } - - private boolean addExpressionRequirements(final @NotNull YamlConfiguration out, final @NotNull String path, final @NotNull String expression) { - final String connector; - if (expression.contains("$AND") && !expression.contains("$OR")) { - connector = "\\$AND"; - } else if (expression.contains("$OR") && !expression.contains("$AND")) { - connector = "\\$OR"; - out.set(path + ".minimum_requirements", 1); - } else { - connector = null; - } - - final String[] parts = connector == null ? new String[]{expression} : expression.split(connector); - int requirement = 1; - for (final String part : parts) { - if (!addExpressionRequirement(out, path + ".requirements.condition_" + requirement, part.trim())) { - return false; - } - requirement++; - } - return true; - } - - private boolean addExpressionRequirement(final @NotNull YamlConfiguration out, final @NotNull String path, final @NotNull String expression) { - final String cleanExpression = expression.replace("(", "").replace(")", "").trim(); - final boolean inverted = cleanExpression.startsWith("$NOT "); - final String normalized = inverted ? cleanExpression.substring(5).trim() : cleanExpression; - final String[] parts = normalized.split("\\s+", 3); - if (parts.length < 3) { - return false; - } - - final String input = translateText(parts[0]); - final String operator = parts[1].toUpperCase(Locale.ROOT); - final String output = translateText(parts[2]); - - switch (operator) { - case "$HASPERM": - out.set(path + ".type", inverted ? "!has permission" : "has permission"); - out.set(path + ".permission", output); - return true; - case "$EQUALS": - out.set(path + ".type", inverted ? "string does not equal ignorecase" : "string equals ignorecase"); - out.set(path + ".input", input); - out.set(path + ".output", output); - return true; - case "$ATLEAST": - out.set(path + ".type", inverted ? "<" : ">="); - out.set(path + ".input", input); - out.set(path + ".output", output); - return true; - default: - return false; - } - } - - private void addLegacyHasRequirements(final @NotNull YamlConfiguration out, final @NotNull String path, final @NotNull ConfigurationSection variant) { - int requirement = 1; - for (int index = 0; index < 20; index++) { - final String value = variant.getString("value" + index, null); - final String compare = variant.getString("compare" + index, null); - if (value == null || compare == null) { - continue; - } - - addLegacyComparison(out, path + ".requirements.condition_" + requirement, value, compare); - requirement++; - } - } - - private void addLegacyComparison( - final @NotNull YamlConfiguration out, - final @NotNull String path, - final @NotNull String rawValue, - final @NotNull String rawCompare - ) { - String value = translateText(rawValue.trim()); - String compare = translateText(rawCompare.trim()); - String type = "string equals ignorecase"; - String input; - String output; - - if (value.toUpperCase(Locale.ROOT).startsWith("NOT ")) { - type = "string does not equal ignorecase"; - input = compare; - output = value.substring(4).trim(); - } else if (value.toUpperCase(Locale.ROOT).endsWith(" ISGREATER")) { - type = ">"; - input = value.substring(0, value.length() - " ISGREATER".length()).trim(); - output = compare; - } else if (value.toUpperCase(Locale.ROOT).endsWith(" ISLESS")) { - type = "<"; - input = value.substring(0, value.length() - " ISLESS".length()).trim(); - output = compare; - } else if (value.toUpperCase(Locale.ROOT).endsWith(" ISEQUAL")) { - type = "string equals ignorecase"; - input = value.substring(0, value.length() - " ISEQUAL".length()).trim(); - output = compare; - } else if (value.toUpperCase(Locale.ROOT).endsWith(" ISNOTEQUAL")) { - type = "string does not equal ignorecase"; - input = value.substring(0, value.length() - " ISNOTEQUAL".length()).trim(); - output = compare; - } else if (containsPlaceholder(value) || !containsPlaceholder(compare)) { - input = value; - output = compare; - } else { - input = compare; - output = value; - } - - out.set(path + ".type", type); - out.set(path + ".input", input); - out.set(path + ".output", output); - } - - private @NotNull List convertCommands(final @NotNull List sourceCommands) { - final List commands = new ArrayList<>(); - - for (final String sourceCommand : sourceCommands) { - if (sourceCommand == null || sourceCommand.isBlank()) { - continue; - } - - final String command = translateText(sourceCommand.trim()); - final int equalsIndex = command.indexOf('='); - - if (command.equalsIgnoreCase("cpc") || command.equalsIgnoreCase("close") || command.equalsIgnoreCase("[close]")) { - commands.add("[close]"); - continue; - } - - if (command.startsWith("[")) { - commands.add(convertBracketCommand(command)); - continue; - } - - if (equalsIndex > 0) { - final String type = command.substring(0, equalsIndex).trim().toLowerCase(Locale.ROOT); - final String executable = command.substring(equalsIndex + 1).trim(); - - switch (type) { - case "open": - commands.add("[openguimenu] " + executable); - continue; - case "open_gui_inventory": - case "openguiinventory": - case "open_inventory": - case "openinventory": - commands.add("[open_gui_inventory] " + executable); - continue; - case "sound": - commands.add("[sound] " + executable); - continue; - case "console": - commands.add("[console] " + executable); - continue; - case "msg": - case "message": - commands.add((looksMiniMessage(executable) ? "[minimessage] " : "[message] ") + executable); - continue; - case "minimessage": - case "mini_message": - commands.add("[minimessage] " + executable); - continue; - case "server": - case "connect": - commands.add("[connect] " + executable); - continue; - case "chat": - commands.add("[chat] " + executable); - continue; - case "close": - commands.add("[close]"); - continue; - case "refresh": - commands.add("[refresh]"); - continue; - default: - commands.add("[player] " + stripSlash(command)); - continue; - } - } - - commands.add("[player] " + stripSlash(command)); - } - - return commands; - } - - private @NotNull String convertBracketCommand(final @NotNull String command) { - final int endIndex = command.indexOf(']'); - if (endIndex <= 1) { - return "[player] " + stripSlash(command); - } - - final String type = command.substring(1, endIndex).trim().toLowerCase(Locale.ROOT); - final String executable = command.substring(endIndex + 1).trim(); - - switch (type) { - case "open": - return "[openguimenu] " + executable; - case "open_gui_inventory": - case "openguiinventory": - case "open_inventory": - case "openinventory": - return "[open_gui_inventory] " + executable; - case "msg": - case "message": - return (looksMiniMessage(executable) ? "[minimessage] " : "[message] ") + executable; - case "server": - return "[connect] " + executable; - case "sound": - case "console": - case "chat": - case "close": - case "refresh": - return "[" + type + "]" + (executable.isBlank() ? "" : " " + executable); - default: - return "[player] " + stripSlash(command); - } - } - - private @NotNull String convertMaterial(final @NotNull String sourceMaterial) { - final String material = translateText(sourceMaterial.trim()); - final String lowerMaterial = material.toLowerCase(Locale.ROOT); - - if (containsPlaceholder(material)) { - return "placeholder-" + material; - } - - if (lowerMaterial.startsWith("cps=")) { - final String value = material.substring(material.indexOf('=') + 1).trim(); - return value.equalsIgnoreCase("self") ? "head-%player_name%" : "head-" + value; - } - - if (lowerMaterial.startsWith("head=") || lowerMaterial.startsWith("skull=")) { - return "head-" + material.substring(material.indexOf('=') + 1).trim(); - } - - if (lowerMaterial.startsWith("basehead=")) { - return "basehead-" + material.substring(material.indexOf('=') + 1).trim(); - } - - if (lowerMaterial.startsWith("hdb=")) { - return "hdb-" + material.substring(material.indexOf('=') + 1).trim(); - } - - if (lowerMaterial.startsWith("itemsadder=")) { - return "itemsadder-" + material.substring(material.indexOf('=') + 1).trim(); - } - - if (lowerMaterial.startsWith("nexo=")) { - return "nexo-" + material.substring(material.indexOf('=') + 1).trim(); - } - - if (lowerMaterial.startsWith("oraxen=")) { - return "oraxen-" + material.substring(material.indexOf('=') + 1).trim(); - } - - if (lowerMaterial.startsWith("craftengine=")) { - return "craftengine-" + material.substring(material.indexOf('=') + 1).trim(); - } - - if (lowerMaterial.startsWith("mmo=") || lowerMaterial.startsWith("mmoitems=")) { - final String value = material.substring(material.indexOf('=') + 1).trim().replace(' ', ':'); - return "mmoitems-" + value; - } - - return material.toUpperCase(Locale.ROOT); - } - - private @NotNull List convertEnchantments(final @NotNull List sourceEnchantments) { - final List enchantments = new ArrayList<>(); - for (final String enchantment : sourceEnchantments) { - final String[] parts = enchantment.trim().split("\\s+", 2); - if (parts.length == 0 || parts[0].isBlank()) { - continue; - } - - final String level = parts.length > 1 ? parts[1].trim() : "1"; - enchantments.add(parts[0].toUpperCase(Locale.ROOT) + ";" + level); - } - return enchantments; - } - - private @NotNull List getOpenCommands(final @NotNull ConfigurationSection panel) { - final List commands = new ArrayList<>(); - - if (panel.isList("commands")) { - commands.addAll(getStringList(panel, "commands")); - } - - if (panel.isString("command")) { - commands.add(panel.getString("command", "")); - } - - commands.addAll(getStringList(panel, "aliases")); - commands.removeIf(String::isBlank); - return commands; - } - - private @Nullable String getFillItemName(final @NotNull ConfigurationSection items) { - for (final String key : items.getKeys(false)) { - final ConfigurationSection item = items.getConfigurationSection(key); - if (item != null && item.getBoolean("fill", false)) { - return key; - } - } - return null; - } - - private @NotNull List tokenizeLayout(final @NotNull String line) { - final String trimmed = line.trim(); - if (trimmed.contains(" ")) { - return List.of(trimmed.split("\\s+")); - } - - final List tokens = new ArrayList<>(); - for (int i = 0; i < trimmed.length(); i++) { - tokens.add(String.valueOf(trimmed.charAt(i))); - } - return tokens; - } - - private @NotNull File resolvePanelsFolder(final @NotNull File source) throws IOException { - if (!source.exists()) { - throw new IOException("Source path does not exist: " + source.getPath()); - } - - if (source.isFile()) { - return source.getParentFile(); - } - - final File nestedPanels = new File(source, "panels"); - if (nestedPanels.isDirectory()) { - return nestedPanels; - } - - return source; - } - - private @NotNull File resolveDefaultSource() throws IOException { - final File converterDirectory = plugin.getConfiguration().getConverterDirectory(); - if (!converterDirectory.exists() && !converterDirectory.mkdirs()) { - throw new IOException("Could not create converter folder " + converterDirectory.getPath()); - } - - final List candidates = List.of( - new File(converterDirectory, "CommandPanels"), - new File(converterDirectory, "commandpanels"), - new File(converterDirectory, "panels"), - converterDirectory - ); - - for (final File candidate : candidates) { - if (!candidate.exists()) { - continue; - } - - final File panelsFolder = resolvePanelsFolder(candidate); - final File[] files = panelsFolder.listFiles((dir, name) -> name.toLowerCase(Locale.ROOT).endsWith(".yml")); - if (files != null && files.length > 0) { - return candidate; - } - } - - throw new IOException("Put your CommandPanels folder into " + converterDirectory.getPath() + " and run /dm convertcommandpanels again."); - } - - private int parseSlot(final @NotNull String key, final @NotNull CommandPanelsConversionResult result, final @NotNull String menuName) { - try { - return Integer.parseInt(key); - } catch (final NumberFormatException exception) { - result.warn("Menu " + menuName + " contains non-numeric legacy item slot '" + key + "'."); - return -1; - } - } - - private @NotNull String itemName(final int slot, final @NotNull String suffix) { - return "slot_" + slot + "_" + sanitizeMenuName(suffix); - } - - private @NotNull String stripExtension(final @NotNull String filename) { - final int index = filename.lastIndexOf('.'); - return index == -1 ? filename : filename.substring(0, index); - } - - private @NotNull String sanitizeFolder(final @NotNull String name) { - return name.replace("\\", "/").replace("..", "").replaceAll("^/+", "").replaceAll("/+$", ""); - } - - private @NotNull String sanitizeMenuName(final @NotNull String name) { - final String sanitized = name.trim().toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9_-]", "_"); - return sanitized.isBlank() ? "menu" : sanitized; - } - - private void setIfPresent(final @NotNull YamlConfiguration out, final @NotNull String path, final @Nullable Object value) { - if (value == null) { - return; - } - - if (value instanceof List && ((List) value).isEmpty()) { - return; - } - - out.set(path, value); - } - - private @NotNull List getStringList(final @NotNull ConfigurationSection section, final @NotNull String path) { - if (section.isList(path)) { - return section.getStringList(path); - } - - if (section.isString(path)) { - return List.of(section.getString(path, "")); - } - - return List.of(); - } - - private @NotNull List translateTextList(final @NotNull List source) { - final List translated = new ArrayList<>(); - for (final String line : source) { - if (line == null) { - continue; - } - - translated.add(translateText(line)); - } - return translated; - } - - private @Nullable String translateText(final @Nullable String source) { - if (source == null) { - return null; - } - - return source - .replace("%cp-player-name%", "%player_name%") - .replace("%cp-player-uuid%", "%player_uuid%") - .replace("%cp-player-world%", "%player_world%"); - } - - private boolean containsPlaceholder(final @NotNull String input) { - return input.contains("%"); - } - - private boolean looksMiniMessage(final @NotNull String input) { - final String lowerInput = input.toLowerCase(Locale.ROOT); - return lowerInput.contains("<#") - || lowerInput.contains("") - || lowerInput.contains("") - || lowerInput.contains("") - || lowerInput.contains("") - || lowerInput.contains("") - || lowerInput.contains("") - || lowerInput.contains(""); - } - - private @NotNull String stripSlash(final @NotNull String command) { - return command.startsWith("/") ? command.substring(1) : command; - } -} diff --git a/src/main/java/com/extendedclip/deluxemenus/utils/Messages.java b/src/main/java/com/extendedclip/deluxemenus/utils/Messages.java index bcf8eeaf..01919b25 100644 --- a/src/main/java/com/extendedclip/deluxemenus/utils/Messages.java +++ b/src/main/java/com/extendedclip/deluxemenus/utils/Messages.java @@ -73,16 +73,12 @@ public enum Messages { .append(text("/dm dump ", NamedTextColor.WHITE)) .append(newline()) .append(text(">", NamedTextColor.AQUA)) - .append(space().append(space())) - .append(text("/dm meta ", NamedTextColor.WHITE)) - .append(newline()) - .append(text(">", NamedTextColor.AQUA)) - .append(space().append(space())) - .append(text("/dm convertcommandpanels [folder] [--output folder]", NamedTextColor.WHITE)) - .append(newline()) - .append(text(">", NamedTextColor.AQUA)) - .append(space().append(space())) - .append(text("/dm reload [menu-name]", NamedTextColor.WHITE))), + .append(space().append(space())) + .append(text("/dm meta ", NamedTextColor.WHITE)) + .append(newline()) + .append(text(">", NamedTextColor.AQUA)) + .append(space().append(space())) + .append(text("/dm reload [menu-name]", NamedTextColor.WHITE))), NO_PERMISSION(text("You don't have permission to do that!", NamedTextColor.RED)), NO_PERMISSION_PLAYER_ARGUMENT(text("You don't have permission to use the argument -p:!", NamedTextColor.RED)), diff --git a/src/main/java/com/extendedclip/deluxemenus/utils/StringUtils.java b/src/main/java/com/extendedclip/deluxemenus/utils/StringUtils.java index f0536274..81fd30f8 100644 --- a/src/main/java/com/extendedclip/deluxemenus/utils/StringUtils.java +++ b/src/main/java/com/extendedclip/deluxemenus/utils/StringUtils.java @@ -1,27 +1,19 @@ package com.extendedclip.deluxemenus.utils; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import me.clip.placeholderapi.PlaceholderAPI; -import net.kyori.adventure.text.minimessage.MiniMessage; -import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; -import net.md_5.bungee.api.ChatColor; -import org.bukkit.Color; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import me.clip.placeholderapi.PlaceholderAPI; +import net.md_5.bungee.api.ChatColor; +import org.bukkit.Color; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; public class StringUtils { - private final static Pattern HEX_PATTERN = Pattern - .compile("&(#[a-f0-9]{6})", Pattern.CASE_INSENSITIVE); - private final static Pattern MINI_MESSAGE_PATTERN = Pattern.compile( - "<(/?(#[a-f0-9]{6}|black|dark_blue|dark_green|dark_aqua|dark_red|dark_purple|gold|gray|dark_gray|blue|green|aqua|red|light_purple|yellow|white|bold|b|italic|i|underlined|u|strikethrough|st|obfuscated|obf|reset)|gradient:|rainbow|transition:|color:)", - Pattern.CASE_INSENSITIVE - ); - private final static MiniMessage MINI_MESSAGE = MiniMessage.miniMessage(); - private final static LegacyComponentSerializer LEGACY_SECTION = LegacyComponentSerializer.legacySection(); + private final static Pattern HEX_PATTERN = Pattern + .compile("&(#[a-f0-9]{6})", Pattern.CASE_INSENSITIVE); /** * Translates the ampersand color codes like '&7' to their section symbol counterparts like '§7'. @@ -31,39 +23,18 @@ public class StringUtils { * @param input The string in which to translate the color codes. * @return The string with the translated colors. */ - @NotNull - public static String color(@NotNull String input) { - if (hasMiniMessageFormat(input)) { - return miniMessage(input); - } - - return legacyColor(input); - } - - @NotNull - public static String miniMessage(@NotNull final String input) { - try { - return legacyColor(LEGACY_SECTION.serialize(MINI_MESSAGE.deserialize(input))); - } catch (final Exception ignored) { - return legacyColor(input); - } - } - - private static boolean hasMiniMessageFormat(@NotNull final String input) { - return MINI_MESSAGE_PATTERN.matcher(input).find(); - } - - @NotNull - private static String legacyColor(@NotNull String input) { - Matcher m = HEX_PATTERN.matcher(input); - if (VersionHelper.IS_HEX_VERSION) { - while (m.find()) { - input = input.replace(m.group(), ChatColor.of(m.group(1)).toString()); - } + @NotNull + public static String color(@NotNull String input) { + // Hex Support for 1.16.1+ + Matcher m = HEX_PATTERN.matcher(input); + if (VersionHelper.IS_HEX_VERSION) { + while (m.find()) { + input = input.replace(m.group(), ChatColor.of(m.group(1)).toString()); + } } - - return ChatColor.translateAlternateColorCodes('&', input); - } + + return ChatColor.translateAlternateColorCodes('&', input); + } @NotNull public static String replacePlaceholdersAndArguments(@NotNull String input, final @Nullable Map arguments, diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 74eab4af..ec50f892 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -20,13 +20,10 @@ permissions: deluxemenus.open.others: description: open a menu with /dm open default: op - deluxemenus.open.bypass: - description: attempt to open a menu for a viewer skipping view requirement checking for the player - default: op - deluxemenus.convertcommandpanels: - description: convert CommandPanels panel files into DeluxeMenus menus - default: op - deluxemenus.menu.*: + deluxemenus.open.bypass: + description: attempt to open a menu for a viewer skipping view requirement checking for the player + default: op + deluxemenus.menu.*: description: permission for all menus default: op deluxemenus.openrequirement.bypass.*: From f9b73afd917e0275bd7b0a0259968d0aa66b2d91 Mon Sep 17 00:00:00 2001 From: unknown <1tranducnhan.2021@gmail.com> Date: Thu, 21 May 2026 20:06:19 +0700 Subject: [PATCH 03/12] Support MiniMessage menu titles --- build.gradle.kts | 9 ++-- gradle/libs.versions.toml | 12 +++--- .../extendedclip/deluxemenus/menu/Menu.java | 2 +- .../deluxemenus/utils/StringUtils.java | 43 ++++++++++++------- 4 files changed, 41 insertions(+), 25 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index fffd5a9a..3b218436 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -44,10 +44,11 @@ dependencies { compileOnly(libs.papi) - implementation(libs.nashorn) - implementation(libs.adventure.platform) - implementation(libs.adventure.minimessage) - implementation(libs.bstats) + implementation(libs.nashorn) + implementation(libs.adventure.platform) + implementation(libs.adventure.minimessage) + implementation(libs.adventure.legacy) + implementation(libs.bstats) compileOnly("org.jetbrains:annotations:23.0.0") } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 06e791dd..fab80a35 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,8 +17,9 @@ bstats = "3.1.0" # Implementation nashorn = "15.6" -adventure-platform = "4.4.1" -adventure-minimessage = "4.24.0" +adventure-platform = "4.4.1" +adventure-minimessage = "4.24.0" +adventure-legacy = "4.24.0" [libraries] # Compile only @@ -39,6 +40,7 @@ sig = { module = "io.github.valerashimchuck:simpleitemgenerator-api", version.re # Implementation nashorn = { module = "org.openjdk.nashorn:nashorn-core", version.ref = "nashorn" } -adventure-platform = { module = "net.kyori:adventure-platform-bukkit", version.ref = "adventure-platform" } -adventure-minimessage = { module = "net.kyori:adventure-text-minimessage", version.ref = "adventure-minimessage" } -bstats = { module = "org.bstats:bstats-bukkit", version.ref = "bstats" } +adventure-platform = { module = "net.kyori:adventure-platform-bukkit", version.ref = "adventure-platform" } +adventure-minimessage = { module = "net.kyori:adventure-text-minimessage", version.ref = "adventure-minimessage" } +adventure-legacy = { module = "net.kyori:adventure-text-serializer-legacy", version.ref = "adventure-legacy" } +bstats = { module = "org.bstats:bstats-bukkit", version.ref = "bstats" } diff --git a/src/main/java/com/extendedclip/deluxemenus/menu/Menu.java b/src/main/java/com/extendedclip/deluxemenus/menu/Menu.java index 8426f2ff..81fa27bb 100644 --- a/src/main/java/com/extendedclip/deluxemenus/menu/Menu.java +++ b/src/main/java/com/extendedclip/deluxemenus/menu/Menu.java @@ -364,7 +364,7 @@ public void openMenu( this.options.openHandler().ifPresent(h -> h.onClick(holder)); - String title = StringUtils.color(holder.setPlaceholdersAndArguments(this.options.title())); + String title = StringUtils.colorMenuTitle(holder.setPlaceholdersAndArguments(this.options.title())); Inventory inventory; diff --git a/src/main/java/com/extendedclip/deluxemenus/utils/StringUtils.java b/src/main/java/com/extendedclip/deluxemenus/utils/StringUtils.java index 81fd30f8..c8b4b23e 100644 --- a/src/main/java/com/extendedclip/deluxemenus/utils/StringUtils.java +++ b/src/main/java/com/extendedclip/deluxemenus/utils/StringUtils.java @@ -1,19 +1,23 @@ package com.extendedclip.deluxemenus.utils; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import me.clip.placeholderapi.PlaceholderAPI; -import net.md_5.bungee.api.ChatColor; -import org.bukkit.Color; -import org.bukkit.entity.Player; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import me.clip.placeholderapi.PlaceholderAPI; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import net.md_5.bungee.api.ChatColor; +import org.bukkit.Color; +import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; public class StringUtils { - - private final static Pattern HEX_PATTERN = Pattern - .compile("&(#[a-f0-9]{6})", Pattern.CASE_INSENSITIVE); + + private final static Pattern HEX_PATTERN = Pattern + .compile("&(#[a-f0-9]{6})", Pattern.CASE_INSENSITIVE); + private final static MiniMessage MINI_MESSAGE = MiniMessage.miniMessage(); + private final static LegacyComponentSerializer LEGACY_SECTION = LegacyComponentSerializer.legacySection(); /** * Translates the ampersand color codes like '&7' to their section symbol counterparts like '§7'. @@ -33,11 +37,20 @@ public static String color(@NotNull String input) { } } - return ChatColor.translateAlternateColorCodes('&', input); - } - - @NotNull - public static String replacePlaceholdersAndArguments(@NotNull String input, final @Nullable Map arguments, + return ChatColor.translateAlternateColorCodes('&', input); + } + + @NotNull + public static String colorMenuTitle(@NotNull final String input) { + try { + return color(LEGACY_SECTION.serialize(MINI_MESSAGE.deserialize(input))); + } catch (final Exception ignored) { + return color(input); + } + } + + @NotNull + public static String replacePlaceholdersAndArguments(@NotNull String input, final @Nullable Map arguments, final @Nullable Player player, final boolean parsePlaceholdersInsideArguments, final boolean parsePlaceholdersAfterArguments) { From aba5530a91177389af04c4cccdd456577b41c651 Mon Sep 17 00:00:00 2001 From: unknown <1tranducnhan.2021@gmail.com> Date: Thu, 21 May 2026 20:39:23 +0700 Subject: [PATCH 04/12] Add menu editor tools --- .../extendedclip/deluxemenus/DeluxeMenus.java | 35 +- .../command/DeluxeMenusCommand.java | 5 +- .../command/subcommand/DebugCommand.java | 306 ++++++++++++++++++ .../command/subcommand/EditCommand.java | 191 +++++++++++ .../command/subcommand/WebEditorCommand.java | 101 ++++++ .../deluxemenus/editor/MenuConfigEditor.java | 169 ++++++++++ .../editor/MenuEditPromptRegistry.java | 78 +++++ .../deluxemenus/editor/MenuEditorHolder.java | 28 ++ .../editor/MenuEditorListener.java | 92 ++++++ .../deluxemenus/editor/MenuEditorManager.java | 49 +++ .../deluxemenus/editor/WebEditorServer.java | 285 ++++++++++++++++ .../deluxemenus/utils/Messages.java | 22 +- src/main/resources/plugin.yml | 17 +- 13 files changed, 1357 insertions(+), 21 deletions(-) create mode 100644 src/main/java/com/extendedclip/deluxemenus/command/subcommand/DebugCommand.java create mode 100644 src/main/java/com/extendedclip/deluxemenus/command/subcommand/EditCommand.java create mode 100644 src/main/java/com/extendedclip/deluxemenus/command/subcommand/WebEditorCommand.java create mode 100644 src/main/java/com/extendedclip/deluxemenus/editor/MenuConfigEditor.java create mode 100644 src/main/java/com/extendedclip/deluxemenus/editor/MenuEditPromptRegistry.java create mode 100644 src/main/java/com/extendedclip/deluxemenus/editor/MenuEditorHolder.java create mode 100644 src/main/java/com/extendedclip/deluxemenus/editor/MenuEditorListener.java create mode 100644 src/main/java/com/extendedclip/deluxemenus/editor/MenuEditorManager.java create mode 100644 src/main/java/com/extendedclip/deluxemenus/editor/WebEditorServer.java diff --git a/src/main/java/com/extendedclip/deluxemenus/DeluxeMenus.java b/src/main/java/com/extendedclip/deluxemenus/DeluxeMenus.java index 319272da..85902cd6 100644 --- a/src/main/java/com/extendedclip/deluxemenus/DeluxeMenus.java +++ b/src/main/java/com/extendedclip/deluxemenus/DeluxeMenus.java @@ -4,9 +4,11 @@ import com.extendedclip.deluxemenus.command.DeluxeMenusCommand; import com.extendedclip.deluxemenus.config.DeluxeMenusConfig; import com.extendedclip.deluxemenus.config.GeneralConfig; -import com.extendedclip.deluxemenus.dupe.DupeFixer; -import com.extendedclip.deluxemenus.dupe.MenuItemMarker; -import com.extendedclip.deluxemenus.hooks.*; +import com.extendedclip.deluxemenus.dupe.DupeFixer; +import com.extendedclip.deluxemenus.dupe.MenuItemMarker; +import com.extendedclip.deluxemenus.editor.MenuEditorListener; +import com.extendedclip.deluxemenus.editor.WebEditorServer; +import com.extendedclip.deluxemenus.hooks.*; import com.extendedclip.deluxemenus.listener.PlayerListener; import com.extendedclip.deluxemenus.menu.Menu; import com.extendedclip.deluxemenus.menu.MenuItem; @@ -65,7 +67,8 @@ public class DeluxeMenus extends JavaPlugin { private Map itemHooks; private final GeneralConfig generalConfig = new GeneralConfig(this); - private DeluxeMenusConfig menuConfig; + private DeluxeMenusConfig menuConfig; + private WebEditorServer webEditorServer; @NotNull private final TaskScheduler scheduler = UniversalScheduler.getScheduler(this); @@ -109,7 +112,9 @@ public void onEnable() { debug(DebugLevel.HIGHEST, Level.WARNING, "Failed to load from config.yml. Use /dm reload after fixing your errors."); } - new PlayerListener(this).register(); + this.webEditorServer = new WebEditorServer(this); + new PlayerListener(this).register(); + new MenuEditorListener(this).register(); if (!new DeluxeMenusCommand(this).register()) { debug(DebugLevel.HIGHEST, Level.SEVERE, "Could not register the DeluxeMenus command!"); } @@ -131,9 +136,13 @@ public void onDisable() { this.audiences = null; } - Menu.unloadForShutdown(this); - - itemHooks.clear(); + Menu.unloadForShutdown(this); + if (this.webEditorServer != null) { + this.webEditorServer.stop(); + this.webEditorServer = null; + } + + itemHooks.clear(); HandlerList.unregisterAll(this); } @@ -197,9 +206,13 @@ public MenuItemMarker getMenuItemMarker() { return menuItemMarker; } - public DeluxeMenusConfig getConfiguration() { - return menuConfig; - } + public DeluxeMenusConfig getConfiguration() { + return menuConfig; + } + + public WebEditorServer getWebEditorServer() { + return webEditorServer; + } public VaultHook getVault() { return vaultHook; diff --git a/src/main/java/com/extendedclip/deluxemenus/command/DeluxeMenusCommand.java b/src/main/java/com/extendedclip/deluxemenus/command/DeluxeMenusCommand.java index e360bde8..c6ee2db9 100644 --- a/src/main/java/com/extendedclip/deluxemenus/command/DeluxeMenusCommand.java +++ b/src/main/java/com/extendedclip/deluxemenus/command/DeluxeMenusCommand.java @@ -90,13 +90,16 @@ public List onTabComplete( private void registerSubCommands() { final List commands = List.of( new DumpCommand(plugin), + new DebugCommand(plugin), + new EditCommand(plugin), new ExecuteCommand(plugin), new HelpCommand(plugin), new ListCommand(plugin), new MetaCommand(plugin), new OpenCommand(plugin), new RefreshCommand(plugin), - new ReloadCommand(plugin) + new ReloadCommand(plugin), + new WebEditorCommand(plugin) ); for (final SubCommand subCommand : commands) { diff --git a/src/main/java/com/extendedclip/deluxemenus/command/subcommand/DebugCommand.java b/src/main/java/com/extendedclip/deluxemenus/command/subcommand/DebugCommand.java new file mode 100644 index 00000000..e1a68d67 --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/command/subcommand/DebugCommand.java @@ -0,0 +1,306 @@ +package com.extendedclip.deluxemenus.command.subcommand; + +import com.extendedclip.deluxemenus.DeluxeMenus; +import com.extendedclip.deluxemenus.hooks.ItemHook; +import com.extendedclip.deluxemenus.menu.Menu; +import com.extendedclip.deluxemenus.menu.MenuHolder; +import com.extendedclip.deluxemenus.menu.MenuItem; +import com.extendedclip.deluxemenus.requirement.RequirementList; +import com.extendedclip.deluxemenus.utils.Messages; +import com.extendedclip.deluxemenus.utils.StringUtils; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.TreeMap; +import java.util.stream.Collectors; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static net.kyori.adventure.text.Component.newline; +import static net.kyori.adventure.text.Component.text; + +public class DebugCommand extends SubCommand { + + private static final String DEBUG_COMMAND = "deluxemenus.debug"; + + public DebugCommand(final @NotNull DeluxeMenus plugin) { + super(plugin); + } + + @Override + public @NotNull String getName() { + return "debug"; + } + + @Override + public void execute(final @NotNull CommandSender sender, final @NotNull List arguments) { + if (!sender.hasPermission(DEBUG_COMMAND)) { + plugin.sms(sender, Messages.NO_PERMISSION); + return; + } + + if (arguments.size() < 2) { + sendUsage(sender); + return; + } + + switch (arguments.get(0).toLowerCase(Locale.ROOT)) { + case "placeholder": + case "placeholders": + debugPlaceholder(sender, arguments); + break; + case "hook": + case "itemhook": + debugHook(sender, arguments); + break; + case "item": + debugMenuItem(sender, arguments); + break; + default: + sendUsage(sender); + break; + } + } + + @Override + public @Nullable List onTabComplete(final @NotNull CommandSender sender, final @NotNull List arguments) { + if (!sender.hasPermission(DEBUG_COMMAND)) { + return null; + } + + if (arguments.isEmpty()) { + return List.of(getName()); + } + + if (arguments.size() == 1) { + return complete(List.of(getName()), arguments.get(0)); + } + + if (!getName().equalsIgnoreCase(arguments.get(0))) { + return null; + } + + if (arguments.size() == 2) { + return complete(List.of("placeholder", "hook", "item"), arguments.get(1)); + } + + final String mode = arguments.get(1).toLowerCase(Locale.ROOT); + if (arguments.size() == 3) { + if ("placeholder".equals(mode) || "placeholders".equals(mode)) { + return getPlayerNameCompletion(arguments.get(2)); + } + if ("hook".equals(mode) || "itemhook".equals(mode)) { + return completeHookPrefixes(arguments.get(2)); + } + if ("item".equals(mode)) { + return complete(Menu.getAllMenuNames(), arguments.get(2)); + } + } + + if (arguments.size() == 5 && "item".equals(mode)) { + return getPlayerNameCompletion(arguments.get(4)); + } + + if (arguments.size() == 4 && ("hook".equals(mode) || "itemhook".equals(mode))) { + return getPlayerNameCompletion(arguments.get(3)); + } + + return null; + } + + private void debugPlaceholder(final @NotNull CommandSender sender, final @NotNull List arguments) { + if (arguments.size() < 3) { + plugin.sms(sender, usage("/dm debug placeholder ")); + return; + } + + final Player player = Bukkit.getPlayerExact(arguments.get(1)); + if (player == null) { + plugin.sms(sender, Messages.PLAYER_IS_NOT_ONLINE.message().replaceText(PLAYER_REPLACER_BUILDER.replacement(arguments.get(1)).build())); + return; + } + + final String input = String.join(" ", arguments.subList(2, arguments.size())); + final String parsed = StringUtils.replacePlaceholders(input, player); + + plugin.sms(sender, header("Placeholder debug") + .append(row("Player", player.getName())) + .append(row("Input", input)) + .append(row("Parsed", parsed))); + } + + private void debugHook(final @NotNull CommandSender sender, final @NotNull List arguments) { + final String material = arguments.get(1); + final Optional optionalHook = findHook(material); + if (optionalHook.isEmpty()) { + plugin.sms(sender, header("Hook debug") + .append(row("Material", material)) + .append(row("Result", "No matching item hook prefix"))); + return; + } + + final Player player = arguments.size() >= 3 ? Bukkit.getPlayerExact(arguments.get(2)) : sender instanceof Player ? (Player) sender : null; + if (arguments.size() >= 3 && player == null) { + plugin.sms(sender, Messages.PLAYER_IS_NOT_ONLINE.message().replaceText(PLAYER_REPLACER_BUILDER.replacement(arguments.get(2)).build())); + return; + } + + final ItemHook hook = optionalHook.get(); + final String hookArgument = parseHookArgument(material, hook, player); + final ItemStack itemStack = player == null ? hook.getItem(hookArgument) : hook.getItem(player, hookArgument); + + plugin.sms(sender, header("Hook debug") + .append(row("Hook", hook.getClass().getSimpleName())) + .append(row("Prefix", hook.getPrefix())) + .append(row("Argument", hookArgument)) + .append(row("Item", describeItem(itemStack)))); + } + + private void debugMenuItem(final @NotNull CommandSender sender, final @NotNull List arguments) { + if (arguments.size() < 4) { + plugin.sms(sender, usage("/dm debug item ")); + return; + } + + final Optional optionalMenu = Menu.getMenuByName(arguments.get(1)); + if (optionalMenu.isEmpty()) { + plugin.sms(sender, Messages.INVALID_MENU.message().replaceText(MENU_REPLACER_BUILDER.replacement(arguments.get(1)).build())); + return; + } + + final int slot; + try { + slot = Integer.parseInt(arguments.get(2)); + } catch (final NumberFormatException exception) { + plugin.sms(sender, usage("/dm debug item ")); + return; + } + + final Player player = Bukkit.getPlayerExact(arguments.get(3)); + if (player == null) { + plugin.sms(sender, Messages.PLAYER_IS_NOT_ONLINE.message().replaceText(PLAYER_REPLACER_BUILDER.replacement(arguments.get(3)).build())); + return; + } + + final Menu menu = optionalMenu.get(); + final TreeMap slotItems = menu.getMenuItems().get(slot); + if (slotItems == null || slotItems.isEmpty()) { + plugin.sms(sender, header("Menu item debug") + .append(row("Menu", menu.options().name())) + .append(row("Slot", String.valueOf(slot))) + .append(row("Result", "No configured item in this slot"))); + return; + } + + final MenuItem item = slotItems.firstEntry().getValue(); + final MenuHolder holder = new MenuHolder(plugin, player); + holder.setMenuName(menu.options().name()); + holder.setPlaceholderPlayer(player); + holder.parsePlaceholdersInArguments(menu.options().parsePlaceholdersInArguments()); + holder.parsePlaceholdersAfterArguments(menu.options().parsePlaceholdersAfterArguments()); + + final ItemStack rendered = item.getItemStack(holder); + + plugin.sms(sender, header("Menu item debug") + .append(row("Menu", menu.options().name())) + .append(row("Player", player.getName())) + .append(row("Slot", String.valueOf(slot))) + .append(row("Priority", String.valueOf(item.options().priority()))) + .append(row("Material", item.options().material())) + .append(row("Rendered", describeItem(rendered))) + .append(row("View requirements", describeRequirementList(item.options().viewRequirements())))); + } + + private @NotNull Optional findHook(final @NotNull String material) { + final String lowerMaterial = material.toLowerCase(Locale.ROOT); + return plugin.getItemHooks().values() + .stream() + .filter(hook -> lowerMaterial.startsWith(hook.getPrefix())) + .findFirst(); + } + + private @NotNull String parseHookArgument(final @NotNull String material, final @NotNull ItemHook hook, final @Nullable Player player) { + final String argument = material.substring(hook.getPrefix().length()); + return player == null ? argument : StringUtils.replacePlaceholders(argument, player); + } + + private @NotNull String describeItem(final @Nullable ItemStack itemStack) { + if (itemStack == null) { + return "null"; + } + + final StringBuilder builder = new StringBuilder(itemStack.getType().name()) + .append(" x") + .append(itemStack.getAmount()); + + if (itemStack.getType() == Material.STONE) { + builder.append(" (fallback possible)"); + } + + final ItemMeta itemMeta = itemStack.getItemMeta(); + if (itemMeta != null && itemMeta.hasDisplayName()) { + builder.append(" | ").append(itemMeta.getDisplayName()); + } + + return builder.toString(); + } + + private @NotNull String describeRequirementList(final @NotNull Optional optionalRequirementList) { + if (optionalRequirementList.isEmpty()) { + return "none"; + } + + final RequirementList requirementList = optionalRequirementList.get(); + return requirementList.getRequirements().size() + + " configured, minimum " + + requirementList.getMinimumRequirements(); + } + + private void sendUsage(final @NotNull CommandSender sender) { + plugin.sms(sender, header("Debug usage") + .append(row("Placeholder", "/dm debug placeholder ")) + .append(row("Hook", "/dm debug hook [player]")) + .append(row("Item", "/dm debug item "))); + } + + private @NotNull Component usage(final @NotNull String usage) { + return text("Incorrect Usage! Use ", NamedTextColor.RED) + .append(text(usage, NamedTextColor.GRAY)); + } + + private @NotNull Component header(final @NotNull String title) { + return text(title, NamedTextColor.GOLD); + } + + private @NotNull Component row(final @NotNull String label, final @NotNull String value) { + return newline() + .append(text("> ", NamedTextColor.AQUA)) + .append(text(label, NamedTextColor.GRAY)) + .append(text(": ", NamedTextColor.DARK_GRAY)) + .append(text(value, NamedTextColor.WHITE)); + } + + private @NotNull List completeHookPrefixes(final @NotNull String argument) { + final List prefixes = plugin.getItemHooks().values() + .stream() + .map(ItemHook::getPrefix) + .collect(Collectors.toList()); + return complete(prefixes, argument); + } + + private @NotNull List complete(final @NotNull Collection values, final @NotNull String argument) { + final String lowerArgument = argument.toLowerCase(Locale.ROOT); + return values.stream() + .filter(value -> value.toLowerCase(Locale.ROOT).startsWith(lowerArgument)) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/extendedclip/deluxemenus/command/subcommand/EditCommand.java b/src/main/java/com/extendedclip/deluxemenus/command/subcommand/EditCommand.java new file mode 100644 index 00000000..4cacf734 --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/command/subcommand/EditCommand.java @@ -0,0 +1,191 @@ +package com.extendedclip.deluxemenus.command.subcommand; + +import com.extendedclip.deluxemenus.DeluxeMenus; +import com.extendedclip.deluxemenus.editor.MenuConfigEditor; +import com.extendedclip.deluxemenus.editor.MenuEditPromptRegistry; +import com.extendedclip.deluxemenus.editor.MenuEditorManager; +import com.extendedclip.deluxemenus.menu.Menu; +import com.extendedclip.deluxemenus.utils.Messages; +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.stream.Collectors; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class EditCommand extends SubCommand { + + private static final String EDIT_PERMISSION = "deluxemenus.edit"; + private final MenuEditorManager editorManager; + private final MenuConfigEditor configEditor; + + public EditCommand(final @NotNull DeluxeMenus plugin) { + super(plugin); + this.editorManager = new MenuEditorManager(plugin); + this.configEditor = new MenuConfigEditor(plugin); + } + + @Override + public @NotNull String getName() { + return "edit"; + } + + @Override + public void execute(final @NotNull CommandSender sender, final @NotNull List arguments) { + if (!sender.hasPermission(EDIT_PERMISSION)) { + plugin.sms(sender, Messages.NO_PERMISSION); + return; + } + + if (arguments.isEmpty()) { + plugin.sms(sender, Messages.WRONG_USAGE); + return; + } + + final Optional optionalMenu = Menu.getMenuByName(arguments.get(0)); + if (optionalMenu.isEmpty()) { + plugin.sms(sender, Messages.INVALID_MENU.message().replaceText(MENU_REPLACER_BUILDER.replacement(arguments.get(0)).build())); + return; + } + + if (arguments.size() == 1) { + if (!(sender instanceof Player)) { + plugin.sms(sender, Messages.MUST_SPECIFY_PLAYER); + return; + } + + editorManager.open((Player) sender, optionalMenu.get()); + return; + } + + if (arguments.size() >= 5 + && "set".equalsIgnoreCase(arguments.get(1))) { + setValue(sender, optionalMenu.get(), arguments); + return; + } + + if (arguments.size() >= 4 + && "prompt".equalsIgnoreCase(arguments.get(1))) { + prompt(sender, optionalMenu.get(), arguments); + return; + } + + plugin.sms(sender, Messages.WRONG_USAGE); + } + + @Override + public @Nullable List onTabComplete(final @NotNull CommandSender sender, final @NotNull List arguments) { + if (!sender.hasPermission(EDIT_PERMISSION)) { + return null; + } + + if (arguments.isEmpty()) { + return List.of(getName()); + } + + if (arguments.size() == 1) { + return complete(List.of(getName()), arguments.get(0)); + } + + if (!getName().equalsIgnoreCase(arguments.get(0))) { + return null; + } + + if (arguments.size() == 2) { + return complete(Menu.getAllMenuNames(), arguments.get(1)); + } + + if (arguments.size() == 3) { + return complete(List.of("set", "prompt"), arguments.get(2)); + } + + if (arguments.size() == 5) { + return complete(editableOptions(), arguments.get(4)); + } + + return null; + } + + private void setValue(final @NotNull CommandSender sender, final @NotNull Menu menu, final @NotNull List arguments) { + final int slot; + try { + slot = Integer.parseInt(arguments.get(2)); + } catch (final NumberFormatException exception) { + plugin.sms(sender, Messages.WRONG_USAGE); + return; + } + + final String option = arguments.get(3).toLowerCase(Locale.ROOT); + if (!editableOptions().contains(option)) { + plugin.sms(sender, Component.text("Unknown editable option: " + option, NamedTextColor.RED)); + return; + } + + final String value = String.join(" ", arguments.subList(4, arguments.size())); + try { + if (!configEditor.setItemValue(menu, slot, option, value)) { + plugin.sms(sender, Component.text("No editable item was found in slot " + slot + ".", NamedTextColor.RED)); + return; + } + } catch (final IOException exception) { + plugin.printStacktrace("Failed to save menu edit.", exception); + plugin.sms(sender, Component.text("Failed to save menu edit.", NamedTextColor.RED)); + return; + } + + Menu.unload(plugin, menu.options().name()); + plugin.getConfiguration().loadGUIMenu(menu.options().name()); + plugin.sms(sender, Component.text("Updated " + option + " for slot " + slot + ".", NamedTextColor.GREEN)); + } + + private void prompt(final @NotNull CommandSender sender, final @NotNull Menu menu, final @NotNull List arguments) { + if (!(sender instanceof Player)) { + plugin.sms(sender, Messages.MUST_SPECIFY_PLAYER); + return; + } + + final int slot; + try { + slot = Integer.parseInt(arguments.get(2)); + } catch (final NumberFormatException exception) { + plugin.sms(sender, Messages.WRONG_USAGE); + return; + } + + final String option = arguments.get(3).toLowerCase(Locale.ROOT); + if (!editableOptions().contains(option)) { + plugin.sms(sender, Component.text("Unknown editable option: " + option, NamedTextColor.RED)); + return; + } + + final Player player = (Player) sender; + MenuEditPromptRegistry.begin(player, menu.options().name(), slot, option); + plugin.sms(player, Component.text("Type the new " + option + " in chat, or type cancel.", NamedTextColor.YELLOW)); + } + + private @NotNull List editableOptions() { + return List.of( + "material", + "display_name", + "lore", + "click_commands", + "left_click_commands", + "right_click_commands", + "shift_left_click_commands", + "shift_right_click_commands" + ); + } + + private @NotNull List complete(final @NotNull Collection values, final @NotNull String argument) { + final String lowerArgument = argument.toLowerCase(Locale.ROOT); + return values.stream() + .filter(value -> value.toLowerCase(Locale.ROOT).startsWith(lowerArgument)) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/extendedclip/deluxemenus/command/subcommand/WebEditorCommand.java b/src/main/java/com/extendedclip/deluxemenus/command/subcommand/WebEditorCommand.java new file mode 100644 index 00000000..a94db548 --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/command/subcommand/WebEditorCommand.java @@ -0,0 +1,101 @@ +package com.extendedclip.deluxemenus.command.subcommand; + +import com.extendedclip.deluxemenus.DeluxeMenus; +import com.extendedclip.deluxemenus.menu.Menu; +import com.extendedclip.deluxemenus.utils.Messages; +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.stream.Collectors; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static net.kyori.adventure.text.Component.text; + +public class WebEditorCommand extends SubCommand { + + private static final String WEB_EDITOR_PERMISSION = "deluxemenus.webeditor"; + + public WebEditorCommand(final @NotNull DeluxeMenus plugin) { + super(plugin); + } + + @Override + public @NotNull String getName() { + return "webeditor"; + } + + @Override + public void execute(final @NotNull CommandSender sender, final @NotNull List arguments) { + if (!sender.hasPermission(WEB_EDITOR_PERMISSION)) { + plugin.sms(sender, Messages.NO_PERMISSION); + return; + } + + if (arguments.isEmpty()) { + plugin.sms(sender, Messages.WRONG_USAGE); + return; + } + + final Optional optionalMenu = Menu.getMenuByName(arguments.get(0)); + if (optionalMenu.isEmpty()) { + plugin.sms(sender, Messages.INVALID_MENU.message().replaceText(MENU_REPLACER_BUILDER.replacement(arguments.get(0)).build())); + return; + } + + final int port = arguments.size() >= 2 ? parsePort(arguments.get(1)) : 8765; + try { + final String url = plugin.getWebEditorServer().createSession(optionalMenu.get(), port); + plugin.sms(sender, text("Web editor link: ", NamedTextColor.GREEN) + .append(text(url, NamedTextColor.YELLOW).clickEvent(ClickEvent.openUrl(url)))); + } catch (final IOException exception) { + plugin.printStacktrace("Failed to start web editor.", exception); + plugin.sms(sender, text("Failed to start web editor.", NamedTextColor.RED)); + } + } + + @Override + public @Nullable List onTabComplete(final @NotNull CommandSender sender, final @NotNull List arguments) { + if (!sender.hasPermission(WEB_EDITOR_PERMISSION)) { + return null; + } + + if (arguments.isEmpty()) { + return List.of(getName()); + } + + if (arguments.size() == 1) { + return complete(List.of(getName()), arguments.get(0)); + } + + if (!getName().equalsIgnoreCase(arguments.get(0))) { + return null; + } + + if (arguments.size() == 2) { + return complete(Menu.getAllMenuNames(), arguments.get(1)); + } + + return null; + } + + private int parsePort(final @NotNull String input) { + try { + return Integer.parseInt(input); + } catch (final NumberFormatException exception) { + return 8765; + } + } + + private @NotNull List complete(final @NotNull Collection values, final @NotNull String argument) { + final String lowerArgument = argument.toLowerCase(Locale.ROOT); + return values.stream() + .filter(value -> value.toLowerCase(Locale.ROOT).startsWith(lowerArgument)) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/extendedclip/deluxemenus/editor/MenuConfigEditor.java b/src/main/java/com/extendedclip/deluxemenus/editor/MenuConfigEditor.java new file mode 100644 index 00000000..3999e242 --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/editor/MenuConfigEditor.java @@ -0,0 +1,169 @@ +package com.extendedclip.deluxemenus.editor; + +import com.extendedclip.deluxemenus.DeluxeMenus; +import com.extendedclip.deluxemenus.menu.Menu; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.configuration.file.YamlConfiguration; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class MenuConfigEditor { + + private final DeluxeMenus plugin; + + public MenuConfigEditor(final @NotNull DeluxeMenus plugin) { + this.plugin = plugin; + } + + public @NotNull Optional resolveFile(final @NotNull Menu menu) { + if ("config".equalsIgnoreCase(menu.path())) { + return Optional.empty(); + } + + final File directory = menu.options().subMenu() + ? plugin.getConfiguration().getSubMenuDirectory() + : plugin.getConfiguration().getMenuDirector(); + return Optional.of(new File(directory, menu.path())); + } + + public @NotNull String readRaw(final @NotNull Menu menu) throws IOException { + final Optional optionalFile = resolveFile(menu); + if (optionalFile.isPresent()) { + return Files.readString(optionalFile.get().toPath(), StandardCharsets.UTF_8); + } + + return plugin.getConfig().saveToString(); + } + + public void saveRaw(final @NotNull Menu menu, final @NotNull String raw) throws IOException { + final Optional optionalFile = resolveFile(menu); + if (optionalFile.isPresent()) { + Files.writeString(optionalFile.get().toPath(), raw, StandardCharsets.UTF_8); + return; + } + + try { + plugin.getConfig().loadFromString(raw); + plugin.saveConfig(); + } catch (final InvalidConfigurationException exception) { + throw new IOException("Invalid YAML", exception); + } + } + + public boolean setMaterial(final @NotNull Menu menu, final int slot, final @NotNull String material) throws IOException { + return setItemValue(menu, slot, "material", material); + } + + public boolean setItemValue(final @NotNull Menu menu, final int slot, final @NotNull String option, final @NotNull String value) throws IOException { + final YamlConfiguration config = load(menu); + final String itemPath = findOrCreateItemPath(config, menu, slot); + if (itemPath == null) { + return false; + } + + if (isListOption(option)) { + config.set(itemPath + "." + option, parseList(value)); + } else { + config.set(itemPath + "." + option, value); + } + + save(menu, config); + return true; + } + + public @NotNull Optional getItemString(final @NotNull Menu menu, final int slot, final @NotNull String option) { + final YamlConfiguration config = load(menu); + final String itemPath = findItemPath(config, menu, slot); + if (itemPath == null) { + return Optional.empty(); + } + + if (config.isList(itemPath + "." + option)) { + return Optional.of(String.join("\n", config.getStringList(itemPath + "." + option))); + } + + return Optional.ofNullable(config.getString(itemPath + "." + option)); + } + + private @NotNull YamlConfiguration load(final @NotNull Menu menu) { + final Optional optionalFile = resolveFile(menu); + return optionalFile + .map(YamlConfiguration::loadConfiguration) + .orElseGet(() -> { + final YamlConfiguration yaml = new YamlConfiguration(); + for (final String key : plugin.getConfig().getKeys(false)) { + yaml.set(key, plugin.getConfig().get(key)); + } + return yaml; + }); + } + + private void save(final @NotNull Menu menu, final @NotNull YamlConfiguration config) throws IOException { + final Optional optionalFile = resolveFile(menu); + if (optionalFile.isPresent()) { + config.save(optionalFile.get()); + return; + } + + for (final String configKey : config.getKeys(false)) { + plugin.getConfig().set(configKey, config.get(configKey)); + } + plugin.saveConfig(); + } + + private @Nullable String findOrCreateItemPath(final @NotNull YamlConfiguration config, final @NotNull Menu menu, final int slot) { + final String existingPath = findItemPath(config, menu, slot); + if (existingPath != null) { + return existingPath; + } + + final String root = getRoot(menu); + final String itemKey = "slot_" + slot; + final String itemPath = root + "items." + itemKey; + config.set(itemPath + ".slot", slot); + config.set(itemPath + ".material", "STONE"); + return itemPath; + } + + private @Nullable String findItemPath(final @NotNull YamlConfiguration config, final @NotNull Menu menu, final int slot) { + final String root = getRoot(menu); + final ConfigurationSection items = config.getConfigurationSection(root + "items"); + if (items == null) { + return null; + } + + for (final String key : items.getKeys(false)) { + final String itemPath = root + "items." + key; + if (config.getInt(itemPath + ".slot", 0) != slot) { + continue; + } + + return itemPath; + } + + return null; + } + + private @NotNull String getRoot(final @NotNull Menu menu) { + return resolveFile(menu).isPresent() ? "" : "gui_menus." + menu.options().name() + "."; + } + + private boolean isListOption(final @NotNull String option) { + return option.endsWith("_commands") || "lore".equals(option); + } + + private @NotNull List parseList(final @NotNull String value) { + if (value.isBlank()) { + return List.of(); + } + return Arrays.asList(value.split("\\R")); + } +} diff --git a/src/main/java/com/extendedclip/deluxemenus/editor/MenuEditPromptRegistry.java b/src/main/java/com/extendedclip/deluxemenus/editor/MenuEditPromptRegistry.java new file mode 100644 index 00000000..349cd560 --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/editor/MenuEditPromptRegistry.java @@ -0,0 +1,78 @@ +package com.extendedclip.deluxemenus.editor; + +import com.extendedclip.deluxemenus.DeluxeMenus; +import com.extendedclip.deluxemenus.menu.Menu; +import java.io.IOException; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import static net.kyori.adventure.text.Component.text; + +public final class MenuEditPromptRegistry { + + private static final Map PROMPTS = new ConcurrentHashMap<>(); + + private MenuEditPromptRegistry() { + } + + public static void begin(final @NotNull Player player, final @NotNull String menuName, final int slot, final @NotNull String option) { + PROMPTS.put(player.getUniqueId(), new Prompt(menuName, slot, option)); + } + + public static boolean hasPrompt(final @NotNull Player player) { + return PROMPTS.containsKey(player.getUniqueId()); + } + + public static void cancel(final @NotNull Player player) { + PROMPTS.remove(player.getUniqueId()); + } + + public static void submit(final @NotNull DeluxeMenus plugin, final @NotNull Player player, final @NotNull String value) { + final Prompt prompt = PROMPTS.remove(player.getUniqueId()); + if (prompt == null) { + return; + } + + if ("cancel".equalsIgnoreCase(value)) { + plugin.sms(player, text("Edit cancelled.", NamedTextColor.GRAY)); + return; + } + + final Optional optionalMenu = Menu.getMenuByName(prompt.menuName); + if (optionalMenu.isEmpty()) { + plugin.sms(player, text("Menu is no longer loaded.", NamedTextColor.RED)); + return; + } + + final Menu menu = optionalMenu.get(); + final MenuConfigEditor configEditor = new MenuConfigEditor(plugin); + try { + configEditor.setItemValue(menu, prompt.slot, prompt.option, value); + } catch (final IOException exception) { + plugin.printStacktrace("Failed to save menu edit.", exception); + plugin.sms(player, text("Failed to save menu edit.", NamedTextColor.RED)); + return; + } + + Menu.unload(plugin, menu.options().name()); + plugin.getConfiguration().loadGUIMenu(menu.options().name()); + plugin.sms(player, text("Updated " + prompt.option + " for slot " + prompt.slot + ".", NamedTextColor.GREEN)); + } + + private static class Prompt { + private final String menuName; + private final int slot; + private final String option; + + private Prompt(final @NotNull String menuName, final int slot, final @NotNull String option) { + this.menuName = menuName; + this.slot = slot; + this.option = option; + } + } +} diff --git a/src/main/java/com/extendedclip/deluxemenus/editor/MenuEditorHolder.java b/src/main/java/com/extendedclip/deluxemenus/editor/MenuEditorHolder.java new file mode 100644 index 00000000..86a0137a --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/editor/MenuEditorHolder.java @@ -0,0 +1,28 @@ +package com.extendedclip.deluxemenus.editor; + +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryHolder; +import org.jetbrains.annotations.NotNull; + +public class MenuEditorHolder implements InventoryHolder { + + private final String menuName; + private Inventory inventory; + + public MenuEditorHolder(final @NotNull String menuName) { + this.menuName = menuName; + } + + public @NotNull String menuName() { + return this.menuName; + } + + @Override + public @NotNull Inventory getInventory() { + return this.inventory; + } + + public void setInventory(final @NotNull Inventory inventory) { + this.inventory = inventory; + } +} diff --git a/src/main/java/com/extendedclip/deluxemenus/editor/MenuEditorListener.java b/src/main/java/com/extendedclip/deluxemenus/editor/MenuEditorListener.java new file mode 100644 index 00000000..b5fe0655 --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/editor/MenuEditorListener.java @@ -0,0 +1,92 @@ +package com.extendedclip.deluxemenus.editor; + +import com.extendedclip.deluxemenus.DeluxeMenus; +import com.extendedclip.deluxemenus.listener.Listener; +import com.extendedclip.deluxemenus.menu.Menu; +import com.extendedclip.deluxemenus.menu.MenuItem; +import java.util.Optional; +import java.util.TreeMap; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.player.AsyncPlayerChatEvent; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.InventoryHolder; +import org.jetbrains.annotations.NotNull; + +import static net.kyori.adventure.text.Component.newline; +import static net.kyori.adventure.text.Component.text; + +public class MenuEditorListener extends Listener { + + public MenuEditorListener(final @NotNull DeluxeMenus plugin) { + super(plugin); + } + + @EventHandler + public void onChat(final @NotNull AsyncPlayerChatEvent event) { + if (!MenuEditPromptRegistry.hasPrompt(event.getPlayer())) { + return; + } + + event.setCancelled(true); + plugin.getScheduler().runTask(event.getPlayer(), () -> MenuEditPromptRegistry.submit(plugin, event.getPlayer(), event.getMessage())); + } + + @EventHandler + public void onClick(final @NotNull InventoryClickEvent event) { + final InventoryHolder holder = event.getInventory().getHolder(); + if (!(holder instanceof MenuEditorHolder)) { + return; + } + + event.setCancelled(true); + if (!(event.getWhoClicked() instanceof Player)) { + return; + } + + final Player player = (Player) event.getWhoClicked(); + final MenuEditorHolder editorHolder = (MenuEditorHolder) holder; + final Optional optionalMenu = Menu.getMenuByName(editorHolder.menuName()); + if (optionalMenu.isEmpty()) { + player.closeInventory(); + return; + } + + final int slot = event.getRawSlot(); + final TreeMap items = optionalMenu.get().getMenuItems().get(slot); + if (items == null || items.isEmpty()) { + plugin.sms(player, text("Slot " + slot + " is empty.", NamedTextColor.GRAY) + .append(newline()) + .append(action(editorHolder.menuName(), slot, "material"))); + return; + } + + final MenuItem item = items.firstEntry().getValue(); + plugin.sms(player, text("Editing " + editorHolder.menuName() + " slot " + slot, NamedTextColor.GOLD) + .append(newline()) + .append(text("> Material: ", NamedTextColor.GRAY)) + .append(text(item.options().material(), NamedTextColor.WHITE)) + .append(newline()) + .append(text("> Priority: ", NamedTextColor.GRAY)) + .append(text(String.valueOf(item.options().priority()), NamedTextColor.WHITE)) + .append(newline()) + .append(action(editorHolder.menuName(), slot, "material")) + .append(newline()) + .append(action(editorHolder.menuName(), slot, "display_name")) + .append(newline()) + .append(action(editorHolder.menuName(), slot, "lore")) + .append(newline()) + .append(action(editorHolder.menuName(), slot, "left_click_commands")) + .append(newline()) + .append(action(editorHolder.menuName(), slot, "right_click_commands"))); + } + + private @NotNull Component action(final @NotNull String menuName, final int slot, final @NotNull String option) { + final String command = "/dm edit " + menuName + " prompt " + slot + " " + option; + return text(command, NamedTextColor.YELLOW) + .clickEvent(ClickEvent.runCommand(command)); + } +} diff --git a/src/main/java/com/extendedclip/deluxemenus/editor/MenuEditorManager.java b/src/main/java/com/extendedclip/deluxemenus/editor/MenuEditorManager.java new file mode 100644 index 00000000..40a8fda1 --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/editor/MenuEditorManager.java @@ -0,0 +1,49 @@ +package com.extendedclip.deluxemenus.editor; + +import com.extendedclip.deluxemenus.DeluxeMenus; +import com.extendedclip.deluxemenus.menu.Menu; +import com.extendedclip.deluxemenus.menu.MenuHolder; +import com.extendedclip.deluxemenus.menu.MenuItem; +import java.util.Map; +import java.util.TreeMap; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; + +public class MenuEditorManager { + + private final DeluxeMenus plugin; + + public MenuEditorManager(final @NotNull DeluxeMenus plugin) { + this.plugin = plugin; + } + + public void open(final @NotNull Player player, final @NotNull Menu menu) { + final int size = menu.options().size(); + final MenuEditorHolder editorHolder = new MenuEditorHolder(menu.options().name()); + final Inventory inventory = Bukkit.createInventory(editorHolder, size, "Editing: " + menu.options().name()); + editorHolder.setInventory(inventory); + + final MenuHolder renderHolder = new MenuHolder(plugin, player); + renderHolder.setMenuName(menu.options().name()); + renderHolder.setPlaceholderPlayer(player); + renderHolder.parsePlaceholdersInArguments(menu.options().parsePlaceholdersInArguments()); + renderHolder.parsePlaceholdersAfterArguments(menu.options().parsePlaceholdersAfterArguments()); + + for (final Map.Entry> entry : menu.getMenuItems().entrySet()) { + final int slot = entry.getKey(); + if (slot < 0 || slot >= size || entry.getValue().isEmpty()) { + continue; + } + + final ItemStack itemStack = entry.getValue().firstEntry().getValue().getItemStack(renderHolder); + if (itemStack != null) { + inventory.setItem(slot, itemStack); + } + } + + player.openInventory(inventory); + } +} diff --git a/src/main/java/com/extendedclip/deluxemenus/editor/WebEditorServer.java b/src/main/java/com/extendedclip/deluxemenus/editor/WebEditorServer.java new file mode 100644 index 00000000..e3ceb142 --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/editor/WebEditorServer.java @@ -0,0 +1,285 @@ +package com.extendedclip.deluxemenus.editor; + +import com.extendedclip.deluxemenus.DeluxeMenus; +import com.extendedclip.deluxemenus.menu.Menu; +import com.extendedclip.deluxemenus.menu.MenuItem; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpServer; +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.Instant; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class WebEditorServer { + + private final DeluxeMenus plugin; + private final MenuConfigEditor configEditor; + private final Map sessions = new ConcurrentHashMap<>(); + private HttpServer server; + + public WebEditorServer(final @NotNull DeluxeMenus plugin) { + this.plugin = plugin; + this.configEditor = new MenuConfigEditor(plugin); + } + + public @NotNull String createSession(final @NotNull Menu menu, final int requestedPort) throws IOException { + ensureStarted(requestedPort); + + final String token = UUID.randomUUID().toString().replace("-", ""); + sessions.put(token, new Session(menu.options().name(), Instant.now().plus(Duration.ofMinutes(15)))); + + final String host = plugin.getServer().getIp() == null || plugin.getServer().getIp().isBlank() + ? "localhost" + : plugin.getServer().getIp(); + return "http://" + host + ":" + server.getAddress().getPort() + "/dm-web/" + token; + } + + public void stop() { + if (server != null) { + server.stop(0); + server = null; + } + sessions.clear(); + } + + private void ensureStarted(final int requestedPort) throws IOException { + if (server != null) { + return; + } + + server = HttpServer.create(new InetSocketAddress(requestedPort), 0); + server.createContext("/dm-web", this::handle); + server.setExecutor(null); + server.start(); + } + + private void handle(final @NotNull HttpExchange exchange) throws IOException { + final String path = exchange.getRequestURI().getPath(); + final String[] parts = path.split("/"); + if (parts.length < 3) { + send(exchange, 404, "Not found", "text/plain"); + return; + } + + final String token = parts[2]; + final Optional optionalSession = getSession(token); + if (optionalSession.isEmpty()) { + send(exchange, 403, "Invalid or expired editor session.", "text/plain"); + return; + } + + if (parts.length >= 4 && "save".equals(parts[3]) && "POST".equalsIgnoreCase(exchange.getRequestMethod())) { + save(exchange, optionalSession.get()); + return; + } + + if (parts.length >= 4 && "save-item".equals(parts[3]) && "POST".equalsIgnoreCase(exchange.getRequestMethod())) { + saveItem(exchange, optionalSession.get()); + return; + } + + if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) { + send(exchange, 405, "Method not allowed", "text/plain"); + return; + } + + render(exchange, optionalSession.get()); + } + + private void render(final @NotNull HttpExchange exchange, final @NotNull Session session) throws IOException { + final Optional optionalMenu = Menu.getMenuByName(session.menuName); + if (optionalMenu.isEmpty()) { + send(exchange, 404, "Menu is not loaded.", "text/plain"); + return; + } + + final Menu menu = optionalMenu.get(); + final String html = "DeluxeMenus Editor" + + "" + + "
DeluxeMenus Web Editor - " + escape(menu.options().name()) + "
" + + "

Preview

" + renderGrid(menu) + "
" + + renderItemForms(menu, session) + + "

Raw YAML

" + + "
" + + "
" + + "
"; + send(exchange, 200, html, "text/html; charset=utf-8"); + } + + private void save(final @NotNull HttpExchange exchange, final @NotNull Session session) throws IOException { + final Optional optionalMenu = Menu.getMenuByName(session.menuName); + if (optionalMenu.isEmpty()) { + send(exchange, 404, "Menu is not loaded.", "text/plain"); + return; + } + + final String body = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); + final String content = parseFormValue(body, "content"); + configEditor.saveRaw(optionalMenu.get(), content); + final String menuName = optionalMenu.get().options().name(); + plugin.getScheduler().runTask(() -> { + Menu.unload(plugin, menuName); + plugin.getConfiguration().loadGUIMenu(menuName); + }); + send(exchange, 200, "Saved. You can go back and refresh the editor.", "text/plain"); + } + + private void saveItem(final @NotNull HttpExchange exchange, final @NotNull Session session) throws IOException { + final Optional optionalMenu = Menu.getMenuByName(session.menuName); + if (optionalMenu.isEmpty()) { + send(exchange, 404, "Menu is not loaded.", "text/plain"); + return; + } + + final Map form = parseForm(new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8)); + final int slot = parseInt(form.get("slot")); + final Menu menu = optionalMenu.get(); + configEditor.setItemValue(menu, slot, "material", form.getOrDefault("material", "STONE")); + configEditor.setItemValue(menu, slot, "display_name", form.getOrDefault("display_name", "")); + configEditor.setItemValue(menu, slot, "lore", form.getOrDefault("lore", "")); + configEditor.setItemValue(menu, slot, "left_click_commands", form.getOrDefault("left_click_commands", "")); + configEditor.setItemValue(menu, slot, "right_click_commands", form.getOrDefault("right_click_commands", "")); + configEditor.setItemValue(menu, slot, "click_commands", form.getOrDefault("click_commands", "")); + + final String menuName = menu.options().name(); + plugin.getScheduler().runTask(() -> { + Menu.unload(plugin, menuName); + plugin.getConfiguration().loadGUIMenu(menuName); + }); + send(exchange, 303, "", "text/plain", "/dm-web/" + session.token()); + } + + private @NotNull Optional getSession(final @NotNull String token) { + final Session session = sessions.get(token); + if (session == null || session.expiresAt.isBefore(Instant.now())) { + sessions.remove(token); + return Optional.empty(); + } + session.token = token; + return Optional.of(session); + } + + private @NotNull String renderGrid(final @NotNull Menu menu) { + final StringBuilder builder = new StringBuilder(); + for (int slot = 0; slot < menu.options().size(); slot++) { + final Map items = menu.getMenuItems().get(slot); + final boolean filled = items != null && !items.isEmpty(); + final String label = filled ? items.values().iterator().next().options().material() : String.valueOf(slot); + builder.append("
") + .append(slot) + .append("
"); + } + return builder.toString(); + } + + private @NotNull String renderItemForms(final @NotNull Menu menu, final @NotNull Session session) { + final StringBuilder builder = new StringBuilder("

Slot editor

"); + for (int slot = 0; slot < menu.options().size(); slot++) { + final Map items = menu.getMenuItems().get(slot); + if (items == null || items.isEmpty()) { + continue; + } + builder.append(renderItemForm(menu, session, slot)); + } + builder.append(renderItemForm(menu, session, 0)); + builder.append("
"); + return builder.toString(); + } + + private @NotNull String renderItemForm(final @NotNull Menu menu, final @NotNull Session session, final int defaultSlot) { + return "
" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "

"; + } + + private @NotNull String value(final @NotNull Menu menu, final int slot, final @NotNull String option) { + return configEditor.getItemString(menu, slot, option).orElse(""); + } + + private @NotNull String parseFormValue(final @NotNull String body, final @NotNull String key) { + return parseForm(body).getOrDefault(key, ""); + } + + private @NotNull Map parseForm(final @NotNull String body) { + final Map form = new ConcurrentHashMap<>(); + for (final String part : body.split("&")) { + final int index = part.indexOf('='); + if (index <= 0) { + continue; + } + final String formKey = URLDecoder.decode(part.substring(0, index), StandardCharsets.UTF_8); + form.put(formKey, URLDecoder.decode(part.substring(index + 1), StandardCharsets.UTF_8)); + } + return form; + } + + private int parseInt(final @Nullable String input) { + try { + return Integer.parseInt(input); + } catch (final Exception exception) { + return 0; + } + } + + private @NotNull String escape(final @NotNull String input) { + return input.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """); + } + + private void send(final @NotNull HttpExchange exchange, final int status, final @NotNull String body, final @NotNull String contentType) throws IOException { + send(exchange, status, body, contentType, null); + } + + private void send(final @NotNull HttpExchange exchange, final int status, final @NotNull String body, final @NotNull String contentType, final @Nullable String location) throws IOException { + final byte[] bytes = body.getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().set("Content-Type", contentType); + if (location != null) { + exchange.getResponseHeaders().set("Location", location); + } + exchange.sendResponseHeaders(status, bytes.length); + try (OutputStream output = exchange.getResponseBody()) { + output.write(bytes); + } + } + + private static class Session { + private final String menuName; + private final Instant expiresAt; + private String token; + + private Session(final @NotNull String menuName, final @NotNull Instant expiresAt) { + this.menuName = menuName; + this.expiresAt = expiresAt; + } + + private @NotNull String token() { + return token; + } + } +} diff --git a/src/main/java/com/extendedclip/deluxemenus/utils/Messages.java b/src/main/java/com/extendedclip/deluxemenus/utils/Messages.java index 01919b25..e30c7446 100644 --- a/src/main/java/com/extendedclip/deluxemenus/utils/Messages.java +++ b/src/main/java/com/extendedclip/deluxemenus/utils/Messages.java @@ -70,11 +70,23 @@ public enum Messages { .append(newline()) .append(text(">", NamedTextColor.AQUA)) .append(space().append(space())) - .append(text("/dm dump ", NamedTextColor.WHITE)) - .append(newline()) - .append(text(">", NamedTextColor.AQUA)) - .append(space().append(space())) - .append(text("/dm meta ", NamedTextColor.WHITE)) + .append(text("/dm dump ", NamedTextColor.WHITE)) + .append(newline()) + .append(text(">", NamedTextColor.AQUA)) + .append(space().append(space())) + .append(text("/dm debug ", NamedTextColor.WHITE)) + .append(newline()) + .append(text(">", NamedTextColor.AQUA)) + .append(space().append(space())) + .append(text("/dm edit ", NamedTextColor.WHITE)) + .append(newline()) + .append(text(">", NamedTextColor.AQUA)) + .append(space().append(space())) + .append(text("/dm webeditor ", NamedTextColor.WHITE)) + .append(newline()) + .append(text(">", NamedTextColor.AQUA)) + .append(space().append(space())) + .append(text("/dm meta ", NamedTextColor.WHITE)) .append(newline()) .append(text(">", NamedTextColor.AQUA)) .append(space().append(space())) diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index ec50f892..27036d61 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -11,10 +11,19 @@ commands: description: DeluxeMenus main commands aliases: [ dm, deluxemenu, dmenu ] permissions: - deluxemenus.admin: - description: admin commands - default: op - deluxemenus.open: + deluxemenus.admin: + description: admin commands + default: op + deluxemenus.debug: + description: debug placeholders, menu items, and hooked items + default: op + deluxemenus.edit: + description: edit menus in game + default: op + deluxemenus.webeditor: + description: create temporary web editor sessions + default: op + deluxemenus.open: description: open a menu with /dm open default: op deluxemenus.open.others: From f9abc8154ee28cf9f447f5fce37da450d8178fa0 Mon Sep 17 00:00:00 2001 From: unknown <1tranducnhan.2021@gmail.com> Date: Thu, 21 May 2026 20:45:53 +0700 Subject: [PATCH 05/12] Fix MiniMessage title fallback --- .../deluxemenus/utils/StringUtils.java | 77 ++++++++++++++++++- 1 file changed, 75 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/extendedclip/deluxemenus/utils/StringUtils.java b/src/main/java/com/extendedclip/deluxemenus/utils/StringUtils.java index c8b4b23e..2bf9d046 100644 --- a/src/main/java/com/extendedclip/deluxemenus/utils/StringUtils.java +++ b/src/main/java/com/extendedclip/deluxemenus/utils/StringUtils.java @@ -16,8 +16,41 @@ public class StringUtils { private final static Pattern HEX_PATTERN = Pattern .compile("&(#[a-f0-9]{6})", Pattern.CASE_INSENSITIVE); + private final static Pattern MINI_HEX_PATTERN = Pattern + .compile("<(?:color:)?#([a-f0-9]{6})>", Pattern.CASE_INSENSITIVE); private final static MiniMessage MINI_MESSAGE = MiniMessage.miniMessage(); private final static LegacyComponentSerializer LEGACY_SECTION = LegacyComponentSerializer.legacySection(); + private final static Map MINI_TITLE_TAGS = Map.ofEntries( + Map.entry("black", ChatColor.BLACK.toString()), + Map.entry("dark_blue", ChatColor.DARK_BLUE.toString()), + Map.entry("dark_green", ChatColor.DARK_GREEN.toString()), + Map.entry("dark_aqua", ChatColor.DARK_AQUA.toString()), + Map.entry("dark_red", ChatColor.DARK_RED.toString()), + Map.entry("dark_purple", ChatColor.DARK_PURPLE.toString()), + Map.entry("gold", ChatColor.GOLD.toString()), + Map.entry("gray", ChatColor.GRAY.toString()), + Map.entry("grey", ChatColor.GRAY.toString()), + Map.entry("dark_gray", ChatColor.DARK_GRAY.toString()), + Map.entry("dark_grey", ChatColor.DARK_GRAY.toString()), + Map.entry("blue", ChatColor.BLUE.toString()), + Map.entry("green", ChatColor.GREEN.toString()), + Map.entry("aqua", ChatColor.AQUA.toString()), + Map.entry("red", ChatColor.RED.toString()), + Map.entry("light_purple", ChatColor.LIGHT_PURPLE.toString()), + Map.entry("yellow", ChatColor.YELLOW.toString()), + Map.entry("white", ChatColor.WHITE.toString()), + Map.entry("bold", ChatColor.BOLD.toString()), + Map.entry("b", ChatColor.BOLD.toString()), + Map.entry("italic", ChatColor.ITALIC.toString()), + Map.entry("i", ChatColor.ITALIC.toString()), + Map.entry("underlined", ChatColor.UNDERLINE.toString()), + Map.entry("u", ChatColor.UNDERLINE.toString()), + Map.entry("strikethrough", ChatColor.STRIKETHROUGH.toString()), + Map.entry("st", ChatColor.STRIKETHROUGH.toString()), + Map.entry("obfuscated", ChatColor.MAGIC.toString()), + Map.entry("obf", ChatColor.MAGIC.toString()), + Map.entry("reset", ChatColor.RESET.toString()) + ); /** * Translates the ampersand color codes like '&7' to their section symbol counterparts like '§7'. @@ -43,10 +76,50 @@ public static String color(@NotNull String input) { @NotNull public static String colorMenuTitle(@NotNull final String input) { try { - return color(LEGACY_SECTION.serialize(MINI_MESSAGE.deserialize(input))); + final String parsed = color(LEGACY_SECTION.serialize(MINI_MESSAGE.deserialize(input))); + if (!hasMiniTitleTag(parsed)) { + return parsed; + } } catch (final Exception ignored) { - return color(input); } + return color(replaceMiniTitleTags(input)); + } + + private static boolean hasMiniTitleTag(@NotNull final String input) { + return input.indexOf('<') != -1 && input.indexOf('>') != -1; + } + + @NotNull + private static String replaceMiniTitleTags(@NotNull String input) { + if (VersionHelper.IS_HEX_VERSION) { + final Matcher matcher = MINI_HEX_PATTERN.matcher(input); + final StringBuffer buffer = new StringBuffer(); + while (matcher.find()) { + matcher.appendReplacement(buffer, Matcher.quoteReplacement(ChatColor.of("#" + matcher.group(1)).toString())); + } + matcher.appendTail(buffer); + input = buffer.toString(); + } + + for (final Map.Entry entry : MINI_TITLE_TAGS.entrySet()) { + input = replaceMiniTitleTag(input, entry.getKey(), entry.getValue()); + } + + return input; + } + + @NotNull + private static String replaceMiniTitleTag( + @NotNull String input, + @NotNull final String tag, + @NotNull final String replacement + ) { + input = Pattern.compile("<" + tag + ">", Pattern.CASE_INSENSITIVE) + .matcher(input) + .replaceAll(Matcher.quoteReplacement(replacement)); + return Pattern.compile("", Pattern.CASE_INSENSITIVE) + .matcher(input) + .replaceAll(Matcher.quoteReplacement(ChatColor.RESET.toString())); } @NotNull From 43bf81a653c0af3b03996b44e70a095b06fca85a Mon Sep 17 00:00:00 2001 From: unknown <1tranducnhan.2021@gmail.com> Date: Thu, 21 May 2026 20:59:58 +0700 Subject: [PATCH 06/12] Polish menu editor tools --- .../command/DeluxeMenusCommand.java | 4 +- .../command/subcommand/EditCommand.java | 126 +++++- .../command/subcommand/WebEditorCommand.java | 29 +- .../deluxemenus/editor/MenuConfigEditor.java | 130 +++++- .../editor/MenuEditPromptRegistry.java | 15 +- .../deluxemenus/editor/MenuEditorHolder.java | 21 + .../editor/MenuEditorListener.java | 176 ++++++-- .../deluxemenus/editor/MenuEditorManager.java | 153 ++++++- .../deluxemenus/editor/WebEditorServer.java | 390 +++++++++++++----- 9 files changed, 895 insertions(+), 149 deletions(-) diff --git a/src/main/java/com/extendedclip/deluxemenus/command/DeluxeMenusCommand.java b/src/main/java/com/extendedclip/deluxemenus/command/DeluxeMenusCommand.java index c6ee2db9..953fbb26 100644 --- a/src/main/java/com/extendedclip/deluxemenus/command/DeluxeMenusCommand.java +++ b/src/main/java/com/extendedclip/deluxemenus/command/DeluxeMenusCommand.java @@ -99,7 +99,9 @@ private void registerSubCommands() { new OpenCommand(plugin), new RefreshCommand(plugin), new ReloadCommand(plugin), - new WebEditorCommand(plugin) + new WebEditorCommand(plugin), + new WebEditorCommand(plugin, "browsereditor"), + new WebEditorCommand(plugin, "browseeditor") ); for (final SubCommand subCommand : commands) { diff --git a/src/main/java/com/extendedclip/deluxemenus/command/subcommand/EditCommand.java b/src/main/java/com/extendedclip/deluxemenus/command/subcommand/EditCommand.java index 4cacf734..104b12f6 100644 --- a/src/main/java/com/extendedclip/deluxemenus/command/subcommand/EditCommand.java +++ b/src/main/java/com/extendedclip/deluxemenus/command/subcommand/EditCommand.java @@ -48,7 +48,7 @@ public void execute(final @NotNull CommandSender sender, final @NotNull List optionalMenu = Menu.getMenuByName(arguments.get(0)); + final Optional optionalMenu = findMenu(arguments.get(0)); if (optionalMenu.isEmpty()) { plugin.sms(sender, Messages.INVALID_MENU.message().replaceText(MENU_REPLACER_BUILDER.replacement(arguments.get(0)).build())); return; @@ -64,6 +64,12 @@ public void execute(final @NotNull CommandSender sender, final @NotNull List= 5 && "set".equalsIgnoreCase(arguments.get(1))) { setValue(sender, optionalMenu.get(), arguments); @@ -76,6 +82,18 @@ public void execute(final @NotNull CommandSender sender, final @NotNull List= 4 + && "menu".equalsIgnoreCase(arguments.get(1))) { + setMenuValue(sender, optionalMenu.get(), arguments); + return; + } + + if (arguments.size() == 3 + && "delete".equalsIgnoreCase(arguments.get(1))) { + deleteItem(sender, optionalMenu.get(), arguments.get(2)); + return; + } + plugin.sms(sender, Messages.WRONG_USAGE); } @@ -98,20 +116,38 @@ public void execute(final @NotNull CommandSender sender, final @NotNull List arguments) { final int slot; try { @@ -139,8 +175,7 @@ private void setValue(final @NotNull CommandSender sender, final @NotNull Menu m return; } - Menu.unload(plugin, menu.options().name()); - plugin.getConfiguration().loadGUIMenu(menu.options().name()); + configEditor.reload(menu); plugin.sms(sender, Component.text("Updated " + option + " for slot " + slot + ".", NamedTextColor.GREEN)); } @@ -169,19 +204,96 @@ private void prompt(final @NotNull CommandSender sender, final @NotNull Menu men plugin.sms(player, Component.text("Type the new " + option + " in chat, or type cancel.", NamedTextColor.YELLOW)); } + private void setMenuValue(final @NotNull CommandSender sender, final @NotNull Menu menu, final @NotNull List arguments) { + final String option = arguments.get(2).toLowerCase(Locale.ROOT); + if (!menuOptions().contains(option)) { + plugin.sms(sender, Component.text("Unknown editable menu option: " + option, NamedTextColor.RED)); + return; + } + + final String value = String.join(" ", arguments.subList(3, arguments.size())); + try { + configEditor.setMenuValue(menu, option, value); + } catch (final IOException exception) { + plugin.printStacktrace("Failed to save menu edit.", exception); + plugin.sms(sender, Component.text("Failed to save menu edit.", NamedTextColor.RED)); + return; + } + + configEditor.reload(menu); + plugin.sms(sender, Component.text("Updated " + option + " for " + menu.options().name() + ".", NamedTextColor.GREEN)); + } + + private void deleteItem(final @NotNull CommandSender sender, final @NotNull Menu menu, final @NotNull String slotInput) { + final int slot = parseSlot(sender, slotInput); + if (slot < 0) { + return; + } + + try { + if (!configEditor.deleteItem(menu, slot)) { + plugin.sms(sender, Component.text("No item config was found in slot " + slot + ".", NamedTextColor.RED)); + return; + } + } catch (final IOException exception) { + plugin.printStacktrace("Failed to delete menu item.", exception); + plugin.sms(sender, Component.text("Failed to delete menu item.", NamedTextColor.RED)); + return; + } + + configEditor.reload(menu); + plugin.sms(sender, Component.text("Deleted item config for slot " + slot + ".", NamedTextColor.GREEN)); + } + + private int parseSlot(final @NotNull CommandSender sender, final @NotNull String input) { + try { + return Integer.parseInt(input); + } catch (final NumberFormatException exception) { + plugin.sms(sender, Messages.WRONG_USAGE); + return -1; + } + } + private @NotNull List editableOptions() { return List.of( "material", + "amount", + "priority", "display_name", "lore", + "model_data", + "item_flags", + "update", "click_commands", "left_click_commands", "right_click_commands", "shift_left_click_commands", - "shift_right_click_commands" + "shift_right_click_commands", + "middle_click_commands" ); } + private @NotNull List menuOptions() { + return List.of("menu_title", "size"); + } + + private @NotNull Optional findMenu(final @NotNull String menuName) { + final Optional menu = Menu.getMenuByName(menuName); + if (menu.isPresent()) { + return menu; + } + + return Menu.getSubMenuByName(menuName); + } + + private @NotNull Collection menuNames() { + return java.util.stream.Stream.concat( + Menu.getAllMenuNames().stream(), + Menu.getAllSubMenus().stream().map(menu -> menu.options().name()) + ) + .collect(Collectors.toList()); + } + private @NotNull List complete(final @NotNull Collection values, final @NotNull String argument) { final String lowerArgument = argument.toLowerCase(Locale.ROOT); return values.stream() diff --git a/src/main/java/com/extendedclip/deluxemenus/command/subcommand/WebEditorCommand.java b/src/main/java/com/extendedclip/deluxemenus/command/subcommand/WebEditorCommand.java index a94db548..05a1c28e 100644 --- a/src/main/java/com/extendedclip/deluxemenus/command/subcommand/WebEditorCommand.java +++ b/src/main/java/com/extendedclip/deluxemenus/command/subcommand/WebEditorCommand.java @@ -20,14 +20,20 @@ public class WebEditorCommand extends SubCommand { private static final String WEB_EDITOR_PERMISSION = "deluxemenus.webeditor"; + private final String name; public WebEditorCommand(final @NotNull DeluxeMenus plugin) { + this(plugin, "webeditor"); + } + + public WebEditorCommand(final @NotNull DeluxeMenus plugin, final @NotNull String name) { super(plugin); + this.name = name; } @Override public @NotNull String getName() { - return "webeditor"; + return name; } @Override @@ -42,7 +48,7 @@ public void execute(final @NotNull CommandSender sender, final @NotNull List optionalMenu = Menu.getMenuByName(arguments.get(0)); + final Optional optionalMenu = findMenu(arguments.get(0)); if (optionalMenu.isEmpty()) { plugin.sms(sender, Messages.INVALID_MENU.message().replaceText(MENU_REPLACER_BUILDER.replacement(arguments.get(0)).build())); return; @@ -78,7 +84,7 @@ public void execute(final @NotNull CommandSender sender, final @NotNull List value.toLowerCase(Locale.ROOT).startsWith(lowerArgument)) .collect(Collectors.toList()); } + + private @NotNull Optional findMenu(final @NotNull String menuName) { + final Optional menu = Menu.getMenuByName(menuName); + if (menu.isPresent()) { + return menu; + } + + return Menu.getSubMenuByName(menuName); + } + + private @NotNull Collection menuNames() { + return java.util.stream.Stream.concat( + Menu.getAllMenuNames().stream(), + Menu.getAllSubMenus().stream().map(menu -> menu.options().name()) + ) + .collect(Collectors.toList()); + } } diff --git a/src/main/java/com/extendedclip/deluxemenus/editor/MenuConfigEditor.java b/src/main/java/com/extendedclip/deluxemenus/editor/MenuConfigEditor.java index 3999e242..a9244698 100644 --- a/src/main/java/com/extendedclip/deluxemenus/editor/MenuConfigEditor.java +++ b/src/main/java/com/extendedclip/deluxemenus/editor/MenuConfigEditor.java @@ -9,6 +9,7 @@ import java.util.Arrays; import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.InvalidConfigurationException; import org.bukkit.configuration.file.YamlConfiguration; @@ -71,6 +72,12 @@ public boolean setItemValue(final @NotNull Menu menu, final int slot, final @Not if (isListOption(option)) { config.set(itemPath + "." + option, parseList(value)); + } else if (isIntegerOption(option)) { + config.set(itemPath + "." + option, parseInteger(value)); + } else if (isBooleanOption(option)) { + config.set(itemPath + "." + option, Boolean.parseBoolean(value)); + } else if (value.isBlank() && isOptionalOption(option)) { + config.set(itemPath + "." + option, null); } else { config.set(itemPath + "." + option, value); } @@ -79,6 +86,41 @@ public boolean setItemValue(final @NotNull Menu menu, final int slot, final @Not return true; } + public boolean deleteItem(final @NotNull Menu menu, final int slot) throws IOException { + final YamlConfiguration config = load(menu); + final String itemPath = findItemPath(config, menu, slot); + if (itemPath == null) { + return false; + } + + config.set(itemPath, null); + save(menu, config); + return true; + } + + public boolean setMenuValue(final @NotNull Menu menu, final @NotNull String option, final @NotNull String value) throws IOException { + final YamlConfiguration config = load(menu); + final String root = getRoot(menu); + + if ("size".equals(option)) { + config.set(root + option, parseInteger(value)); + } else if (value.isBlank()) { + config.set(root + option, null); + } else { + config.set(root + option, value); + } + + save(menu, config); + return true; + } + + public @NotNull Optional getMenuString(final @NotNull Menu menu, final @NotNull String option) { + final YamlConfiguration config = load(menu); + final String root = getRoot(menu); + final Object value = config.get(root + option); + return Optional.ofNullable(value).map(String::valueOf); + } + public @NotNull Optional getItemString(final @NotNull Menu menu, final int slot, final @NotNull String option) { final YamlConfiguration config = load(menu); final String itemPath = findItemPath(config, menu, slot); @@ -90,7 +132,26 @@ public boolean setItemValue(final @NotNull Menu menu, final int slot, final @Not return Optional.of(String.join("\n", config.getStringList(itemPath + "." + option))); } - return Optional.ofNullable(config.getString(itemPath + "." + option)); + return Optional.ofNullable(config.get(itemPath + "." + option)).map(String::valueOf); + } + + public void reload(final @NotNull Menu menu) { + final String menuName = menu.options().name(); + final boolean subMenu = menu.options().subMenu(); + final boolean mainConfigMenu = "config".equalsIgnoreCase(menu.path()); + + Menu.unload(plugin, menuName); + if (subMenu) { + if (mainConfigMenu) { + plugin.getConfiguration().loadSubMenus(); + return; + } + + plugin.getConfiguration().loadSubMenuFromFile(menuName); + return; + } + + plugin.getConfiguration().loadGUIMenu(menuName); } private @NotNull YamlConfiguration load(final @NotNull Menu menu) { @@ -142,7 +203,7 @@ private void save(final @NotNull Menu menu, final @NotNull YamlConfiguration con for (final String key : items.getKeys(false)) { final String itemPath = root + "items." + key; - if (config.getInt(itemPath + ".slot", 0) != slot) { + if (!matchesSlot(config, itemPath, slot)) { continue; } @@ -153,17 +214,76 @@ private void save(final @NotNull Menu menu, final @NotNull YamlConfiguration con } private @NotNull String getRoot(final @NotNull Menu menu) { - return resolveFile(menu).isPresent() ? "" : "gui_menus." + menu.options().name() + "."; + if (resolveFile(menu).isPresent()) { + return ""; + } + + final String section = menu.options().subMenu() ? "sub_menus" : "gui_menus"; + return section + "." + menu.options().name() + "."; } private boolean isListOption(final @NotNull String option) { - return option.endsWith("_commands") || "lore".equals(option); + return option.endsWith("_commands") || "lore".equals(option) || "item_flags".equals(option); + } + + private boolean isIntegerOption(final @NotNull String option) { + return "amount".equals(option) || "priority".equals(option) || "slot".equals(option); + } + + private boolean isBooleanOption(final @NotNull String option) { + return "update".equals(option); + } + + private boolean isOptionalOption(final @NotNull String option) { + return !"material".equals(option); + } + + private int parseInteger(final @NotNull String value) { + try { + return Integer.parseInt(value.trim()); + } catch (final NumberFormatException exception) { + return 0; + } } private @NotNull List parseList(final @NotNull String value) { if (value.isBlank()) { return List.of(); } - return Arrays.asList(value.split("\\R")); + return Arrays.stream(value.split("\\R|\\s\\|\\s|\\|")) + .map(String::trim) + .filter(line -> !line.isEmpty()) + .collect(Collectors.toList()); + } + + private boolean matchesSlot(final @NotNull YamlConfiguration config, final @NotNull String itemPath, final int slot) { + if (config.getInt(itemPath + ".slot", Integer.MIN_VALUE) == slot) { + return true; + } + + if (!config.isList(itemPath + ".slots")) { + return false; + } + + for (final String configuredSlot : config.getStringList(itemPath + ".slots")) { + if (configuredSlotMatches(configuredSlot, slot)) { + return true; + } + } + + return false; + } + + private boolean configuredSlotMatches(final @NotNull String configuredSlot, final int slot) { + final String[] range = configuredSlot.split("-", 2); + try { + if (range.length == 2) { + return slot >= Integer.parseInt(range[0].trim()) && slot <= Integer.parseInt(range[1].trim()); + } + + return slot == Integer.parseInt(configuredSlot.trim()); + } catch (final NumberFormatException exception) { + return false; + } } } diff --git a/src/main/java/com/extendedclip/deluxemenus/editor/MenuEditPromptRegistry.java b/src/main/java/com/extendedclip/deluxemenus/editor/MenuEditPromptRegistry.java index 349cd560..cf1c6a2d 100644 --- a/src/main/java/com/extendedclip/deluxemenus/editor/MenuEditPromptRegistry.java +++ b/src/main/java/com/extendedclip/deluxemenus/editor/MenuEditPromptRegistry.java @@ -43,7 +43,7 @@ public static void submit(final @NotNull DeluxeMenus plugin, final @NotNull Play return; } - final Optional optionalMenu = Menu.getMenuByName(prompt.menuName); + final Optional optionalMenu = findMenu(prompt.menuName); if (optionalMenu.isEmpty()) { plugin.sms(player, text("Menu is no longer loaded.", NamedTextColor.RED)); return; @@ -59,9 +59,18 @@ public static void submit(final @NotNull DeluxeMenus plugin, final @NotNull Play return; } - Menu.unload(plugin, menu.options().name()); - plugin.getConfiguration().loadGUIMenu(menu.options().name()); + configEditor.reload(menu); plugin.sms(player, text("Updated " + prompt.option + " for slot " + prompt.slot + ".", NamedTextColor.GREEN)); + findMenu(prompt.menuName).ifPresent(reloaded -> new MenuEditorManager(plugin).openSlot(player, reloaded, prompt.slot)); + } + + private static @NotNull Optional findMenu(final @NotNull String menuName) { + final Optional menu = Menu.getMenuByName(menuName); + if (menu.isPresent()) { + return menu; + } + + return Menu.getSubMenuByName(menuName); } private static class Prompt { diff --git a/src/main/java/com/extendedclip/deluxemenus/editor/MenuEditorHolder.java b/src/main/java/com/extendedclip/deluxemenus/editor/MenuEditorHolder.java index 86a0137a..08ba887e 100644 --- a/src/main/java/com/extendedclip/deluxemenus/editor/MenuEditorHolder.java +++ b/src/main/java/com/extendedclip/deluxemenus/editor/MenuEditorHolder.java @@ -6,17 +6,38 @@ public class MenuEditorHolder implements InventoryHolder { + public enum View { + PREVIEW, + SLOT + } + private final String menuName; + private final View view; + private final int slot; private Inventory inventory; public MenuEditorHolder(final @NotNull String menuName) { + this(menuName, View.PREVIEW, -1); + } + + public MenuEditorHolder(final @NotNull String menuName, final @NotNull View view, final int slot) { this.menuName = menuName; + this.view = view; + this.slot = slot; } public @NotNull String menuName() { return this.menuName; } + public @NotNull View view() { + return this.view; + } + + public int slot() { + return this.slot; + } + @Override public @NotNull Inventory getInventory() { return this.inventory; diff --git a/src/main/java/com/extendedclip/deluxemenus/editor/MenuEditorListener.java b/src/main/java/com/extendedclip/deluxemenus/editor/MenuEditorListener.java index b5fe0655..f495ed96 100644 --- a/src/main/java/com/extendedclip/deluxemenus/editor/MenuEditorListener.java +++ b/src/main/java/com/extendedclip/deluxemenus/editor/MenuEditorListener.java @@ -3,16 +3,17 @@ import com.extendedclip.deluxemenus.DeluxeMenus; import com.extendedclip.deluxemenus.listener.Listener; import com.extendedclip.deluxemenus.menu.Menu; -import com.extendedclip.deluxemenus.menu.MenuItem; +import java.io.IOException; import java.util.Optional; -import java.util.TreeMap; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; -import org.bukkit.event.player.AsyncPlayerChatEvent; import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryDragEvent; +import org.bukkit.event.player.AsyncPlayerChatEvent; +import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.inventory.InventoryHolder; import org.jetbrains.annotations.NotNull; @@ -21,8 +22,13 @@ public class MenuEditorListener extends Listener { + private final MenuEditorManager editorManager; + private final MenuConfigEditor configEditor; + public MenuEditorListener(final @NotNull DeluxeMenus plugin) { super(plugin); + this.editorManager = new MenuEditorManager(plugin); + this.configEditor = new MenuConfigEditor(plugin); } @EventHandler @@ -35,6 +41,18 @@ public void onChat(final @NotNull AsyncPlayerChatEvent event) { plugin.getScheduler().runTask(event.getPlayer(), () -> MenuEditPromptRegistry.submit(plugin, event.getPlayer(), event.getMessage())); } + @EventHandler + public void onQuit(final @NotNull PlayerQuitEvent event) { + MenuEditPromptRegistry.cancel(event.getPlayer()); + } + + @EventHandler + public void onDrag(final @NotNull InventoryDragEvent event) { + if (event.getInventory().getHolder() instanceof MenuEditorHolder) { + event.setCancelled(true); + } + } + @EventHandler public void onClick(final @NotNull InventoryClickEvent event) { final InventoryHolder holder = event.getInventory().getHolder(); @@ -49,44 +67,144 @@ public void onClick(final @NotNull InventoryClickEvent event) { final Player player = (Player) event.getWhoClicked(); final MenuEditorHolder editorHolder = (MenuEditorHolder) holder; - final Optional optionalMenu = Menu.getMenuByName(editorHolder.menuName()); + final Optional optionalMenu = findMenu(editorHolder.menuName()); if (optionalMenu.isEmpty()) { player.closeInventory(); + plugin.sms(player, text("Menu is no longer loaded.", NamedTextColor.RED)); return; } final int slot = event.getRawSlot(); - final TreeMap items = optionalMenu.get().getMenuItems().get(slot); - if (items == null || items.isEmpty()) { - plugin.sms(player, text("Slot " + slot + " is empty.", NamedTextColor.GRAY) - .append(newline()) - .append(action(editorHolder.menuName(), slot, "material"))); + if (slot < 0 || slot >= event.getInventory().getSize()) { return; } - final MenuItem item = items.firstEntry().getValue(); - plugin.sms(player, text("Editing " + editorHolder.menuName() + " slot " + slot, NamedTextColor.GOLD) - .append(newline()) - .append(text("> Material: ", NamedTextColor.GRAY)) - .append(text(item.options().material(), NamedTextColor.WHITE)) - .append(newline()) - .append(text("> Priority: ", NamedTextColor.GRAY)) - .append(text(String.valueOf(item.options().priority()), NamedTextColor.WHITE)) - .append(newline()) - .append(action(editorHolder.menuName(), slot, "material")) - .append(newline()) - .append(action(editorHolder.menuName(), slot, "display_name")) - .append(newline()) - .append(action(editorHolder.menuName(), slot, "lore")) + if (editorHolder.view() == MenuEditorHolder.View.PREVIEW) { + editorManager.openSlot(player, optionalMenu.get(), slot); + return; + } + + handleSlotEditor(player, optionalMenu.get(), editorHolder.slot(), slot); + } + + private void handleSlotEditor( + final @NotNull Player player, + final @NotNull Menu menu, + final int editedSlot, + final int buttonSlot + ) { + switch (buttonSlot) { + case MenuEditorManager.BUTTON_MATERIAL: + beginPrompt(player, menu, editedSlot, "material"); + break; + case MenuEditorManager.BUTTON_AMOUNT: + beginPrompt(player, menu, editedSlot, "amount"); + break; + case MenuEditorManager.BUTTON_DISPLAY_NAME: + beginPrompt(player, menu, editedSlot, "display_name"); + break; + case MenuEditorManager.BUTTON_LORE: + beginPrompt(player, menu, editedSlot, "lore"); + break; + case MenuEditorManager.BUTTON_MODEL_DATA: + beginPrompt(player, menu, editedSlot, "model_data"); + break; + case MenuEditorManager.BUTTON_ITEM_FLAGS: + beginPrompt(player, menu, editedSlot, "item_flags"); + break; + case MenuEditorManager.BUTTON_UPDATE: + toggleUpdate(player, menu, editedSlot); + break; + case MenuEditorManager.BUTTON_CLICK_COMMANDS: + beginPrompt(player, menu, editedSlot, "click_commands"); + break; + case MenuEditorManager.BUTTON_LEFT_CLICK_COMMANDS: + beginPrompt(player, menu, editedSlot, "left_click_commands"); + break; + case MenuEditorManager.BUTTON_RIGHT_CLICK_COMMANDS: + beginPrompt(player, menu, editedSlot, "right_click_commands"); + break; + case MenuEditorManager.BUTTON_SHIFT_LEFT_CLICK_COMMANDS: + beginPrompt(player, menu, editedSlot, "shift_left_click_commands"); + break; + case MenuEditorManager.BUTTON_SHIFT_RIGHT_CLICK_COMMANDS: + beginPrompt(player, menu, editedSlot, "shift_right_click_commands"); + break; + case MenuEditorManager.BUTTON_MIDDLE_CLICK_COMMANDS: + beginPrompt(player, menu, editedSlot, "middle_click_commands"); + break; + case MenuEditorManager.BUTTON_PRIORITY: + beginPrompt(player, menu, editedSlot, "priority"); + break; + case MenuEditorManager.BUTTON_DELETE: + deleteItem(player, menu, editedSlot); + break; + case MenuEditorManager.BUTTON_BACK: + editorManager.open(player, menu); + break; + case MenuEditorManager.BUTTON_REFRESH: + editorManager.openSlot(player, menu, editedSlot); + break; + default: + break; + } + } + + private void beginPrompt( + final @NotNull Player player, + final @NotNull Menu menu, + final int slot, + final @NotNull String option + ) { + MenuEditPromptRegistry.begin(player, menu.options().name(), slot, option); + player.closeInventory(); + + final String current = configEditor.getItemString(menu, slot, option).orElse("-"); + plugin.sms(player, text("Editing " + option + " for " + menu.options().name() + " slot " + slot, NamedTextColor.GOLD) .append(newline()) - .append(action(editorHolder.menuName(), slot, "left_click_commands")) + .append(text("Current: ", NamedTextColor.GRAY)) + .append(text(current, NamedTextColor.WHITE)) .append(newline()) - .append(action(editorHolder.menuName(), slot, "right_click_commands"))); + .append(text("Send a value in chat. Use | between lore or command lines. ", NamedTextColor.YELLOW)) + .append(text("Cancel", NamedTextColor.RED).clickEvent(ClickEvent.suggestCommand("cancel")))); + } + + private void toggleUpdate(final @NotNull Player player, final @NotNull Menu menu, final int slot) { + final boolean current = Boolean.parseBoolean(configEditor.getItemString(menu, slot, "update").orElse("false")); + try { + configEditor.setItemValue(menu, slot, "update", String.valueOf(!current)); + configEditor.reload(menu); + } catch (final IOException exception) { + plugin.printStacktrace("Failed to save menu edit.", exception); + plugin.sms(player, text("Failed to save menu edit.", NamedTextColor.RED)); + return; + } + + findMenu(menu.options().name()).ifPresent(reloaded -> editorManager.openSlot(player, reloaded, slot)); } - private @NotNull Component action(final @NotNull String menuName, final int slot, final @NotNull String option) { - final String command = "/dm edit " + menuName + " prompt " + slot + " " + option; - return text(command, NamedTextColor.YELLOW) - .clickEvent(ClickEvent.runCommand(command)); + private void deleteItem(final @NotNull Player player, final @NotNull Menu menu, final int slot) { + try { + if (!configEditor.deleteItem(menu, slot)) { + plugin.sms(player, text("No item config was found for slot " + slot + ".", NamedTextColor.RED)); + return; + } + configEditor.reload(menu); + } catch (final IOException exception) { + plugin.printStacktrace("Failed to delete menu item.", exception); + plugin.sms(player, text("Failed to delete menu item.", NamedTextColor.RED)); + return; + } + + findMenu(menu.options().name()).ifPresent(reloaded -> editorManager.open(player, reloaded)); + } + + private @NotNull Optional findMenu(final @NotNull String menuName) { + final Optional menu = Menu.getMenuByName(menuName); + if (menu.isPresent()) { + return menu; + } + + return Menu.getSubMenuByName(menuName); } } diff --git a/src/main/java/com/extendedclip/deluxemenus/editor/MenuEditorManager.java b/src/main/java/com/extendedclip/deluxemenus/editor/MenuEditorManager.java index 40a8fda1..9ff5077c 100644 --- a/src/main/java/com/extendedclip/deluxemenus/editor/MenuEditorManager.java +++ b/src/main/java/com/extendedclip/deluxemenus/editor/MenuEditorManager.java @@ -4,34 +4,56 @@ import com.extendedclip.deluxemenus.menu.Menu; import com.extendedclip.deluxemenus.menu.MenuHolder; import com.extendedclip.deluxemenus.menu.MenuItem; +import com.extendedclip.deluxemenus.utils.StringUtils; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.TreeMap; import org.bukkit.Bukkit; +import org.bukkit.Material; import org.bukkit.entity.Player; import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemFlag; import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; public class MenuEditorManager { + public static final int BUTTON_MATERIAL = 19; + public static final int BUTTON_AMOUNT = 20; + public static final int BUTTON_DISPLAY_NAME = 21; + public static final int BUTTON_LORE = 22; + public static final int BUTTON_MODEL_DATA = 23; + public static final int BUTTON_ITEM_FLAGS = 24; + public static final int BUTTON_UPDATE = 25; + public static final int BUTTON_CLICK_COMMANDS = 28; + public static final int BUTTON_LEFT_CLICK_COMMANDS = 29; + public static final int BUTTON_RIGHT_CLICK_COMMANDS = 30; + public static final int BUTTON_SHIFT_LEFT_CLICK_COMMANDS = 31; + public static final int BUTTON_SHIFT_RIGHT_CLICK_COMMANDS = 32; + public static final int BUTTON_MIDDLE_CLICK_COMMANDS = 33; + public static final int BUTTON_PRIORITY = 37; + public static final int BUTTON_DELETE = 40; + public static final int BUTTON_BACK = 45; + public static final int BUTTON_REFRESH = 49; + private final DeluxeMenus plugin; + private final MenuConfigEditor configEditor; public MenuEditorManager(final @NotNull DeluxeMenus plugin) { this.plugin = plugin; + this.configEditor = new MenuConfigEditor(plugin); } public void open(final @NotNull Player player, final @NotNull Menu menu) { final int size = menu.options().size(); final MenuEditorHolder editorHolder = new MenuEditorHolder(menu.options().name()); - final Inventory inventory = Bukkit.createInventory(editorHolder, size, "Editing: " + menu.options().name()); + final Inventory inventory = Bukkit.createInventory(editorHolder, size, title("Edit: " + menu.options().name())); editorHolder.setInventory(inventory); - final MenuHolder renderHolder = new MenuHolder(plugin, player); - renderHolder.setMenuName(menu.options().name()); - renderHolder.setPlaceholderPlayer(player); - renderHolder.parsePlaceholdersInArguments(menu.options().parsePlaceholdersInArguments()); - renderHolder.parsePlaceholdersAfterArguments(menu.options().parsePlaceholdersAfterArguments()); - + final MenuHolder renderHolder = renderHolder(player, menu); for (final Map.Entry> entry : menu.getMenuItems().entrySet()) { final int slot = entry.getKey(); if (slot < 0 || slot >= size || entry.getValue().isEmpty()) { @@ -46,4 +68,121 @@ public void open(final @NotNull Player player, final @NotNull Menu menu) { player.openInventory(inventory); } + + public void openSlot(final @NotNull Player player, final @NotNull Menu menu, final int slot) { + final MenuEditorHolder editorHolder = new MenuEditorHolder(menu.options().name(), MenuEditorHolder.View.SLOT, slot); + final Inventory inventory = Bukkit.createInventory(editorHolder, 54, title("Slot " + slot + ": " + menu.options().name())); + editorHolder.setInventory(inventory); + + inventory.setItem(4, previewItem(player, menu, slot)); + inventory.setItem(13, button(Material.PAPER, "&eSlot " + slot, + "&7Menu: &f" + menu.options().name(), + "&7File: &f" + menu.path(), + "&7Loaded item: &f" + (hasItem(menu, slot) ? "yes" : "no"))); + + inventory.setItem(BUTTON_MATERIAL, promptButton(Material.STONE, "Material", menu, slot, "material")); + inventory.setItem(BUTTON_AMOUNT, promptButton(Material.EMERALD, "Amount", menu, slot, "amount")); + inventory.setItem(BUTTON_DISPLAY_NAME, promptButton(Material.NAME_TAG, "Display Name", menu, slot, "display_name")); + inventory.setItem(BUTTON_LORE, promptButton(Material.BOOK, "Lore", menu, slot, "lore")); + inventory.setItem(BUTTON_MODEL_DATA, promptButton(Material.ITEM_FRAME, "Model Data", menu, slot, "model_data")); + inventory.setItem(BUTTON_ITEM_FLAGS, promptButton(Material.HOPPER, "Item Flags", menu, slot, "item_flags")); + inventory.setItem(BUTTON_UPDATE, button(Material.CLOCK, "&eToggle Update", + "&7Current: &f" + value(menu, slot, "update", "false"), + "&7Click to toggle placeholder refresh.")); + inventory.setItem(BUTTON_CLICK_COMMANDS, promptButton(Material.COMMAND_BLOCK, "Click Commands", menu, slot, "click_commands")); + inventory.setItem(BUTTON_LEFT_CLICK_COMMANDS, promptButton(Material.COMMAND_BLOCK, "Left Click Commands", menu, slot, "left_click_commands")); + inventory.setItem(BUTTON_RIGHT_CLICK_COMMANDS, promptButton(Material.COMMAND_BLOCK, "Right Click Commands", menu, slot, "right_click_commands")); + inventory.setItem(BUTTON_SHIFT_LEFT_CLICK_COMMANDS, promptButton(Material.COMMAND_BLOCK, "Shift Left Commands", menu, slot, "shift_left_click_commands")); + inventory.setItem(BUTTON_SHIFT_RIGHT_CLICK_COMMANDS, promptButton(Material.COMMAND_BLOCK, "Shift Right Commands", menu, slot, "shift_right_click_commands")); + inventory.setItem(BUTTON_MIDDLE_CLICK_COMMANDS, promptButton(Material.COMMAND_BLOCK, "Middle Click Commands", menu, slot, "middle_click_commands")); + inventory.setItem(BUTTON_PRIORITY, promptButton(Material.COMPARATOR, "Priority", menu, slot, "priority")); + inventory.setItem(BUTTON_DELETE, button(Material.BARRIER, "&cDelete Item", "&7Removes the item config for this slot.")); + inventory.setItem(BUTTON_BACK, button(Material.ARROW, "&aBack", "&7Return to the menu preview.")); + inventory.setItem(BUTTON_REFRESH, button(Material.CHEST, "&bReload Preview", "&7Reload the menu editor view.")); + + player.openInventory(inventory); + } + + private @NotNull MenuHolder renderHolder(final @NotNull Player player, final @NotNull Menu menu) { + final MenuHolder renderHolder = new MenuHolder(plugin, player); + renderHolder.setMenuName(menu.options().name()); + renderHolder.setPlaceholderPlayer(player); + renderHolder.parsePlaceholdersInArguments(menu.options().parsePlaceholdersInArguments()); + renderHolder.parsePlaceholdersAfterArguments(menu.options().parsePlaceholdersAfterArguments()); + return renderHolder; + } + + private @NotNull ItemStack previewItem(final @NotNull Player player, final @NotNull Menu menu, final int slot) { + final TreeMap items = menu.getMenuItems().get(slot); + if (items != null && !items.isEmpty()) { + final ItemStack itemStack = items.firstEntry().getValue().getItemStack(renderHolder(player, menu)); + if (itemStack != null) { + return itemStack; + } + } + + return button(Material.GRAY_STAINED_GLASS_PANE, "&7Empty Slot", "&7Set a material to create an item."); + } + + private @NotNull ItemStack promptButton( + final @NotNull Material material, + final @NotNull String label, + final @NotNull Menu menu, + final int slot, + final @NotNull String option + ) { + return button(material, "&e" + label, + "&7Current:", + "&f" + compact(value(menu, slot, option, "")), + "&7Click to edit."); + } + + private @NotNull ItemStack button(final @NotNull Material material, final @NotNull String name, final @NotNull String... lore) { + final ItemStack itemStack = new ItemStack(material); + final ItemMeta meta = itemStack.getItemMeta(); + if (meta == null) { + return itemStack; + } + + meta.setDisplayName(StringUtils.color(name)); + final List coloredLore = new ArrayList<>(); + for (final String line : lore) { + coloredLore.add(StringUtils.color(line)); + } + meta.setLore(coloredLore); + meta.addItemFlags(ItemFlag.HIDE_ATTRIBUTES); + itemStack.setItemMeta(meta); + return itemStack; + } + + private @NotNull String value(final @NotNull Menu menu, final int slot, final @NotNull String option, final @NotNull String fallback) { + return configEditor.getItemString(menu, slot, option).orElse(fallback); + } + + private boolean hasItem(final @NotNull Menu menu, final int slot) { + final TreeMap items = menu.getMenuItems().get(slot); + return items != null && !items.isEmpty(); + } + + private @NotNull String compact(final @Nullable String value) { + if (value == null || value.isBlank()) { + return "-"; + } + + final String oneLine = value.replace("\r", "").replace("\n", " | "); + if (oneLine.length() <= 34) { + return oneLine; + } + + return oneLine.substring(0, 31) + "..."; + } + + private @NotNull String title(final @NotNull String title) { + final String colored = StringUtils.color("&8" + title); + if (colored.length() <= 32) { + return colored; + } + + return colored.substring(0, 32); + } } diff --git a/src/main/java/com/extendedclip/deluxemenus/editor/WebEditorServer.java b/src/main/java/com/extendedclip/deluxemenus/editor/WebEditorServer.java index e3ceb142..1e90f4ff 100644 --- a/src/main/java/com/extendedclip/deluxemenus/editor/WebEditorServer.java +++ b/src/main/java/com/extendedclip/deluxemenus/editor/WebEditorServer.java @@ -3,15 +3,19 @@ import com.extendedclip.deluxemenus.DeluxeMenus; import com.extendedclip.deluxemenus.menu.Menu; import com.extendedclip.deluxemenus.menu.MenuItem; +import com.sun.net.httpserver.Headers; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpServer; +import java.io.File; import java.io.IOException; import java.io.OutputStream; +import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.time.Instant; +import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.UUID; @@ -35,12 +39,9 @@ public WebEditorServer(final @NotNull DeluxeMenus plugin) { ensureStarted(requestedPort); final String token = UUID.randomUUID().toString().replace("-", ""); - sessions.put(token, new Session(menu.options().name(), Instant.now().plus(Duration.ofMinutes(15)))); + sessions.put(token, new Session(token, menu.options().name(), Instant.now().plus(Duration.ofMinutes(60)))); - final String host = plugin.getServer().getIp() == null || plugin.getServer().getIp().isBlank() - ? "localhost" - : plugin.getServer().getIp(); - return "http://" + host + ":" + server.getAddress().getPort() + "/dm-web/" + token; + return "http://" + publicHost() + ":" + server.getAddress().getPort() + "/dm-web/" + token; } public void stop() { @@ -70,97 +71,150 @@ private void handle(final @NotNull HttpExchange exchange) throws IOException { return; } - final String token = parts[2]; - final Optional optionalSession = getSession(token); + final Optional optionalSession = getSession(parts[2]); if (optionalSession.isEmpty()) { send(exchange, 403, "Invalid or expired editor session.", "text/plain"); return; } - if (parts.length >= 4 && "save".equals(parts[3]) && "POST".equalsIgnoreCase(exchange.getRequestMethod())) { - save(exchange, optionalSession.get()); + final Session session = optionalSession.get(); + final String action = parts.length >= 4 ? parts[3] : ""; + final String method = exchange.getRequestMethod(); + + if ("save-menu".equals(action) && "POST".equalsIgnoreCase(method)) { + saveMenu(exchange, session); + return; + } + + if ("save-item".equals(action) && "POST".equalsIgnoreCase(method)) { + saveItem(exchange, session); + return; + } + + if ("delete-item".equals(action) && "POST".equalsIgnoreCase(method)) { + deleteItem(exchange, session); return; } - if (parts.length >= 4 && "save-item".equals(parts[3]) && "POST".equalsIgnoreCase(exchange.getRequestMethod())) { - saveItem(exchange, optionalSession.get()); + if ("save-raw".equals(action) && "POST".equalsIgnoreCase(method)) { + saveRaw(exchange, session); return; } - if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) { + if (!"GET".equalsIgnoreCase(method)) { send(exchange, 405, "Method not allowed", "text/plain"); return; } - render(exchange, optionalSession.get()); + render(exchange, session); } private void render(final @NotNull HttpExchange exchange, final @NotNull Session session) throws IOException { - final Optional optionalMenu = Menu.getMenuByName(session.menuName); + final Optional optionalMenu = findMenu(session.menuName); if (optionalMenu.isEmpty()) { send(exchange, 404, "Menu is not loaded.", "text/plain"); return; } final Menu menu = optionalMenu.get(); - final String html = "DeluxeMenus Editor" - + "" - + "
DeluxeMenus Web Editor - " + escape(menu.options().name()) + "
" - + "

Preview

" + renderGrid(menu) + "
" - + renderItemForms(menu, session) - + "

Raw YAML

" - + "
" - + "
" - + "
"; + final Map query = parseForm(exchange.getRequestURI().getRawQuery()); + final int selectedSlot = clamp(parseInt(query.get("slot")), 0, Math.max(0, menu.options().size() - 1)); + final String saved = query.getOrDefault("saved", ""); + final String html = "" + + "" + + "DeluxeMenus Editor" + + style() + + "" + + "
DeluxeMenus Editor" + escape(menu.options().name()) + "
" + + "Refresh
" + + "
" + + "

" + escape(menu.options().name()) + "

" + + menu.options().size() + " slots
" + + "
" + renderGrid(menu, session, selectedSlot) + "
" + + status(saved) + + "
" + + "
" + + renderMenuForm(menu, session, selectedSlot) + + renderItemForm(menu, session, selectedSlot) + + renderRawForm(menu, session, selectedSlot) + + "
" + + "
" + + script() + + ""; send(exchange, 200, html, "text/html; charset=utf-8"); } - private void save(final @NotNull HttpExchange exchange, final @NotNull Session session) throws IOException { - final Optional optionalMenu = Menu.getMenuByName(session.menuName); + private void saveMenu(final @NotNull HttpExchange exchange, final @NotNull Session session) throws IOException { + final Optional optionalMenu = findMenu(session.menuName); + if (optionalMenu.isEmpty()) { + send(exchange, 404, "Menu is not loaded.", "text/plain"); + return; + } + + final Map form = parseForm(readBody(exchange)); + final Menu menu = optionalMenu.get(); + configEditor.setMenuValue(menu, "menu_title", form.getOrDefault("menu_title", menu.options().title())); + configEditor.setMenuValue(menu, "size", form.getOrDefault("size", String.valueOf(menu.options().size()))); + reload(menu); + redirect(exchange, session, parseInt(form.get("slot")), "menu"); + } + + private void saveRaw(final @NotNull HttpExchange exchange, final @NotNull Session session) throws IOException { + final Optional optionalMenu = findMenu(session.menuName); if (optionalMenu.isEmpty()) { send(exchange, 404, "Menu is not loaded.", "text/plain"); return; } - final String body = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); - final String content = parseFormValue(body, "content"); - configEditor.saveRaw(optionalMenu.get(), content); - final String menuName = optionalMenu.get().options().name(); - plugin.getScheduler().runTask(() -> { - Menu.unload(plugin, menuName); - plugin.getConfiguration().loadGUIMenu(menuName); - }); - send(exchange, 200, "Saved. You can go back and refresh the editor.", "text/plain"); + final Map form = parseForm(readBody(exchange)); + final Menu menu = optionalMenu.get(); + configEditor.saveRaw(menu, form.getOrDefault("content", "")); + reload(menu); + redirect(exchange, session, parseInt(form.get("slot")), "raw"); } private void saveItem(final @NotNull HttpExchange exchange, final @NotNull Session session) throws IOException { - final Optional optionalMenu = Menu.getMenuByName(session.menuName); + final Optional optionalMenu = findMenu(session.menuName); if (optionalMenu.isEmpty()) { send(exchange, 404, "Menu is not loaded.", "text/plain"); return; } - final Map form = parseForm(new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8)); + final Map form = parseForm(readBody(exchange)); final int slot = parseInt(form.get("slot")); final Menu menu = optionalMenu.get(); - configEditor.setItemValue(menu, slot, "material", form.getOrDefault("material", "STONE")); + final String material = form.getOrDefault("material", "").isBlank() ? "STONE" : form.get("material"); + configEditor.setItemValue(menu, slot, "material", material); + configEditor.setItemValue(menu, slot, "amount", form.getOrDefault("amount", "-1")); + configEditor.setItemValue(menu, slot, "priority", form.getOrDefault("priority", "1")); configEditor.setItemValue(menu, slot, "display_name", form.getOrDefault("display_name", "")); configEditor.setItemValue(menu, slot, "lore", form.getOrDefault("lore", "")); + configEditor.setItemValue(menu, slot, "model_data", form.getOrDefault("model_data", "")); + configEditor.setItemValue(menu, slot, "item_flags", form.getOrDefault("item_flags", "")); + configEditor.setItemValue(menu, slot, "update", form.containsKey("update") ? "true" : "false"); + configEditor.setItemValue(menu, slot, "click_commands", form.getOrDefault("click_commands", "")); configEditor.setItemValue(menu, slot, "left_click_commands", form.getOrDefault("left_click_commands", "")); configEditor.setItemValue(menu, slot, "right_click_commands", form.getOrDefault("right_click_commands", "")); - configEditor.setItemValue(menu, slot, "click_commands", form.getOrDefault("click_commands", "")); + configEditor.setItemValue(menu, slot, "shift_left_click_commands", form.getOrDefault("shift_left_click_commands", "")); + configEditor.setItemValue(menu, slot, "shift_right_click_commands", form.getOrDefault("shift_right_click_commands", "")); + configEditor.setItemValue(menu, slot, "middle_click_commands", form.getOrDefault("middle_click_commands", "")); + reload(menu); + redirect(exchange, session, slot, "slot"); + } + + private void deleteItem(final @NotNull HttpExchange exchange, final @NotNull Session session) throws IOException { + final Optional optionalMenu = findMenu(session.menuName); + if (optionalMenu.isEmpty()) { + send(exchange, 404, "Menu is not loaded.", "text/plain"); + return; + } - final String menuName = menu.options().name(); - plugin.getScheduler().runTask(() -> { - Menu.unload(plugin, menuName); - plugin.getConfiguration().loadGUIMenu(menuName); - }); - send(exchange, 303, "", "text/plain", "/dm-web/" + session.token()); + final Map form = parseForm(readBody(exchange)); + final int slot = parseInt(form.get("slot")); + final Menu menu = optionalMenu.get(); + configEditor.deleteItem(menu, slot); + reload(menu); + redirect(exchange, session, slot, "delete"); } private @NotNull Optional getSession(final @NotNull String token) { @@ -169,63 +223,146 @@ private void saveItem(final @NotNull HttpExchange exchange, final @NotNull Sessi sessions.remove(token); return Optional.empty(); } - session.token = token; + return Optional.of(session); } - private @NotNull String renderGrid(final @NotNull Menu menu) { + private @NotNull Optional findMenu(final @NotNull String menuName) { + final Optional menu = Menu.getMenuByName(menuName); + if (menu.isPresent()) { + return menu; + } + + return Menu.getSubMenuByName(menuName); + } + + private @NotNull String renderGrid(final @NotNull Menu menu, final @NotNull Session session, final int selectedSlot) { final StringBuilder builder = new StringBuilder(); for (int slot = 0; slot < menu.options().size(); slot++) { - final Map items = menu.getMenuItems().get(slot); - final boolean filled = items != null && !items.isEmpty(); - final String label = filled ? items.values().iterator().next().options().material() : String.valueOf(slot); - builder.append("
") + .append(escape(slotLabel(menu, slot))) + .append("\">") .append(slot) - .append("
"); + .append("") + .append(escape(compact(slotLabel(menu, slot), 13))) + .append(""); } return builder.toString(); } - private @NotNull String renderItemForms(final @NotNull Menu menu, final @NotNull Session session) { - final StringBuilder builder = new StringBuilder("

Slot editor

"); - for (int slot = 0; slot < menu.options().size(); slot++) { - final Map items = menu.getMenuItems().get(slot); - if (items == null || items.isEmpty()) { - continue; - } - builder.append(renderItemForm(menu, session, slot)); + private @NotNull String renderMenuForm(final @NotNull Menu menu, final @NotNull Session session, final int selectedSlot) { + final String source = configEditor.resolveFile(menu).map(File::getPath).orElse("config.yml"); + return "
" + + "" + + "

Menu

" + escape(source) + "
" + + input("Title", "menu_title", configEditor.getMenuString(menu, "menu_title").orElse(menu.options().title())) + + "" + + "" + + "
"; + } + + private @NotNull String renderItemForm(final @NotNull Menu menu, final @NotNull Session session, final int selectedSlot) { + return "
" + + "
" + + "" + + "

Slot " + selectedSlot + "

" + escape(slotLabel(menu, selectedSlot)) + "
" + + "
" + + input("Material", "material", value(menu, selectedSlot, "material", "")) + + input("Amount", "amount", value(menu, selectedSlot, "amount", "-1")) + + input("Priority", "priority", value(menu, selectedSlot, "priority", "1")) + + input("Model Data", "model_data", value(menu, selectedSlot, "model_data", "")) + + "
" + + input("Display Name", "display_name", value(menu, selectedSlot, "display_name", "")) + + textarea("Lore", "lore", value(menu, selectedSlot, "lore", "")) + + textarea("Item Flags", "item_flags", value(menu, selectedSlot, "item_flags", "")) + + checkbox("Update Placeholders", "update", Boolean.parseBoolean(value(menu, selectedSlot, "update", "false"))) + + "
" + + textarea("Click Commands", "click_commands", value(menu, selectedSlot, "click_commands", "")) + + textarea("Left Click Commands", "left_click_commands", value(menu, selectedSlot, "left_click_commands", "")) + + textarea("Right Click Commands", "right_click_commands", value(menu, selectedSlot, "right_click_commands", "")) + + textarea("Shift Left Commands", "shift_left_click_commands", value(menu, selectedSlot, "shift_left_click_commands", "")) + + textarea("Shift Right Commands", "shift_right_click_commands", value(menu, selectedSlot, "shift_right_click_commands", "")) + + textarea("Middle Click Commands", "middle_click_commands", value(menu, selectedSlot, "middle_click_commands", "")) + + "
" + + "
" + + "
" + + "" + + "
"; + } + + private @NotNull String renderRawForm(final @NotNull Menu menu, final @NotNull Session session, final int selectedSlot) throws IOException { + return "
Raw YAML" + + "
" + + "" + + "" + + "
"; + } + + private @NotNull String input(final @NotNull String label, final @NotNull String name, final @NotNull String value) { + return ""; + } + + private @NotNull String textarea(final @NotNull String label, final @NotNull String name, final @NotNull String value) { + return ""; + } + + private @NotNull String checkbox(final @NotNull String label, final @NotNull String name, final boolean checked) { + return ""; + } + + private @NotNull String status(final @NotNull String saved) { + if (saved.isBlank()) { + return ""; } - builder.append(renderItemForm(menu, session, 0)); - builder.append("
"); - return builder.toString(); + + return "
Saved " + escape(saved) + ".
"; + } + + private @NotNull String value(final @NotNull Menu menu, final int slot, final @NotNull String option, final @NotNull String fallback) { + return configEditor.getItemString(menu, slot, option).orElse(fallback); + } + + private boolean isFilled(final @NotNull Menu menu, final int slot) { + final Map items = menu.getMenuItems().get(slot); + return items != null && !items.isEmpty(); + } + + private @NotNull String slotLabel(final @NotNull Menu menu, final int slot) { + final Map items = menu.getMenuItems().get(slot); + if (items == null || items.isEmpty()) { + return "Empty"; + } + + return items.values().iterator().next().options().material(); } - private @NotNull String renderItemForm(final @NotNull Menu menu, final @NotNull Session session, final int defaultSlot) { - return "
" - + "" - + "" - + "" - + "" - + "" - + "" - + "" - + "

"; + private void reload(final @NotNull Menu menu) { + plugin.getScheduler().runTask(() -> configEditor.reload(menu)); } - private @NotNull String value(final @NotNull Menu menu, final int slot, final @NotNull String option) { - return configEditor.getItemString(menu, slot, option).orElse(""); + private void redirect(final @NotNull HttpExchange exchange, final @NotNull Session session, final int slot, final @NotNull String saved) throws IOException { + send(exchange, 303, "", "text/plain", "/dm-web/" + session.token + "?slot=" + Math.max(0, slot) + "&saved=" + saved); } - private @NotNull String parseFormValue(final @NotNull String body, final @NotNull String key) { - return parseForm(body).getOrDefault(key, ""); + private @NotNull String readBody(final @NotNull HttpExchange exchange) throws IOException { + return new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); } - private @NotNull Map parseForm(final @NotNull String body) { - final Map form = new ConcurrentHashMap<>(); + private @NotNull Map parseForm(final @Nullable String body) { + final Map form = new HashMap<>(); + if (body == null || body.isBlank()) { + return form; + } + for (final String part : body.split("&")) { final int index = part.indexOf('='); if (index <= 0) { @@ -245,6 +382,35 @@ private int parseInt(final @Nullable String input) { } } + private int clamp(final int value, final int min, final int max) { + return Math.max(min, Math.min(max, value)); + } + + private @NotNull String compact(final @NotNull String value, final int limit) { + if (value.length() <= limit) { + return value; + } + + return value.substring(0, Math.max(0, limit - 1)) + "..."; + } + + private @NotNull String publicHost() { + final String configuredHost = plugin.getServer().getIp(); + if (configuredHost != null && !configuredHost.isBlank() && !"0.0.0.0".equals(configuredHost)) { + return configuredHost; + } + + try { + final InetAddress localHost = InetAddress.getLocalHost(); + if (!localHost.isLoopbackAddress()) { + return localHost.getHostAddress(); + } + } catch (final Exception ignored) { + } + + return "localhost"; + } + private @NotNull String escape(final @NotNull String input) { return input.replace("&", "&") .replace("<", "<") @@ -256,11 +422,20 @@ private void send(final @NotNull HttpExchange exchange, final int status, final send(exchange, status, body, contentType, null); } - private void send(final @NotNull HttpExchange exchange, final int status, final @NotNull String body, final @NotNull String contentType, final @Nullable String location) throws IOException { + private void send( + final @NotNull HttpExchange exchange, + final int status, + final @NotNull String body, + final @NotNull String contentType, + final @Nullable String location + ) throws IOException { final byte[] bytes = body.getBytes(StandardCharsets.UTF_8); - exchange.getResponseHeaders().set("Content-Type", contentType); + final Headers headers = exchange.getResponseHeaders(); + headers.set("Content-Type", contentType); + headers.set("Cache-Control", "no-store"); + headers.set("X-Content-Type-Options", "nosniff"); if (location != null) { - exchange.getResponseHeaders().set("Location", location); + headers.set("Location", location); } exchange.sendResponseHeaders(status, bytes.length); try (OutputStream output = exchange.getResponseBody()) { @@ -268,18 +443,45 @@ private void send(final @NotNull HttpExchange exchange, final int status, final } } + private @NotNull String style() { + return ""; + } + + private @NotNull String script() { + return ""; + } + private static class Session { + private final String token; private final String menuName; private final Instant expiresAt; - private String token; - private Session(final @NotNull String menuName, final @NotNull Instant expiresAt) { + private Session(final @NotNull String token, final @NotNull String menuName, final @NotNull Instant expiresAt) { + this.token = token; this.menuName = menuName; this.expiresAt = expiresAt; } - - private @NotNull String token() { - return token; - } } } From e7d3e572c571c338446790c288242b510c811e26 Mon Sep 17 00:00:00 2001 From: unknown <1tranducnhan.2021@gmail.com> Date: Thu, 21 May 2026 21:04:08 +0700 Subject: [PATCH 07/12] Use localhost for local web editor links --- .../deluxemenus/command/subcommand/WebEditorCommand.java | 3 +++ .../extendedclip/deluxemenus/editor/WebEditorServer.java | 9 --------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/extendedclip/deluxemenus/command/subcommand/WebEditorCommand.java b/src/main/java/com/extendedclip/deluxemenus/command/subcommand/WebEditorCommand.java index 05a1c28e..23140e2d 100644 --- a/src/main/java/com/extendedclip/deluxemenus/command/subcommand/WebEditorCommand.java +++ b/src/main/java/com/extendedclip/deluxemenus/command/subcommand/WebEditorCommand.java @@ -59,6 +59,9 @@ public void execute(final @NotNull CommandSender sender, final @NotNull List Date: Thu, 21 May 2026 21:06:21 +0700 Subject: [PATCH 08/12] Use player host for web editor links --- .../command/subcommand/WebEditorCommand.java | 64 +++++++++++++++++-- .../deluxemenus/editor/WebEditorServer.java | 48 ++++++++++++-- 2 files changed, 102 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/extendedclip/deluxemenus/command/subcommand/WebEditorCommand.java b/src/main/java/com/extendedclip/deluxemenus/command/subcommand/WebEditorCommand.java index 23140e2d..e6dae7d2 100644 --- a/src/main/java/com/extendedclip/deluxemenus/command/subcommand/WebEditorCommand.java +++ b/src/main/java/com/extendedclip/deluxemenus/command/subcommand/WebEditorCommand.java @@ -4,6 +4,8 @@ import com.extendedclip.deluxemenus.menu.Menu; import com.extendedclip.deluxemenus.utils.Messages; import java.io.IOException; +import java.lang.reflect.Method; +import java.net.InetSocketAddress; import java.util.Collection; import java.util.List; import java.util.Locale; @@ -12,6 +14,7 @@ import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -20,6 +23,7 @@ public class WebEditorCommand extends SubCommand { private static final String WEB_EDITOR_PERMISSION = "deluxemenus.webeditor"; + private static final int DEFAULT_PORT = 8765; private final String name; public WebEditorCommand(final @NotNull DeluxeMenus plugin) { @@ -54,13 +58,15 @@ public void execute(final @NotNull CommandSender sender, final @NotNull List= 2 ? parsePort(arguments.get(1)) : 8765; + final EditorEndpoint endpoint = parseEndpoint(sender, arguments); try { - final String url = plugin.getWebEditorServer().createSession(optionalMenu.get(), port); + final String url = plugin.getWebEditorServer().createSession(optionalMenu.get(), endpoint.port, endpoint.host.orElse(null)); plugin.sms(sender, text("Web editor link: ", NamedTextColor.GREEN) .append(text(url, NamedTextColor.YELLOW).clickEvent(ClickEvent.openUrl(url)))); if (url.contains("://localhost:")) { - plugin.sms(sender, text("Remote hosting needs the web editor port open and the public server IP or domain in the URL.", NamedTextColor.GRAY)); + plugin.sms(sender, text("Remote hosting needs a public host and an open web editor port.", NamedTextColor.GRAY)); + } else { + plugin.sms(sender, text("The web editor port must be open on your hosting panel. The Minecraft port is separate.", NamedTextColor.GRAY)); } } catch (final IOException exception) { plugin.printStacktrace("Failed to start web editor.", exception); @@ -93,11 +99,47 @@ public void execute(final @NotNull CommandSender sender, final @NotNull List arguments) { + int port = DEFAULT_PORT; + Optional host = virtualHost(sender); + + for (int index = 1; index < arguments.size(); index++) { + final String argument = arguments.get(index); + final Optional optionalPort = parsePort(argument); + if (optionalPort.isPresent()) { + port = optionalPort.get(); + continue; + } + + host = Optional.of(argument); + } + + return new EditorEndpoint(port, host); + } + + private @NotNull Optional parsePort(final @NotNull String input) { try { - return Integer.parseInt(input); + return Optional.of(Integer.parseInt(input)); } catch (final NumberFormatException exception) { - return 8765; + return Optional.empty(); + } + } + + private @NotNull Optional virtualHost(final @NotNull CommandSender sender) { + if (!(sender instanceof Player)) { + return Optional.empty(); + } + + try { + final Method method = sender.getClass().getMethod("getVirtualHost"); + final Object virtualHost = method.invoke(sender); + if (virtualHost instanceof InetSocketAddress) { + return Optional.ofNullable(((InetSocketAddress) virtualHost).getHostString()); + } + + return Optional.ofNullable(virtualHost).map(String::valueOf); + } catch (final ReflectiveOperationException exception) { + return Optional.empty(); } } @@ -124,4 +166,14 @@ private int parsePort(final @NotNull String input) { ) .collect(Collectors.toList()); } + + private static class EditorEndpoint { + private final int port; + private final Optional host; + + private EditorEndpoint(final int port, final @NotNull Optional host) { + this.port = port; + this.host = host; + } + } } diff --git a/src/main/java/com/extendedclip/deluxemenus/editor/WebEditorServer.java b/src/main/java/com/extendedclip/deluxemenus/editor/WebEditorServer.java index c0a40aae..6f53c6b6 100644 --- a/src/main/java/com/extendedclip/deluxemenus/editor/WebEditorServer.java +++ b/src/main/java/com/extendedclip/deluxemenus/editor/WebEditorServer.java @@ -34,13 +34,17 @@ public WebEditorServer(final @NotNull DeluxeMenus plugin) { this.configEditor = new MenuConfigEditor(plugin); } - public @NotNull String createSession(final @NotNull Menu menu, final int requestedPort) throws IOException { + public @NotNull String createSession( + final @NotNull Menu menu, + final int requestedPort, + final @Nullable String requestedHost + ) throws IOException { ensureStarted(requestedPort); final String token = UUID.randomUUID().toString().replace("-", ""); sessions.put(token, new Session(token, menu.options().name(), Instant.now().plus(Duration.ofMinutes(60)))); - return "http://" + publicHost() + ":" + server.getAddress().getPort() + "/dm-web/" + token; + return "http://" + publicHost(requestedHost) + ":" + server.getAddress().getPort() + "/dm-web/" + token; } public void stop() { @@ -393,15 +397,51 @@ private int clamp(final int value, final int min, final int max) { return value.substring(0, Math.max(0, limit - 1)) + "..."; } - private @NotNull String publicHost() { + private @NotNull String publicHost(final @Nullable String requestedHost) { + final Optional normalizedRequestHost = normalizeHost(requestedHost); + if (normalizedRequestHost.isPresent()) { + return normalizedRequestHost.get(); + } + final String configuredHost = plugin.getServer().getIp(); if (configuredHost != null && !configuredHost.isBlank() && !"0.0.0.0".equals(configuredHost)) { - return configuredHost; + return normalizeHost(configuredHost).orElse(configuredHost); } return "localhost"; } + private @NotNull Optional normalizeHost(final @Nullable String host) { + if (host == null || host.isBlank()) { + return Optional.empty(); + } + + String normalized = host.trim(); + final int schemeIndex = normalized.indexOf("://"); + if (schemeIndex >= 0) { + normalized = normalized.substring(schemeIndex + 3); + } + + final int pathIndex = normalized.indexOf('/'); + if (pathIndex >= 0) { + normalized = normalized.substring(0, pathIndex); + } + + if (normalized.startsWith("[")) { + final int endIndex = normalized.indexOf(']'); + if (endIndex > 0) { + return Optional.of(normalized.substring(0, endIndex + 1)); + } + } + + final int firstColon = normalized.indexOf(':'); + if (firstColon >= 0 && firstColon == normalized.lastIndexOf(':')) { + normalized = normalized.substring(0, firstColon); + } + + return normalized.isBlank() ? Optional.empty() : Optional.of(normalized); + } + private @NotNull String escape(final @NotNull String input) { return input.replace("&", "&") .replace("<", "<") From c4ae7ca35498541d70de871ba7647fa6d04ea4c4 Mon Sep 17 00:00:00 2001 From: unknown <1tranducnhan.2021@gmail.com> Date: Thu, 21 May 2026 21:12:47 +0700 Subject: [PATCH 09/12] Allow web editor port changes --- .../command/subcommand/WebEditorCommand.java | 53 ++++++++++++++++++- .../deluxemenus/editor/WebEditorServer.java | 6 ++- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/extendedclip/deluxemenus/command/subcommand/WebEditorCommand.java b/src/main/java/com/extendedclip/deluxemenus/command/subcommand/WebEditorCommand.java index e6dae7d2..b2b71412 100644 --- a/src/main/java/com/extendedclip/deluxemenus/command/subcommand/WebEditorCommand.java +++ b/src/main/java/com/extendedclip/deluxemenus/command/subcommand/WebEditorCommand.java @@ -105,6 +105,13 @@ public void execute(final @NotNull CommandSender sender, final @NotNull List optionalEndpoint = parseHostPort(argument); + if (optionalEndpoint.isPresent()) { + port = optionalEndpoint.get().port; + host = optionalEndpoint.get().host; + continue; + } + final Optional optionalPort = parsePort(argument); if (optionalPort.isPresent()) { port = optionalPort.get(); @@ -119,12 +126,56 @@ public void execute(final @NotNull CommandSender sender, final @NotNull List parsePort(final @NotNull String input) { try { - return Optional.of(Integer.parseInt(input)); + final int port = Integer.parseInt(input); + if (port < 1 || port > 65535) { + return Optional.empty(); + } + + return Optional.of(port); } catch (final NumberFormatException exception) { return Optional.empty(); } } + private @NotNull Optional parseHostPort(final @NotNull String input) { + String endpoint = input.trim(); + final int schemeIndex = endpoint.indexOf("://"); + if (schemeIndex >= 0) { + endpoint = endpoint.substring(schemeIndex + 3); + } + + final int pathIndex = endpoint.indexOf('/'); + if (pathIndex >= 0) { + endpoint = endpoint.substring(0, pathIndex); + } + + if (endpoint.startsWith("[")) { + final int endIndex = endpoint.indexOf(']'); + if (endIndex <= 0 || endIndex + 2 > endpoint.length() || endpoint.charAt(endIndex + 1) != ':') { + return Optional.empty(); + } + + final Optional port = parsePort(endpoint.substring(endIndex + 2)); + if (port.isEmpty()) { + return Optional.empty(); + } + + return Optional.of(new EditorEndpoint(port.get(), Optional.of(endpoint.substring(0, endIndex + 1)))); + } + + final int colonIndex = endpoint.lastIndexOf(':'); + if (colonIndex <= 0 || colonIndex != endpoint.indexOf(':')) { + return Optional.empty(); + } + + final Optional port = parsePort(endpoint.substring(colonIndex + 1)); + if (port.isEmpty()) { + return Optional.empty(); + } + + return Optional.of(new EditorEndpoint(port.get(), Optional.of(endpoint.substring(0, colonIndex)))); + } + private @NotNull Optional virtualHost(final @NotNull CommandSender sender) { if (!(sender instanceof Player)) { return Optional.empty(); diff --git a/src/main/java/com/extendedclip/deluxemenus/editor/WebEditorServer.java b/src/main/java/com/extendedclip/deluxemenus/editor/WebEditorServer.java index 6f53c6b6..f9bcf95f 100644 --- a/src/main/java/com/extendedclip/deluxemenus/editor/WebEditorServer.java +++ b/src/main/java/com/extendedclip/deluxemenus/editor/WebEditorServer.java @@ -57,7 +57,11 @@ public void stop() { private void ensureStarted(final int requestedPort) throws IOException { if (server != null) { - return; + if (server.getAddress().getPort() == requestedPort) { + return; + } + + stop(); } server = HttpServer.create(new InetSocketAddress(requestedPort), 0); From 8fa32c0a63744edf287865b2ed7f25657d6127c7 Mon Sep 17 00:00:00 2001 From: unknown <1tranducnhan.2021@gmail.com> Date: Thu, 21 May 2026 21:21:20 +0700 Subject: [PATCH 10/12] Add web editor session controls --- .../command/DeluxeMenusCommand.java | 4 +- .../command/subcommand/WebEditorCommand.java | 100 ++++++++++++++++-- .../deluxemenus/editor/WebEditorServer.java | 88 ++++++++++++++- 3 files changed, 180 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/extendedclip/deluxemenus/command/DeluxeMenusCommand.java b/src/main/java/com/extendedclip/deluxemenus/command/DeluxeMenusCommand.java index 953fbb26..c6ee2db9 100644 --- a/src/main/java/com/extendedclip/deluxemenus/command/DeluxeMenusCommand.java +++ b/src/main/java/com/extendedclip/deluxemenus/command/DeluxeMenusCommand.java @@ -99,9 +99,7 @@ private void registerSubCommands() { new OpenCommand(plugin), new RefreshCommand(plugin), new ReloadCommand(plugin), - new WebEditorCommand(plugin), - new WebEditorCommand(plugin, "browsereditor"), - new WebEditorCommand(plugin, "browseeditor") + new WebEditorCommand(plugin) ); for (final SubCommand subCommand : commands) { diff --git a/src/main/java/com/extendedclip/deluxemenus/command/subcommand/WebEditorCommand.java b/src/main/java/com/extendedclip/deluxemenus/command/subcommand/WebEditorCommand.java index b2b71412..2b792582 100644 --- a/src/main/java/com/extendedclip/deluxemenus/command/subcommand/WebEditorCommand.java +++ b/src/main/java/com/extendedclip/deluxemenus/command/subcommand/WebEditorCommand.java @@ -1,11 +1,14 @@ package com.extendedclip.deluxemenus.command.subcommand; import com.extendedclip.deluxemenus.DeluxeMenus; +import com.extendedclip.deluxemenus.editor.WebEditorServer; import com.extendedclip.deluxemenus.menu.Menu; import com.extendedclip.deluxemenus.utils.Messages; import java.io.IOException; import java.lang.reflect.Method; import java.net.InetSocketAddress; +import java.time.Duration; +import java.time.Instant; import java.util.Collection; import java.util.List; import java.util.Locale; @@ -52,13 +55,35 @@ public void execute(final @NotNull CommandSender sender, final @NotNull List optionalMenu = findMenu(arguments.get(0)); + final String action = arguments.get(0).toLowerCase(Locale.ROOT); + if ("list".equals(action)) { + listSessions(sender); + return; + } + + if ("resume".equals(action)) { + resumeSession(sender, arguments); + return; + } + + if ("cancel".equals(action)) { + cancelSession(sender, arguments); + return; + } + + final int menuIndex = "local".equals(action) ? 1 : 0; + if (arguments.size() <= menuIndex) { + plugin.sms(sender, Messages.WRONG_USAGE); + return; + } + + final Optional optionalMenu = findMenu(arguments.get(menuIndex)); if (optionalMenu.isEmpty()) { - plugin.sms(sender, Messages.INVALID_MENU.message().replaceText(MENU_REPLACER_BUILDER.replacement(arguments.get(0)).build())); + plugin.sms(sender, Messages.INVALID_MENU.message().replaceText(MENU_REPLACER_BUILDER.replacement(arguments.get(menuIndex)).build())); return; } - final EditorEndpoint endpoint = parseEndpoint(sender, arguments); + final EditorEndpoint endpoint = parseEndpoint(sender, arguments, menuIndex + 1); try { final String url = plugin.getWebEditorServer().createSession(optionalMenu.get(), endpoint.port, endpoint.host.orElse(null)); plugin.sms(sender, text("Web editor link: ", NamedTextColor.GREEN) @@ -68,6 +93,10 @@ public void execute(final @NotNull CommandSender sender, final @NotNull List arguments) { + private void listSessions(final @NotNull CommandSender sender) { + final List sessions = plugin.getWebEditorServer().listSessions(); + if (sessions.isEmpty()) { + plugin.sms(sender, text("No active web editor sessions.", NamedTextColor.GRAY)); + return; + } + + plugin.sms(sender, text("Active web editor sessions:", NamedTextColor.GOLD)); + for (final WebEditorServer.SessionView session : sessions) { + final long minutes = Math.max(0, Duration.between(Instant.now(), session.expiresAt()).toMinutes()); + plugin.sms(sender, text("- " + session.menuName() + " (" + minutes + "m): ", NamedTextColor.GRAY) + .append(text(session.url(), NamedTextColor.YELLOW).clickEvent(ClickEvent.openUrl(session.url())))); + } + } + + private void resumeSession(final @NotNull CommandSender sender, final @NotNull List arguments) { + if (arguments.size() < 2) { + plugin.sms(sender, Messages.WRONG_USAGE); + return; + } + + final Optional url = plugin.getWebEditorServer().resumeSession(arguments.get(1)); + if (url.isEmpty()) { + plugin.sms(sender, text("No active web editor session for " + arguments.get(1) + ".", NamedTextColor.RED)); + return; + } + + plugin.sms(sender, text("Web editor link: ", NamedTextColor.GREEN) + .append(text(url.get(), NamedTextColor.YELLOW).clickEvent(ClickEvent.openUrl(url.get())))); + } + + private void cancelSession(final @NotNull CommandSender sender, final @NotNull List arguments) { + if (arguments.size() < 2) { + plugin.sms(sender, Messages.WRONG_USAGE); + return; + } + + if (!plugin.getWebEditorServer().cancelSession(arguments.get(1))) { + plugin.sms(sender, text("No active web editor session for " + arguments.get(1) + ".", NamedTextColor.RED)); + return; + } + + plugin.sms(sender, text("Cancelled web editor session for " + arguments.get(1) + ".", NamedTextColor.GREEN)); + } + + private @NotNull EditorEndpoint parseEndpoint( + final @NotNull CommandSender sender, + final @NotNull List arguments, + final int startIndex + ) { int port = DEFAULT_PORT; Optional host = virtualHost(sender); - for (int index = 1; index < arguments.size(); index++) { + for (int index = startIndex; index < arguments.size(); index++) { final String argument = arguments.get(index); final Optional optionalEndpoint = parseHostPort(argument); if (optionalEndpoint.isPresent()) { @@ -218,6 +301,11 @@ public void execute(final @NotNull CommandSender sender, final @NotNull List commandTargets() { + return java.util.stream.Stream.concat(menuNames().stream(), List.of("list", "resume", "cancel", "local").stream()) + .collect(Collectors.toList()); + } + private static class EditorEndpoint { private final int port; private final Optional host; diff --git a/src/main/java/com/extendedclip/deluxemenus/editor/WebEditorServer.java b/src/main/java/com/extendedclip/deluxemenus/editor/WebEditorServer.java index f9bcf95f..ae1b33e4 100644 --- a/src/main/java/com/extendedclip/deluxemenus/editor/WebEditorServer.java +++ b/src/main/java/com/extendedclip/deluxemenus/editor/WebEditorServer.java @@ -15,10 +15,12 @@ import java.time.Duration; import java.time.Instant; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -39,12 +41,38 @@ public WebEditorServer(final @NotNull DeluxeMenus plugin) { final int requestedPort, final @Nullable String requestedHost ) throws IOException { + cleanupSessions(); + final Optional activeSession = activeSession(menu.options().name()); + if (activeSession.isPresent()) { + throw new ActiveSessionException(activeSession.get().url); + } + ensureStarted(requestedPort); final String token = UUID.randomUUID().toString().replace("-", ""); - sessions.put(token, new Session(token, menu.options().name(), Instant.now().plus(Duration.ofMinutes(60)))); + final String url = "http://" + publicHost(requestedHost) + ":" + server.getAddress().getPort() + "/dm-web/" + token; + sessions.put(token, new Session(token, menu.options().name(), url, Instant.now().plus(Duration.ofMinutes(60)))); + + return url; + } + + public @NotNull Optional resumeSession(final @NotNull String menuName) { + cleanupSessions(); + return activeSession(menuName).map(session -> session.url); + } + + public boolean cancelSession(final @NotNull String menuName) { + cleanupSessions(); + final Optional activeSession = activeSession(menuName); + activeSession.ifPresent(session -> sessions.remove(session.token)); + return activeSession.isPresent(); + } - return "http://" + publicHost(requestedHost) + ":" + server.getAddress().getPort() + "/dm-web/" + token; + public @NotNull List listSessions() { + cleanupSessions(); + return sessions.values().stream() + .map(session -> new SessionView(session.menuName, session.url, session.expiresAt)) + .collect(Collectors.toList()); } public void stop() { @@ -234,6 +262,17 @@ private void deleteItem(final @NotNull HttpExchange exchange, final @NotNull Ses return Optional.of(session); } + private @NotNull Optional activeSession(final @NotNull String menuName) { + return sessions.values().stream() + .filter(session -> session.menuName.equalsIgnoreCase(menuName)) + .findFirst(); + } + + private void cleanupSessions() { + final Instant now = Instant.now(); + sessions.entrySet().removeIf(entry -> entry.getValue().expiresAt.isBefore(now)); + } + private @NotNull Optional findMenu(final @NotNull String menuName) { final Optional menu = Menu.getMenuByName(menuName); if (menu.isPresent()) { @@ -508,14 +547,57 @@ private void send( + ""; } + public static class ActiveSessionException extends IOException { + private final String url; + + private ActiveSessionException(final @NotNull String url) { + this.url = url; + } + + public @NotNull String url() { + return url; + } + } + + public static class SessionView { + private final String menuName; + private final String url; + private final Instant expiresAt; + + private SessionView(final @NotNull String menuName, final @NotNull String url, final @NotNull Instant expiresAt) { + this.menuName = menuName; + this.url = url; + this.expiresAt = expiresAt; + } + + public @NotNull String menuName() { + return menuName; + } + + public @NotNull String url() { + return url; + } + + public @NotNull Instant expiresAt() { + return expiresAt; + } + } + private static class Session { private final String token; private final String menuName; + private final String url; private final Instant expiresAt; - private Session(final @NotNull String token, final @NotNull String menuName, final @NotNull Instant expiresAt) { + private Session( + final @NotNull String token, + final @NotNull String menuName, + final @NotNull String url, + final @NotNull Instant expiresAt + ) { this.token = token; this.menuName = menuName; + this.url = url; this.expiresAt = expiresAt; } } From a4131093142701a463ce30e24a1b860d286b9f55 Mon Sep 17 00:00:00 2001 From: unknown <1tranducnhan.2021@gmail.com> Date: Thu, 21 May 2026 21:25:29 +0700 Subject: [PATCH 11/12] Require public host for web editor --- .../command/subcommand/WebEditorCommand.java | 37 ++++++++++++++++++- .../deluxemenus/config/DeluxeMenusConfig.java | 1 + .../deluxemenus/config/GeneralConfig.java | 7 ++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/extendedclip/deluxemenus/command/subcommand/WebEditorCommand.java b/src/main/java/com/extendedclip/deluxemenus/command/subcommand/WebEditorCommand.java index 2b792582..3e91eb58 100644 --- a/src/main/java/com/extendedclip/deluxemenus/command/subcommand/WebEditorCommand.java +++ b/src/main/java/com/extendedclip/deluxemenus/command/subcommand/WebEditorCommand.java @@ -71,7 +71,8 @@ public void execute(final @NotNull CommandSender sender, final @NotNull List.", NamedTextColor.GRAY)); + return; + } + try { final String url = plugin.getWebEditorServer().createSession(optionalMenu.get(), endpoint.port, endpoint.host.orElse(null)); plugin.sms(sender, text("Web editor link: ", NamedTextColor.GREEN) @@ -185,6 +192,11 @@ private void cancelSession(final @NotNull CommandSender sender, final @NotNull L ) { int port = DEFAULT_PORT; Optional host = virtualHost(sender); + final Optional configuredEndpoint = configuredEndpoint(); + if (configuredEndpoint.isPresent()) { + port = configuredEndpoint.get().port; + host = configuredEndpoint.get().host; + } for (int index = startIndex; index < arguments.size(); index++) { final String argument = arguments.get(index); @@ -259,6 +271,29 @@ private void cancelSession(final @NotNull CommandSender sender, final @NotNull L return Optional.of(new EditorEndpoint(port.get(), Optional.of(endpoint.substring(0, colonIndex)))); } + private @NotNull Optional configuredEndpoint() { + final String publicUrl = plugin.getGeneralConfig().webEditorPublicUrl(); + if (publicUrl == null || publicUrl.isBlank()) { + return Optional.empty(); + } + + final Optional endpoint = parseHostPort(publicUrl); + if (endpoint.isPresent()) { + return endpoint; + } + + return Optional.of(new EditorEndpoint(DEFAULT_PORT, Optional.of(publicUrl))); + } + + private boolean hasPublicHost(final @NotNull EditorEndpoint endpoint) { + if (endpoint.host.isPresent()) { + return true; + } + + final String configuredHost = plugin.getServer().getIp(); + return configuredHost != null && !configuredHost.isBlank() && !"0.0.0.0".equals(configuredHost); + } + private @NotNull Optional virtualHost(final @NotNull CommandSender sender) { if (!(sender instanceof Player)) { return Optional.empty(); diff --git a/src/main/java/com/extendedclip/deluxemenus/config/DeluxeMenusConfig.java b/src/main/java/com/extendedclip/deluxemenus/config/DeluxeMenusConfig.java index 83414dcd..77a4e78d 100644 --- a/src/main/java/com/extendedclip/deluxemenus/config/DeluxeMenusConfig.java +++ b/src/main/java/com/extendedclip/deluxemenus/config/DeluxeMenusConfig.java @@ -177,6 +177,7 @@ public boolean loadDefConfig() { c.addDefault("check_updates", true); c.addDefault("use_admin_commands_in_menus_list", false); c.addDefault("menus_list_page_size", 10); + c.addDefault("web_editor_public_url", ""); c.addDefault("sub_menus", new HashMap<>()); c.options().copyDefaults(true); diff --git a/src/main/java/com/extendedclip/deluxemenus/config/GeneralConfig.java b/src/main/java/com/extendedclip/deluxemenus/config/GeneralConfig.java index 0421b399..70e382cf 100644 --- a/src/main/java/com/extendedclip/deluxemenus/config/GeneralConfig.java +++ b/src/main/java/com/extendedclip/deluxemenus/config/GeneralConfig.java @@ -12,6 +12,7 @@ public class GeneralConfig { private boolean useAdminCommandsInMenusList = false; private int menusListPageSize = 10; private int metasListPageSize = 15; + private String webEditorPublicUrl = ""; public GeneralConfig(final @NotNull DeluxeMenus plugin) { this.plugin = plugin; @@ -23,12 +24,14 @@ public void load() { plugin.getConfig().addDefault("use_admin_commands_in_menus_list", false); plugin.getConfig().addDefault("menus_list_page_size", menusListPageSize); plugin.getConfig().addDefault("metas_list_page_size", metasListPageSize); + plugin.getConfig().addDefault("web_editor_public_url", webEditorPublicUrl); checkForUpdates = plugin.getConfig().getBoolean("check_updates", false); debugLevel = loadDebugLevel(); useAdminCommandsInMenusList = plugin.getConfig().getBoolean("use_admin_commands_in_menus_list", false); menusListPageSize = plugin.getConfig().getInt("menus_list_page_size", 10); metasListPageSize = plugin.getConfig().getInt("metas_list_page_size", 15); + webEditorPublicUrl = plugin.getConfig().getString("web_editor_public_url", ""); } public void reload() { @@ -56,6 +59,10 @@ public int metasListPageSize() { return metasListPageSize; } + public @NotNull String webEditorPublicUrl() { + return webEditorPublicUrl; + } + private @NotNull DebugLevel loadDebugLevel() { String configDebugLevel = plugin.getConfig().getString("debug", "HIGHEST"); From ca747219ee99039dce78548935f7d32e2b782e7c Mon Sep 17 00:00:00 2001 From: unknown <1tranducnhan.2021@gmail.com> Date: Sat, 23 May 2026 12:37:40 +0700 Subject: [PATCH 12/12] Fix editor saves and submenu carryover --- .../deluxemenus/action/ClickActionTask.java | 4 ++++ .../command/subcommand/EditCommand.java | 6 +++--- .../deluxemenus/editor/MenuConfigEditor.java | 20 +++++++++++++++---- .../editor/MenuEditPromptRegistry.java | 2 +- .../deluxemenus/listener/PlayerListener.java | 2 +- .../extendedclip/deluxemenus/menu/Menu.java | 5 +++-- 6 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java b/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java index b0e24937..2ffdd29b 100644 --- a/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java +++ b/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java @@ -557,6 +557,10 @@ private Menu getPlayerInventoryMenuToKeep(final @NotNull Optional ho return null; } + if (holder.isEmpty() || !holder.get().getMenuName().equalsIgnoreCase(menuToOpen.options().name())) { + return null; + } + return holder.flatMap(MenuHolder::getRenderedPlayerInventoryMenu).orElse(null); } } diff --git a/src/main/java/com/extendedclip/deluxemenus/command/subcommand/EditCommand.java b/src/main/java/com/extendedclip/deluxemenus/command/subcommand/EditCommand.java index 104b12f6..c843b005 100644 --- a/src/main/java/com/extendedclip/deluxemenus/command/subcommand/EditCommand.java +++ b/src/main/java/com/extendedclip/deluxemenus/command/subcommand/EditCommand.java @@ -176,7 +176,7 @@ private void setValue(final @NotNull CommandSender sender, final @NotNull Menu m } configEditor.reload(menu); - plugin.sms(sender, Component.text("Updated " + option + " for slot " + slot + ".", NamedTextColor.GREEN)); + plugin.sms(sender, Component.text("Updated " + option + " for slot " + slot + " in " + configEditor.describeTarget(menu) + ".", NamedTextColor.GREEN)); } private void prompt(final @NotNull CommandSender sender, final @NotNull Menu menu, final @NotNull List arguments) { @@ -221,7 +221,7 @@ private void setMenuValue(final @NotNull CommandSender sender, final @NotNull Me } configEditor.reload(menu); - plugin.sms(sender, Component.text("Updated " + option + " for " + menu.options().name() + ".", NamedTextColor.GREEN)); + plugin.sms(sender, Component.text("Updated " + option + " for " + menu.options().name() + " in " + configEditor.describeTarget(menu) + ".", NamedTextColor.GREEN)); } private void deleteItem(final @NotNull CommandSender sender, final @NotNull Menu menu, final @NotNull String slotInput) { @@ -242,7 +242,7 @@ private void deleteItem(final @NotNull CommandSender sender, final @NotNull Menu } configEditor.reload(menu); - plugin.sms(sender, Component.text("Deleted item config for slot " + slot + ".", NamedTextColor.GREEN)); + plugin.sms(sender, Component.text("Deleted item config for slot " + slot + " in " + configEditor.describeTarget(menu) + ".", NamedTextColor.GREEN)); } private int parseSlot(final @NotNull CommandSender sender, final @NotNull String input) { diff --git a/src/main/java/com/extendedclip/deluxemenus/editor/MenuConfigEditor.java b/src/main/java/com/extendedclip/deluxemenus/editor/MenuConfigEditor.java index a9244698..eacd2ea3 100644 --- a/src/main/java/com/extendedclip/deluxemenus/editor/MenuConfigEditor.java +++ b/src/main/java/com/extendedclip/deluxemenus/editor/MenuConfigEditor.java @@ -44,6 +44,10 @@ public MenuConfigEditor(final @NotNull DeluxeMenus plugin) { return plugin.getConfig().saveToString(); } + public @NotNull String describeTarget(final @NotNull Menu menu) { + return resolveFile(menu).map(File::getPath).orElse("config.yml"); + } + public void saveRaw(final @NotNull Menu menu, final @NotNull String raw) throws IOException { final Optional optionalFile = resolveFile(menu); if (optionalFile.isPresent()) { @@ -141,6 +145,8 @@ public void reload(final @NotNull Menu menu) { final boolean mainConfigMenu = "config".equalsIgnoreCase(menu.path()); Menu.unload(plugin, menuName); + plugin.reloadConfig(); + plugin.reload(); if (subMenu) { if (mainConfigMenu) { plugin.getConfiguration().loadSubMenus(); @@ -170,7 +176,12 @@ public void reload(final @NotNull Menu menu) { private void save(final @NotNull Menu menu, final @NotNull YamlConfiguration config) throws IOException { final Optional optionalFile = resolveFile(menu); if (optionalFile.isPresent()) { - config.save(optionalFile.get()); + final File file = optionalFile.get(); + final File parent = file.getParentFile(); + if (parent != null && !parent.exists()) { + parent.mkdirs(); + } + config.save(file); return; } @@ -261,12 +272,13 @@ private boolean matchesSlot(final @NotNull YamlConfiguration config, final @NotN return true; } - if (!config.isList(itemPath + ".slots")) { + final List configuredSlots = config.getList(itemPath + ".slots"); + if (configuredSlots == null) { return false; } - for (final String configuredSlot : config.getStringList(itemPath + ".slots")) { - if (configuredSlotMatches(configuredSlot, slot)) { + for (final Object configuredSlot : configuredSlots) { + if (configuredSlot != null && configuredSlotMatches(String.valueOf(configuredSlot), slot)) { return true; } } diff --git a/src/main/java/com/extendedclip/deluxemenus/editor/MenuEditPromptRegistry.java b/src/main/java/com/extendedclip/deluxemenus/editor/MenuEditPromptRegistry.java index cf1c6a2d..52c5696e 100644 --- a/src/main/java/com/extendedclip/deluxemenus/editor/MenuEditPromptRegistry.java +++ b/src/main/java/com/extendedclip/deluxemenus/editor/MenuEditPromptRegistry.java @@ -60,7 +60,7 @@ public static void submit(final @NotNull DeluxeMenus plugin, final @NotNull Play } configEditor.reload(menu); - plugin.sms(player, text("Updated " + prompt.option + " for slot " + prompt.slot + ".", NamedTextColor.GREEN)); + plugin.sms(player, text("Updated " + prompt.option + " for slot " + prompt.slot + " in " + configEditor.describeTarget(menu) + ".", NamedTextColor.GREEN)); findMenu(prompt.menuName).ifPresent(reloaded -> new MenuEditorManager(plugin).openSlot(player, reloaded, prompt.slot)); } diff --git a/src/main/java/com/extendedclip/deluxemenus/listener/PlayerListener.java b/src/main/java/com/extendedclip/deluxemenus/listener/PlayerListener.java index 224f07f8..e596d9ec 100644 --- a/src/main/java/com/extendedclip/deluxemenus/listener/PlayerListener.java +++ b/src/main/java/com/extendedclip/deluxemenus/listener/PlayerListener.java @@ -68,7 +68,7 @@ public void onLeave(PlayerQuitEvent event) { } } - @EventHandler(priority = EventPriority.LOWEST) + @EventHandler(priority = EventPriority.HIGHEST) public void onDeath(PlayerDeathEvent event) { final Player player = event.getEntity(); final Optional optionalHolder = Menu.getMenuHolder(player); diff --git a/src/main/java/com/extendedclip/deluxemenus/menu/Menu.java b/src/main/java/com/extendedclip/deluxemenus/menu/Menu.java index 81fa27bb..cc95c8b5 100644 --- a/src/main/java/com/extendedclip/deluxemenus/menu/Menu.java +++ b/src/main/java/com/extendedclip/deluxemenus/menu/Menu.java @@ -461,7 +461,8 @@ public void refreshForAll() { } if (openPlayerInventoryMenu.isPresent()) { - activeItems.addAll(openPlayerInventoryMenu.get().getActiveItems(holder, 0, 36, this.options.size())); + final Menu bottomMenu = openPlayerInventoryMenu.get(); + activeItems.addAll(bottomMenu.getActiveItems(holder, 0, Math.min(36, bottomMenu.options().size()), this.options.size())); return activeItems; } @@ -469,7 +470,7 @@ public void refreshForAll() { final String playerInventoryMenuName = this.options.playerInventoryMenu().get(); final Optional bottomMenu = Menu.getSubMenuByName(playerInventoryMenuName); if (bottomMenu.isPresent()) { - activeItems.addAll(bottomMenu.get().getActiveItems(holder, 0, 36, this.options.size())); + activeItems.addAll(bottomMenu.get().getActiveItems(holder, 0, Math.min(36, bottomMenu.get().options().size()), this.options.size())); } else { plugin.debug( DebugLevel.HIGHEST,