diff --git a/.github/ISSUE_TEMPLATE/1-bug.md b/.github/ISSUE_TEMPLATE/1-bug.md index adcf1f1..d2adf25 100644 --- a/.github/ISSUE_TEMPLATE/1-bug.md +++ b/.github/ISSUE_TEMPLATE/1-bug.md @@ -5,7 +5,7 @@ labels: Bug --- + + set-plugin-version + validate + + run + + + true + + + + + + + + + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + true + + + ${project.pluginVersion} + + + + + + + org.apache.maven.plugins + maven-shade-plugin + + false + false + + + *:* + + META-INF/maven/ + + + + + + org.bstats.bukkit + me.filoghost.chestcommands.metrics + + + me.filoghost.updatechecker + me.filoghost.chestcommands.updater + + + me.filoghost.commons + me.filoghost.chestcommands.commons + + + + + + package + + shade + + + + + + + + \ No newline at end of file diff --git a/plugin/src/main/java/me/filoghost/chestcommands/ChestCommands.java b/plugin/src/main/java/me/filoghost/chestcommands/ChestCommands.java new file mode 100644 index 0000000..14293bc --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/ChestCommands.java @@ -0,0 +1,223 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands; + +import me.filoghost.chestcommands.api.internal.BackendAPI; +import me.filoghost.chestcommands.command.CommandHandler; +import me.filoghost.chestcommands.config.ConfigManager; +import me.filoghost.chestcommands.config.CustomPlaceholders; +import me.filoghost.chestcommands.config.Settings; +import me.filoghost.chestcommands.hook.BarAPIHook; +import me.filoghost.chestcommands.hook.BungeeCordHook; +import me.filoghost.chestcommands.hook.PlaceholderAPIHook; +import me.filoghost.chestcommands.hook.VaultEconomyHook; +import me.filoghost.chestcommands.legacy.UpgradeExecutorException; +import me.filoghost.chestcommands.legacy.UpgradesExecutor; +import me.filoghost.chestcommands.listener.CommandListener; +import me.filoghost.chestcommands.listener.InventoryListener; +import me.filoghost.chestcommands.listener.JoinListener; +import me.filoghost.chestcommands.listener.SignListener; +import me.filoghost.chestcommands.logging.Errors; +import me.filoghost.chestcommands.logging.PrintableErrorCollector; +import me.filoghost.chestcommands.menu.MenuManager; +import me.filoghost.chestcommands.parsing.menu.LoadedMenu; +import me.filoghost.chestcommands.placeholder.PlaceholderManager; +import me.filoghost.chestcommands.task.TickingTask; +import me.filoghost.commons.BaseJavaPlugin; +import me.filoghost.commons.CommonsUtil; +import me.filoghost.commons.command.CommandFramework; +import me.filoghost.commons.config.ConfigLoader; +import me.filoghost.commons.logging.ErrorCollector; +import me.filoghost.commons.logging.Log; +import me.filoghost.updatechecker.UpdateChecker; +import org.bstats.bukkit.MetricsLite; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +public class ChestCommands extends BaseJavaPlugin { + + + public static final String CHAT_PREFIX = ChatColor.DARK_GREEN + "[" + ChatColor.GREEN + "ChestCommands" + ChatColor.DARK_GREEN + "] " + ChatColor.GREEN; + + private static Plugin pluginInstance; + private static Path dataFolderPath; + + private static ConfigManager configManager; + private static MenuManager menuManager; + private static CustomPlaceholders placeholders; + + private static ErrorCollector lastLoadErrors; + private static String newVersion; + + @Override + protected void onCheckedEnable() throws PluginEnableException { + if (!CommonsUtil.isClassLoaded("org.bukkit.inventory.ItemFlag")) { // ItemFlag was added in 1.8 + if (Bukkit.getVersion().contains("(MC: 1.8)")) { + throw new PluginEnableException("ChestCommands requires a more recent version of Bukkit 1.8 to run."); + } else { + throw new PluginEnableException("ChestCommands requires at least Bukkit 1.8 to run."); + } + } + + if (pluginInstance != null || System.getProperty("ChestCommandsLoaded") != null) { + Log.warning("Please do not use /reload or plugin reloaders. Use the command \"/cc reload\" instead."); + return; + } + + System.setProperty("ChestCommandsLoaded", "true"); + + pluginInstance = this; + dataFolderPath = getDataFolder().toPath(); + Log.setLogger(getLogger()); + configManager = new ConfigManager(getDataFolderPath()); + menuManager = new MenuManager(); + placeholders = new CustomPlaceholders(); + + BackendAPI.setImplementation(new DefaultBackendAPI()); + + VaultEconomyHook.INSTANCE.setup(); + BarAPIHook.INSTANCE.setup(); + PlaceholderAPIHook.INSTANCE.setup(); + BungeeCordHook.INSTANCE.setup(); + + if (VaultEconomyHook.INSTANCE.isEnabled()) { + Log.info("Hooked Vault"); + } else { + Log.warning("Couldn't find Vault and a compatible economy plugin! Money-related features will not work."); + } + + if (BarAPIHook.INSTANCE.isEnabled()) { + Log.info("Hooked BarAPI"); + } + + if (PlaceholderAPIHook.INSTANCE.isEnabled()) { + Log.info("Hooked PlaceholderAPI"); + } + + if (Settings.update_notifications) { + UpdateChecker.run(this, 56919, (String newVersion) -> { + ChestCommands.newVersion = newVersion; + + Log.info("Found a new version: " + newVersion + " (yours: v" + getDescription().getVersion() + ")"); + Log.info("Download the update on Bukkit Dev:"); + Log.info("https://dev.bukkit.org/projects/chest-commands"); + }); + } + + // Start bStats metrics + int pluginID = 3658; + new MetricsLite(this, pluginID); + + Bukkit.getPluginManager().registerEvents(new CommandListener(menuManager), this); + Bukkit.getPluginManager().registerEvents(new InventoryListener(menuManager), this); + Bukkit.getPluginManager().registerEvents(new JoinListener(), this); + Bukkit.getPluginManager().registerEvents(new SignListener(menuManager), this); + + CommandFramework.register(this, new CommandHandler(menuManager, "chestcommands")); + + ErrorCollector errorCollector = load(); + + if (errorCollector.hasErrors()) { + errorCollector.logToConsole(); + Bukkit.getScheduler().runTaskLater(this, () -> { + Bukkit.getConsoleSender().sendMessage( + ChestCommands.CHAT_PREFIX + ChatColor.RED + "Encountered " + errorCollector.getErrorsCount() + " error(s) on load. " + + "Check previous console logs or run \"/chestcommands errors\" to see them again."); + }, 10L); + } + + Bukkit.getScheduler().runTaskTimer(this, new TickingTask(), 1L, 1L); + } + + @Override + public void onDisable() { + closeAllMenus(); + } + + public static ErrorCollector load() { + ErrorCollector errorCollector = new PrintableErrorCollector(); + menuManager.clear(); + boolean isFreshInstall = !Files.isDirectory(configManager.getRootDataFolder()); + try { + Files.createDirectories(configManager.getRootDataFolder()); + } catch (IOException e) { + errorCollector.add(e, Errors.Config.createDataFolderIOException); + return errorCollector; + } + + UpgradesExecutor upgradeExecutor = new UpgradesExecutor(configManager); + + try { + boolean allUpgradesSuccessful = upgradeExecutor.run(isFreshInstall, errorCollector); + if (!allUpgradesSuccessful) { + errorCollector.add(Errors.Upgrade.failedSomeUpgrades); + } + } catch (UpgradeExecutorException e) { + errorCollector.add(e, Errors.Upgrade.genericExecutorError); + errorCollector.add(Errors.Upgrade.failedSomeUpgrades); + } + + configManager.tryLoadSettings(errorCollector); + configManager.tryLoadLang(errorCollector); + placeholders = configManager.tryLoadCustomPlaceholders(errorCollector); + PlaceholderManager.setStaticPlaceholders(placeholders.getPlaceholders()); + + // Create the menu folder with the example menu + if (!Files.isDirectory(configManager.getMenusFolder())) { + ConfigLoader exampleMenuLoader = configManager.getConfigLoader(configManager.getMenusFolder().resolve("example.yml")); + configManager.tryCreateDefault(errorCollector, exampleMenuLoader); + } + + List loadedMenus = configManager.tryLoadMenus(errorCollector); + for (LoadedMenu loadedMenu : loadedMenus) { + menuManager.registerMenu(loadedMenu, errorCollector); + } + + ChestCommands.lastLoadErrors = errorCollector; + return errorCollector; + } + + public static void closeAllMenus() { + for (Player player : Bukkit.getOnlinePlayers()) { + if (MenuManager.getOpenMenuView(player) != null) { + player.closeInventory(); + } + } + } + + + public static Plugin getPluginInstance() { + return pluginInstance; + } + + public static Path getDataFolderPath() { + return dataFolderPath; + } + + public static MenuManager getMenuManager() { + return menuManager; + } + + public static boolean hasNewVersion() { + return newVersion != null; + } + + public static String getNewVersion() { + return newVersion; + } + + public static ErrorCollector getLastLoadErrors() { + return lastLoadErrors; + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/DefaultBackendAPI.java b/plugin/src/main/java/me/filoghost/chestcommands/DefaultBackendAPI.java new file mode 100644 index 0000000..f8924c1 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/DefaultBackendAPI.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands; + +import me.filoghost.chestcommands.api.ConfigurableIcon; +import me.filoghost.chestcommands.api.Menu; +import me.filoghost.chestcommands.api.PlaceholderReplacer; +import me.filoghost.chestcommands.api.StaticIcon; +import me.filoghost.chestcommands.api.internal.BackendAPI; +import me.filoghost.chestcommands.icon.APIConfigurableIcon; +import me.filoghost.chestcommands.icon.APIStaticIcon; +import me.filoghost.chestcommands.menu.APIMenu; +import me.filoghost.chestcommands.menu.InternalMenu; +import me.filoghost.chestcommands.placeholder.PlaceholderManager; +import me.filoghost.commons.Preconditions; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.Plugin; + +public class DefaultBackendAPI extends BackendAPI { + + @Override + public boolean pluginMenuExists(String menuFileName) { + Preconditions.notNull(menuFileName, "menuFileName"); + + return ChestCommands.getMenuManager().getMenuByFileName(menuFileName) != null; + } + + @Override + public boolean openPluginMenu(Player player, String menuFileName) { + Preconditions.notNull(player, "player"); + Preconditions.notNull(menuFileName, "menuFileName"); + + InternalMenu menu = ChestCommands.getMenuManager().getMenuByFileName(menuFileName); + + if (menu != null) { + menu.open(player); + return true; + } else { + return false; + } + } + + @Override + public ConfigurableIcon createConfigurableIcon(Material material) { + return new APIConfigurableIcon(material); + } + + @Override + public Menu createMenu(Plugin owner, String title, int rows) { + return new APIMenu(owner, title, rows); + } + + @Override + public StaticIcon createStaticIcon(ItemStack itemStack) { + return new APIStaticIcon(itemStack); + } + + @Override + public void registerPlaceholder(Plugin plugin, String identifier, PlaceholderReplacer placeholderReplacer) { + PlaceholderManager.registerPluginPlaceholder(plugin, identifier, placeholderReplacer); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/Permissions.java b/plugin/src/main/java/me/filoghost/chestcommands/Permissions.java new file mode 100644 index 0000000..7ca067b --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/Permissions.java @@ -0,0 +1,20 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands; + +public class Permissions { + + public static final String + + BASE_PREFIX = "chestcommands.", + COMMAND_PREFIX = BASE_PREFIX + "command.", + OPEN_MENU_PREFIX = BASE_PREFIX + "open.", + + UPDATE_NOTIFICATIONS = BASE_PREFIX + "update", + SEE_ERRORS = BASE_PREFIX + "errors", + SIGN_CREATE = BASE_PREFIX + "sign"; + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/action/Action.java b/plugin/src/main/java/me/filoghost/chestcommands/action/Action.java new file mode 100644 index 0000000..f0554ed --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/action/Action.java @@ -0,0 +1,14 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.action; + +import org.bukkit.entity.Player; + +public interface Action { + + void execute(Player player); + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/action/BroadcastAction.java b/plugin/src/main/java/me/filoghost/chestcommands/action/BroadcastAction.java new file mode 100644 index 0000000..69cb8e6 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/action/BroadcastAction.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.action; + +import me.filoghost.chestcommands.placeholder.PlaceholderString; +import me.filoghost.commons.Colors; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +public class BroadcastAction implements Action { + + private final PlaceholderString message; + + public BroadcastAction(String serializedAction) { + message = PlaceholderString.of(Colors.addColors(serializedAction)); + } + + @Override + public void execute(Player player) { + Bukkit.broadcastMessage(message.getValue(player)); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/action/ChangeServerAction.java b/plugin/src/main/java/me/filoghost/chestcommands/action/ChangeServerAction.java new file mode 100644 index 0000000..1cecda7 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/action/ChangeServerAction.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.action; + +import me.filoghost.chestcommands.hook.BungeeCordHook; +import me.filoghost.chestcommands.placeholder.PlaceholderString; +import org.bukkit.entity.Player; + +public class ChangeServerAction implements Action { + + private final PlaceholderString targetServer; + + public ChangeServerAction(String serializedAction) { + targetServer = PlaceholderString.of(serializedAction); + } + + @Override + public void execute(Player player) { + BungeeCordHook.connect(player, targetServer.getValue(player)); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/action/ConsoleCommandAction.java b/plugin/src/main/java/me/filoghost/chestcommands/action/ConsoleCommandAction.java new file mode 100644 index 0000000..3ce2660 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/action/ConsoleCommandAction.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.action; + +import me.filoghost.chestcommands.placeholder.PlaceholderString; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +public class ConsoleCommandAction implements Action { + + private final PlaceholderString command; + + public ConsoleCommandAction(String serializedAction) { + command = PlaceholderString.of(serializedAction); + } + + @Override + public void execute(Player player) { + Bukkit.dispatchCommand(Bukkit.getConsoleSender(), command.getValue(player)); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/action/DisabledAction.java b/plugin/src/main/java/me/filoghost/chestcommands/action/DisabledAction.java new file mode 100644 index 0000000..52a0c47 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/action/DisabledAction.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.action; + +import org.bukkit.entity.Player; + +public class DisabledAction implements Action { + + private final String errorMessage; + + public DisabledAction(String errorMessage) { + this.errorMessage = errorMessage; + } + + @Override + public void execute(Player player) { + player.sendMessage(errorMessage); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/action/DragonBarAction.java b/plugin/src/main/java/me/filoghost/chestcommands/action/DragonBarAction.java new file mode 100644 index 0000000..9af3a0a --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/action/DragonBarAction.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.action; + +import me.filoghost.chestcommands.hook.BarAPIHook; +import me.filoghost.chestcommands.logging.Errors; +import me.filoghost.chestcommands.parsing.NumberParser; +import me.filoghost.chestcommands.parsing.ParseException; +import me.filoghost.chestcommands.placeholder.PlaceholderString; +import me.filoghost.commons.Colors; +import me.filoghost.commons.Strings; +import org.bukkit.entity.Player; + +public class DragonBarAction implements Action { + + private final PlaceholderString message; + private final int seconds; + + public DragonBarAction(String serialiazedAction) throws ParseException { + String message; + + String[] split = Strings.trimmedSplit(serialiazedAction, "\\|", 2); // Max of 2 pieces + if (split.length > 1) { + try { + seconds = NumberParser.getStrictlyPositiveInteger(split[0]); + message = split[1]; + } catch (ParseException e) { + throw new ParseException(Errors.Parsing.invalidBossBarTime(split[0]), e); + } + } else { + seconds = 1; + message = serialiazedAction; + } + + this.message = PlaceholderString.of(Colors.addColors(message)); + } + + @Override + public void execute(Player player) { + if (BarAPIHook.INSTANCE.isEnabled()) { + BarAPIHook.setMessage(player, message.getValue(player), seconds); + } else { + player.sendMessage(Errors.User.configurationError("BarAPI plugin not found")); + } + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/action/GiveItemAction.java b/plugin/src/main/java/me/filoghost/chestcommands/action/GiveItemAction.java new file mode 100644 index 0000000..0b98bd4 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/action/GiveItemAction.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.action; + +import me.filoghost.chestcommands.parsing.ItemStackParser; +import me.filoghost.chestcommands.parsing.ParseException; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +public class GiveItemAction implements Action { + + private final ItemStack itemToGive; + + public GiveItemAction(String serializedAction) throws ParseException { + ItemStackParser reader = new ItemStackParser(serializedAction, true); + reader.checkNotAir(); + itemToGive = reader.createStack(); + } + + @Override + public void execute(Player player) { + player.getInventory().addItem(itemToGive.clone()); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/action/GiveMoneyAction.java b/plugin/src/main/java/me/filoghost/chestcommands/action/GiveMoneyAction.java new file mode 100644 index 0000000..c53e7c7 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/action/GiveMoneyAction.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.action; + +import me.filoghost.chestcommands.hook.VaultEconomyHook; +import me.filoghost.chestcommands.logging.Errors; +import me.filoghost.chestcommands.parsing.NumberParser; +import me.filoghost.chestcommands.parsing.ParseException; +import org.bukkit.entity.Player; + +public class GiveMoneyAction implements Action { + + private final double moneyToGive; + + public GiveMoneyAction(String serializedAction) throws ParseException { + moneyToGive = NumberParser.getStrictlyPositiveDouble(serializedAction); + } + + @Override + public void execute(Player player) { + if (VaultEconomyHook.INSTANCE.isEnabled()) { + VaultEconomyHook.giveMoney(player, moneyToGive); + } else { + player.sendMessage(Errors.User.configurationError("Vault with a compatible economy plugin not found")); + } + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/action/OpCommandAction.java b/plugin/src/main/java/me/filoghost/chestcommands/action/OpCommandAction.java new file mode 100644 index 0000000..1da8b27 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/action/OpCommandAction.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.action; + +import me.filoghost.chestcommands.placeholder.PlaceholderString; +import org.bukkit.entity.Player; + +public class OpCommandAction implements Action { + + private final PlaceholderString command; + + public OpCommandAction(String serializedAction) { + command = PlaceholderString.of(serializedAction); + } + + @Override + public void execute(Player player) { + if (player.isOp()) { + player.chat("/" + command.getValue(player)); + } else { + player.setOp(true); + player.chat("/" + command.getValue(player)); + player.setOp(false); + } + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/action/OpenMenuAction.java b/plugin/src/main/java/me/filoghost/chestcommands/action/OpenMenuAction.java new file mode 100644 index 0000000..0f17da5 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/action/OpenMenuAction.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.action; + +import me.filoghost.chestcommands.ChestCommands; +import me.filoghost.chestcommands.logging.Errors; +import me.filoghost.chestcommands.menu.InternalMenu; +import me.filoghost.chestcommands.placeholder.PlaceholderString; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +public class OpenMenuAction implements Action { + + private final PlaceholderString targetMenu; + + public OpenMenuAction(String serializedAction) { + targetMenu = PlaceholderString.of(serializedAction); + } + + @Override + public void execute(final Player player) { + String menuName = targetMenu.getValue(player); + final InternalMenu menu = ChestCommands.getMenuManager().getMenuByFileName(menuName); + + if (menu != null) { + /* + * Delay the task, since this action is executed in ClickInventoryEvent + * and opening another inventory in the same moment is not a good idea. + */ + Bukkit.getScheduler().runTask(ChestCommands.getPluginInstance(), () -> { + menu.openCheckingPermission(player); + }); + + } else { + player.sendMessage(Errors.User.configurationError("couldn't find the menu \"" + menuName + "\"")); + } + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/action/PlaySoundAction.java b/plugin/src/main/java/me/filoghost/chestcommands/action/PlaySoundAction.java new file mode 100644 index 0000000..e11466e --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/action/PlaySoundAction.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.action; + +import me.filoghost.chestcommands.logging.Errors; +import me.filoghost.chestcommands.parsing.NumberParser; +import me.filoghost.chestcommands.parsing.ParseException; +import me.filoghost.commons.Strings; +import me.filoghost.commons.collection.Registry; +import org.bukkit.Sound; +import org.bukkit.entity.Player; + +import java.util.Optional; + +public class PlaySoundAction implements Action { + + private static final Registry SOUNDS_REGISTRY = Registry.fromEnumValues(Sound.class); + + private final Sound sound; + private final float pitch; + private final float volume; + + public PlaySoundAction(String serializedAction) throws ParseException { + String[] split = Strings.trimmedSplit(serializedAction, ",", 3); + + Optional sound = SOUNDS_REGISTRY.find(split[0]); + if (!sound.isPresent()) { + throw new ParseException(Errors.Parsing.unknownSound(split[0])); + } + this.sound = sound.get(); + + if (split.length > 1) { + try { + pitch = NumberParser.getFloat(split[1]); + } catch (ParseException e) { + throw new ParseException(Errors.Parsing.invalidSoundPitch(split[1]), e); + } + } else { + pitch = 1.0f; + } + + if (split.length > 2) { + try { + volume = NumberParser.getFloat(split[2]); + } catch (ParseException e) { + throw new ParseException(Errors.Parsing.invalidSoundVolume(split[2]), e); + } + } else { + volume = 1.0f; + } + } + + @Override + public void execute(Player player) { + player.playSound(player.getLocation(), sound, volume, pitch); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/action/PlayerCommandAction.java b/plugin/src/main/java/me/filoghost/chestcommands/action/PlayerCommandAction.java new file mode 100644 index 0000000..883da09 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/action/PlayerCommandAction.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.action; + +import me.filoghost.chestcommands.placeholder.PlaceholderString; +import org.bukkit.entity.Player; + +public class PlayerCommandAction implements Action { + + private final PlaceholderString command; + + public PlayerCommandAction(String serializedAction) { + command = PlaceholderString.of(serializedAction); + } + + @Override + public void execute(Player player) { + player.chat('/' + command.getValue(player)); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/action/SendMessageAction.java b/plugin/src/main/java/me/filoghost/chestcommands/action/SendMessageAction.java new file mode 100644 index 0000000..aecfbab --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/action/SendMessageAction.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.action; + +import me.filoghost.chestcommands.placeholder.PlaceholderString; +import me.filoghost.commons.Colors; +import org.bukkit.entity.Player; + +public class SendMessageAction implements Action { + + private final PlaceholderString message; + + public SendMessageAction(String serializedAction) { + message = PlaceholderString.of(Colors.addColors(serializedAction)); + } + + @Override + public void execute(Player player) { + player.sendMessage(message.getValue(player)); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/attribute/ActionsAttribute.java b/plugin/src/main/java/me/filoghost/chestcommands/attribute/ActionsAttribute.java new file mode 100644 index 0000000..3297af5 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/attribute/ActionsAttribute.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.attribute; + +import me.filoghost.chestcommands.action.Action; +import me.filoghost.chestcommands.action.DisabledAction; +import me.filoghost.chestcommands.icon.InternalConfigurableIcon; +import me.filoghost.chestcommands.logging.Errors; +import me.filoghost.chestcommands.parsing.ActionParser; +import me.filoghost.chestcommands.parsing.ParseException; + +import java.util.ArrayList; +import java.util.List; + +public class ActionsAttribute implements IconAttribute { + + private final List actions; + + public ActionsAttribute(List serializedActions, AttributeErrorHandler errorHandler) { + actions = new ArrayList<>(); + + for (String serializedAction : serializedActions) { + if (serializedAction == null || serializedAction.isEmpty()) { + continue; // Skip + } + + try { + actions.add(ActionParser.parse(serializedAction)); + } catch (ParseException e) { + actions.add(new DisabledAction(Errors.User.configurationError( + "an action linked to clicking this icon was not executed because it was not valid"))); + errorHandler.onListElementError(serializedAction, e); + } + } + } + + @Override + public void apply(InternalConfigurableIcon icon) { + icon.setClickActions(actions); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/attribute/AmountAttribute.java b/plugin/src/main/java/me/filoghost/chestcommands/attribute/AmountAttribute.java new file mode 100644 index 0000000..7879b9e --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/attribute/AmountAttribute.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.attribute; + +import me.filoghost.chestcommands.icon.InternalConfigurableIcon; +import me.filoghost.chestcommands.logging.Errors; +import me.filoghost.chestcommands.parsing.ParseException; + +public class AmountAttribute implements IconAttribute { + + private final int amount; + + public AmountAttribute(int amount, AttributeErrorHandler errorHandler) throws ParseException { + if (amount < 0) { + throw new ParseException(Errors.Parsing.zeroOrPositive); + } + this.amount = amount; + } + + @Override + public void apply(InternalConfigurableIcon icon) { + icon.setAmount(amount); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/attribute/AttributeErrorHandler.java b/plugin/src/main/java/me/filoghost/chestcommands/attribute/AttributeErrorHandler.java new file mode 100644 index 0000000..f61ed3d --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/attribute/AttributeErrorHandler.java @@ -0,0 +1,14 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.attribute; + +import me.filoghost.chestcommands.parsing.ParseException; + +public interface AttributeErrorHandler { + + void onListElementError(String listElement, ParseException e); + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/attribute/BannerColorAttribute.java b/plugin/src/main/java/me/filoghost/chestcommands/attribute/BannerColorAttribute.java new file mode 100644 index 0000000..0f73c75 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/attribute/BannerColorAttribute.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.attribute; + +import me.filoghost.chestcommands.icon.InternalConfigurableIcon; +import me.filoghost.chestcommands.parsing.ItemMetaParser; +import me.filoghost.chestcommands.parsing.ParseException; +import org.bukkit.DyeColor; + +public class BannerColorAttribute implements IconAttribute { + + private final DyeColor dyeColor; + + public BannerColorAttribute(String serializedDyeColor, AttributeErrorHandler errorHandler) throws ParseException { + this.dyeColor = ItemMetaParser.parseDyeColor(serializedDyeColor); + } + + @Override + public void apply(InternalConfigurableIcon icon) { + icon.setBannerColor(dyeColor); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/attribute/BannerPatternsAttribute.java b/plugin/src/main/java/me/filoghost/chestcommands/attribute/BannerPatternsAttribute.java new file mode 100644 index 0000000..3cddeea --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/attribute/BannerPatternsAttribute.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.attribute; + +import me.filoghost.chestcommands.icon.InternalConfigurableIcon; +import me.filoghost.chestcommands.parsing.ItemMetaParser; +import me.filoghost.chestcommands.parsing.ParseException; +import org.bukkit.block.banner.Pattern; + +import java.util.ArrayList; +import java.util.List; + +public class BannerPatternsAttribute implements IconAttribute { + + private final List patterns; + + public BannerPatternsAttribute(List serializedPatterns, AttributeErrorHandler errorHandler) { + patterns = new ArrayList<>(); + + for (String serializedPattern : serializedPatterns) { + if (serializedPattern == null || serializedPattern.isEmpty()) { + continue; // Skip + } + + try { + Pattern pattern = ItemMetaParser.parseBannerPattern(serializedPattern); + patterns.add(pattern); + } catch (ParseException e) { + errorHandler.onListElementError(serializedPattern, e); + } + } + + } + + @Override + public void apply(InternalConfigurableIcon icon) { + icon.setBannerPatterns(patterns); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/attribute/ClickPermissionAttribute.java b/plugin/src/main/java/me/filoghost/chestcommands/attribute/ClickPermissionAttribute.java new file mode 100644 index 0000000..4c26176 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/attribute/ClickPermissionAttribute.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.attribute; + +import me.filoghost.chestcommands.icon.InternalConfigurableIcon; + +public class ClickPermissionAttribute implements IconAttribute { + + private final String clickPermission; + + public ClickPermissionAttribute(String clickPermission, AttributeErrorHandler errorHandler) { + this.clickPermission = clickPermission; + } + + @Override + public void apply(InternalConfigurableIcon icon) { + icon.setClickPermission(clickPermission); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/attribute/ClickPermissionMessageAttribute.java b/plugin/src/main/java/me/filoghost/chestcommands/attribute/ClickPermissionMessageAttribute.java new file mode 100644 index 0000000..3b734ed --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/attribute/ClickPermissionMessageAttribute.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.attribute; + +import me.filoghost.chestcommands.icon.InternalConfigurableIcon; + +public class ClickPermissionMessageAttribute implements IconAttribute { + + private final String clickPermissionMessage; + + public ClickPermissionMessageAttribute(String clickPermissionMessage, AttributeErrorHandler errorHandler) { + this.clickPermissionMessage = clickPermissionMessage; + } + + @Override + public void apply(InternalConfigurableIcon icon) { + icon.setNoClickPermissionMessage(clickPermissionMessage); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/attribute/DurabilityAttribute.java b/plugin/src/main/java/me/filoghost/chestcommands/attribute/DurabilityAttribute.java new file mode 100644 index 0000000..1de4fc1 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/attribute/DurabilityAttribute.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.attribute; + +import me.filoghost.chestcommands.icon.InternalConfigurableIcon; + +public class DurabilityAttribute implements IconAttribute { + + private final short durability; + + public DurabilityAttribute(short durability, AttributeErrorHandler errorHandler) { + this.durability = durability; + } + + @Override + public void apply(InternalConfigurableIcon icon) { + icon.setDurability(durability); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/attribute/EnchantmentsAttribute.java b/plugin/src/main/java/me/filoghost/chestcommands/attribute/EnchantmentsAttribute.java new file mode 100644 index 0000000..bd115ab --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/attribute/EnchantmentsAttribute.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.attribute; + +import me.filoghost.chestcommands.icon.InternalConfigurableIcon; +import me.filoghost.chestcommands.parsing.EnchantmentParser; +import me.filoghost.chestcommands.parsing.ParseException; +import org.bukkit.enchantments.Enchantment; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class EnchantmentsAttribute implements IconAttribute { + + private final Map enchantments; + + public EnchantmentsAttribute(List serializedEnchantments, AttributeErrorHandler errorHandler) { + enchantments = new HashMap<>(); + + for (String serializedEnchantment : serializedEnchantments) { + if (serializedEnchantment == null || serializedEnchantment.isEmpty()) { + continue; // Skip + } + + try { + EnchantmentParser.EnchantmentDetails enchantment = EnchantmentParser.parseEnchantment(serializedEnchantment); + enchantments.put(enchantment.getEnchantment(), enchantment.getLevel()); + } catch (ParseException e) { + errorHandler.onListElementError(serializedEnchantment, e); + } + } + } + + @Override + public void apply(InternalConfigurableIcon icon) { + icon.setEnchantments(enchantments); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/attribute/ExpLevelsAttribute.java b/plugin/src/main/java/me/filoghost/chestcommands/attribute/ExpLevelsAttribute.java new file mode 100644 index 0000000..c7fe465 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/attribute/ExpLevelsAttribute.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.attribute; + +import me.filoghost.chestcommands.icon.InternalConfigurableIcon; +import me.filoghost.chestcommands.logging.Errors; +import me.filoghost.chestcommands.parsing.ParseException; + +public class ExpLevelsAttribute implements IconAttribute { + + private final int expLevels; + + public ExpLevelsAttribute(int expLevels, AttributeErrorHandler errorHandler) throws ParseException { + if (expLevels < 0) { + throw new ParseException(Errors.Parsing.zeroOrPositive); + } + this.expLevels = expLevels; + } + + @Override + public void apply(InternalConfigurableIcon icon) { + icon.setRequiredExpLevel(expLevels); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/attribute/IconAttribute.java b/plugin/src/main/java/me/filoghost/chestcommands/attribute/IconAttribute.java new file mode 100644 index 0000000..f3ca62e --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/attribute/IconAttribute.java @@ -0,0 +1,13 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.attribute; + +import me.filoghost.chestcommands.icon.InternalConfigurableIcon; + +public interface IconAttribute { + + void apply(InternalConfigurableIcon icon); +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/attribute/KeepOpenAttribute.java b/plugin/src/main/java/me/filoghost/chestcommands/attribute/KeepOpenAttribute.java new file mode 100644 index 0000000..3ec66aa --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/attribute/KeepOpenAttribute.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.attribute; + +import me.filoghost.chestcommands.api.ClickResult; +import me.filoghost.chestcommands.icon.InternalConfigurableIcon; + +public class KeepOpenAttribute implements IconAttribute { + + private final ClickResult clickResult; + + public KeepOpenAttribute(boolean keepOpen, AttributeErrorHandler errorHandler) { + if (keepOpen) { + this.clickResult = ClickResult.KEEP_OPEN; + } else { + this.clickResult = ClickResult.CLOSE; + } + } + + @Override + public void apply(InternalConfigurableIcon icon) { + icon.setClickResult(clickResult); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/attribute/LeatherColorAttribute.java b/plugin/src/main/java/me/filoghost/chestcommands/attribute/LeatherColorAttribute.java new file mode 100644 index 0000000..1d1a3bf --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/attribute/LeatherColorAttribute.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.attribute; + +import me.filoghost.chestcommands.icon.InternalConfigurableIcon; +import me.filoghost.chestcommands.parsing.ItemMetaParser; +import me.filoghost.chestcommands.parsing.ParseException; +import org.bukkit.Color; + +public class LeatherColorAttribute implements IconAttribute { + + private final Color color; + + public LeatherColorAttribute(String serializedColor, AttributeErrorHandler errorHandler) throws ParseException { + this.color = ItemMetaParser.parseRGBColor(serializedColor); + } + + @Override + public void apply(InternalConfigurableIcon icon) { + icon.setLeatherColor(color); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/attribute/LoreAttribute.java b/plugin/src/main/java/me/filoghost/chestcommands/attribute/LoreAttribute.java new file mode 100644 index 0000000..6dc2a10 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/attribute/LoreAttribute.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.attribute; + +import me.filoghost.chestcommands.config.Settings; +import me.filoghost.chestcommands.icon.InternalConfigurableIcon; +import me.filoghost.commons.Colors; +import me.filoghost.commons.collection.CollectionUtils; + +import java.util.List; + +public class LoreAttribute implements IconAttribute { + + private final List lore; + + public LoreAttribute(List lore, AttributeErrorHandler errorHandler) { + this.lore = colorLore(lore); + } + + private List colorLore(List input) { + return CollectionUtils.transform(input, line -> { + if (!line.isEmpty()) { + return Settings.default_color__lore + Colors.addColors(line); + } else { + return line; + } + }); + } + + @Override + public void apply(InternalConfigurableIcon icon) { + icon.setLore(lore); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/attribute/MaterialAttribute.java b/plugin/src/main/java/me/filoghost/chestcommands/attribute/MaterialAttribute.java new file mode 100644 index 0000000..989545f --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/attribute/MaterialAttribute.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.attribute; + +import me.filoghost.chestcommands.icon.InternalConfigurableIcon; +import me.filoghost.chestcommands.parsing.MaterialParser; +import me.filoghost.chestcommands.parsing.ParseException; +import org.bukkit.Material; + +public class MaterialAttribute implements IconAttribute { + + private final Material material; + + public MaterialAttribute(String serializedMaterial, AttributeErrorHandler errorHandler) throws ParseException { + this.material = MaterialParser.parseMaterial(serializedMaterial); + } + + @Override + public void apply(InternalConfigurableIcon icon) { + icon.setMaterial(material); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/attribute/NBTDataAttribute.java b/plugin/src/main/java/me/filoghost/chestcommands/attribute/NBTDataAttribute.java new file mode 100644 index 0000000..184d3ae --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/attribute/NBTDataAttribute.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.attribute; + +import me.filoghost.chestcommands.icon.InternalConfigurableIcon; +import me.filoghost.chestcommands.parsing.ParseException; +import me.filoghost.chestcommands.util.nbt.parser.MojangsonParseException; +import me.filoghost.chestcommands.util.nbt.parser.MojangsonParser; + +public class NBTDataAttribute implements IconAttribute { + + private final String nbtData; + + public NBTDataAttribute(String nbtData, AttributeErrorHandler errorHandler) throws ParseException { + try { + // Check that NBT syntax is valid before applying it to the icon + MojangsonParser.parse(nbtData); + } catch (MojangsonParseException e) { + throw new ParseException(e.getMessage()); + } + + this.nbtData = nbtData; + } + + @Override + public void apply(InternalConfigurableIcon icon) { + icon.setNBTData(nbtData); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/attribute/NameAttribute.java b/plugin/src/main/java/me/filoghost/chestcommands/attribute/NameAttribute.java new file mode 100644 index 0000000..5b46fc3 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/attribute/NameAttribute.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.attribute; + +import me.filoghost.chestcommands.config.Settings; +import me.filoghost.chestcommands.icon.InternalConfigurableIcon; +import me.filoghost.commons.Colors; + +public class NameAttribute implements IconAttribute { + + private final String name; + + public NameAttribute(String name, AttributeErrorHandler errorHandler) { + this.name = colorName(name); + } + + private String colorName(String name) { + if (!name.isEmpty()) { + return Settings.default_color__name + Colors.addColors(name); + } else { + return name; + } + } + + @Override + public void apply(InternalConfigurableIcon icon) { + icon.setName(name); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/attribute/PositionAttribute.java b/plugin/src/main/java/me/filoghost/chestcommands/attribute/PositionAttribute.java new file mode 100644 index 0000000..4e87842 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/attribute/PositionAttribute.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.attribute; + +import me.filoghost.chestcommands.icon.InternalConfigurableIcon; + +public class PositionAttribute implements IconAttribute { + + private final int position; + + public PositionAttribute(int position, AttributeErrorHandler errorHandler) { + this.position = position; + } + + public int getPosition() { + return position; + } + + @Override + public void apply(InternalConfigurableIcon icon) { + // Position has no effect on the icon itself + } +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/attribute/PriceAttribute.java b/plugin/src/main/java/me/filoghost/chestcommands/attribute/PriceAttribute.java new file mode 100644 index 0000000..17b0e86 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/attribute/PriceAttribute.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.attribute; + +import me.filoghost.chestcommands.icon.InternalConfigurableIcon; +import me.filoghost.chestcommands.logging.Errors; +import me.filoghost.chestcommands.parsing.ParseException; + +public class PriceAttribute implements IconAttribute { + + private final double price; + + public PriceAttribute(double price, AttributeErrorHandler errorHandler) throws ParseException { + if (price < 0) { + throw new ParseException(Errors.Parsing.zeroOrPositive); + } + this.price = price; + } + + @Override + public void apply(InternalConfigurableIcon icon) { + icon.setRequiredMoney(price); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/attribute/RequiredItemsAttribute.java b/plugin/src/main/java/me/filoghost/chestcommands/attribute/RequiredItemsAttribute.java new file mode 100644 index 0000000..a24b49b --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/attribute/RequiredItemsAttribute.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.attribute; + +import me.filoghost.chestcommands.icon.InternalConfigurableIcon; +import me.filoghost.chestcommands.icon.requirement.item.RequiredItem; +import me.filoghost.chestcommands.parsing.ItemStackParser; +import me.filoghost.chestcommands.parsing.ParseException; + +import java.util.ArrayList; +import java.util.List; + +public class RequiredItemsAttribute implements IconAttribute { + + private final List requiredItems; + + public RequiredItemsAttribute(List serializedRequiredItems, AttributeErrorHandler errorHandler) { + requiredItems = new ArrayList<>(); + + for (String serializedItem : serializedRequiredItems) { + try { + ItemStackParser itemReader = new ItemStackParser(serializedItem, true); + itemReader.checkNotAir(); + RequiredItem requiredItem = new RequiredItem(itemReader.getMaterial(), itemReader.getAmount()); + if (itemReader.hasExplicitDurability()) { + requiredItem.setRestrictiveDurability(itemReader.getDurability()); + } + requiredItems.add(requiredItem); + } catch (ParseException e) { + errorHandler.onListElementError(serializedItem, e); + } + } + } + + @Override + public void apply(InternalConfigurableIcon icon) { + icon.setRequiredItems(requiredItems); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/attribute/SkullOwnerAttribute.java b/plugin/src/main/java/me/filoghost/chestcommands/attribute/SkullOwnerAttribute.java new file mode 100644 index 0000000..e756492 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/attribute/SkullOwnerAttribute.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.attribute; + +import me.filoghost.chestcommands.icon.InternalConfigurableIcon; + +public class SkullOwnerAttribute implements IconAttribute { + + private final String skullOwner; + + public SkullOwnerAttribute(String skullOwner, AttributeErrorHandler errorHandler) { + this.skullOwner = skullOwner; + } + + @Override + public void apply(InternalConfigurableIcon icon) { + icon.setSkullOwner(skullOwner); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/attribute/ViewPermissionAttribute.java b/plugin/src/main/java/me/filoghost/chestcommands/attribute/ViewPermissionAttribute.java new file mode 100644 index 0000000..c6262cd --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/attribute/ViewPermissionAttribute.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.attribute; + +import me.filoghost.chestcommands.icon.InternalConfigurableIcon; + +public class ViewPermissionAttribute implements IconAttribute { + + private final String viewPermission; + + public ViewPermissionAttribute(String viewPermission, AttributeErrorHandler errorHandler) { + this.viewPermission = viewPermission; + } + + @Override + public void apply(InternalConfigurableIcon icon) { + icon.setViewPermission(viewPermission); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/command/CommandHandler.java b/plugin/src/main/java/me/filoghost/chestcommands/command/CommandHandler.java new file mode 100644 index 0000000..14899cf --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/command/CommandHandler.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.command; + +import me.filoghost.chestcommands.ChestCommands; +import me.filoghost.chestcommands.Permissions; +import me.filoghost.chestcommands.menu.InternalMenu; +import me.filoghost.chestcommands.menu.MenuManager; +import me.filoghost.chestcommands.util.Utils; +import me.filoghost.commons.command.CommandFramework; +import me.filoghost.commons.command.CommandValidate; +import me.filoghost.commons.logging.ErrorCollector; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.ConsoleCommandSender; +import org.bukkit.entity.Player; + +public class CommandHandler extends CommandFramework { + + private final MenuManager menuManager; + + public CommandHandler(MenuManager menuManager, String label) { + super(label); + this.menuManager = menuManager; + } + + @SuppressWarnings("deprecation") + @Override + public void execute(CommandSender sender, String label, String[] args) { + if (args.length == 0) { + // This info is accessible to anyone. Please don't remove it, remember that Chest Commands is developed for free + sender.sendMessage(ChestCommands.CHAT_PREFIX); + sender.sendMessage(ChatColor.GREEN + "Version: " + ChatColor.GRAY + ChestCommands.getPluginInstance().getDescription().getVersion()); + sender.sendMessage(ChatColor.GREEN + "Developer: " + ChatColor.GRAY + "filoghost"); + sender.sendMessage(ChatColor.GREEN + "Commands: " + ChatColor.GRAY + "/" + label + " help"); + return; + } + + + if (args[0].equalsIgnoreCase("help")) { + checkCommandPermission(sender, "help"); + sender.sendMessage(ChestCommands.CHAT_PREFIX + "Commands:"); + sender.sendMessage(ChatColor.WHITE + "/" + label + " reload" + ChatColor.GRAY + " - Reloads the plugin."); + sender.sendMessage(ChatColor.WHITE + "/" + label + " errors" + ChatColor.GRAY + " - Displays the last load errors on the console."); + sender.sendMessage(ChatColor.WHITE + "/" + label + " list" + ChatColor.GRAY + " - Lists the loaded menus."); + sender.sendMessage(ChatColor.WHITE + "/" + label + " open [player]" + ChatColor.GRAY + " - Opens a menu for a player."); + return; + } + + + if (args[0].equalsIgnoreCase("errors")) { + checkCommandPermission(sender, "errors"); + ErrorCollector errorCollector = ChestCommands.getLastLoadErrors(); + + if (errorCollector.hasErrors()) { + errorCollector.logToConsole(); + sender.sendMessage(ChestCommands.CHAT_PREFIX + ChatColor.RED + "Last time the plugin loaded, " + errorCollector.getErrorsCount() + " error(s) were found."); + if (!(sender instanceof ConsoleCommandSender)) { + sender.sendMessage(ChestCommands.CHAT_PREFIX + ChatColor.RED + "Errors were printed on the console."); + } + } else { + sender.sendMessage(ChestCommands.CHAT_PREFIX + ChatColor.GREEN + "Last plugin load was successful, no errors logged."); + } + return; + } + + + if (args[0].equalsIgnoreCase("reload")) { + checkCommandPermission(sender, "reload"); + + ChestCommands.closeAllMenus(); + + ErrorCollector errorCollector = ChestCommands.load(); + + if (!errorCollector.hasErrors()) { + sender.sendMessage(ChestCommands.CHAT_PREFIX + "Plugin reloaded."); + } else { + errorCollector.logToConsole(); + sender.sendMessage(ChestCommands.CHAT_PREFIX + ChatColor.RED + "Plugin reloaded with " + errorCollector.getErrorsCount() + " error(s)."); + if (!(sender instanceof ConsoleCommandSender)) { + sender.sendMessage(ChestCommands.CHAT_PREFIX + ChatColor.RED + "Please check the console."); + } + } + return; + } + + + if (args[0].equalsIgnoreCase("open")) { + checkCommandPermission(sender, "open"); + CommandValidate.minLength(args, 2, "Usage: /" + label + " open [player]"); + + Player target; + + if (sender instanceof Player) { + if (args.length > 2) { + checkCommandPermission(sender, "open.others"); + target = Bukkit.getPlayerExact(args[2]); + } else { + target = (Player) sender; + } + } else { + CommandValidate.minLength(args, 3, "You must specify a player from the console."); + target = Bukkit.getPlayerExact(args[2]); + } + + CommandValidate.notNull(target, "That player is not online."); + + String menuName = Utils.addYamlExtension(args[1]); + InternalMenu menu = menuManager.getMenuByFileName(menuName); + CommandValidate.notNull(menu, "The menu \"" + menuName + "\" was not found."); + + if (!sender.hasPermission(menu.getOpenPermission())) { + menu.sendNoOpenPermissionMessage(sender); + return; + } + + if (sender.getName().equalsIgnoreCase(target.getName())) { + sender.sendMessage(ChatColor.GREEN + "Opening the menu " + menuName + "."); + } else { + sender.sendMessage(ChatColor.GREEN + "Opening the menu " + menuName + " to " + target.getName() + "."); + } + + menu.open(target); + return; + } + + + if (args[0].equalsIgnoreCase("list")) { + checkCommandPermission(sender, "list"); + sender.sendMessage(ChestCommands.CHAT_PREFIX + "Loaded menus:"); + for (String file : menuManager.getMenuFileNames()) { + sender.sendMessage(ChatColor.GRAY + "- " + ChatColor.WHITE + file); + } + + return; + } + + sender.sendMessage(ChatColor.RED + "Unknown sub-command \"" + args[0] + "\"."); + } + + private void checkCommandPermission(CommandSender sender, String commandPermission) { + CommandValidate.isTrue(sender.hasPermission(Permissions.COMMAND_PREFIX + commandPermission), "You don't have permission."); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/config/ConfigManager.java b/plugin/src/main/java/me/filoghost/chestcommands/config/ConfigManager.java new file mode 100644 index 0000000..77642c4 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/config/ConfigManager.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.config; + +import me.filoghost.chestcommands.logging.Errors; +import me.filoghost.chestcommands.parsing.menu.LoadedMenu; +import me.filoghost.chestcommands.parsing.menu.MenuParser; +import me.filoghost.commons.Preconditions; +import me.filoghost.commons.config.BaseConfigManager; +import me.filoghost.commons.config.Config; +import me.filoghost.commons.config.ConfigLoader; +import me.filoghost.commons.config.exception.ConfigException; +import me.filoghost.commons.config.mapped.MappedConfigLoader; +import me.filoghost.commons.logging.ErrorCollector; + +import java.io.IOException; +import java.nio.file.FileVisitOption; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class ConfigManager extends BaseConfigManager { + + private final MappedConfigLoader settingsConfigLoader; + private final ConfigLoader placeholdersConfigLoader; + private final MappedConfigLoader langConfigLoader; + + public ConfigManager(Path rootDataFolder) { + super(rootDataFolder); + + settingsConfigLoader = getMappedConfigLoader("config.yml", Settings::new); + placeholdersConfigLoader = getConfigLoader("custom-placeholders.yml"); + langConfigLoader = getMappedConfigLoader("lang.yml", Lang::new); + } + + public void tryLoadSettings(ErrorCollector errorCollector) { + try { + settingsConfigLoader.init(); + } catch (ConfigException e) { + logConfigInitException(errorCollector, settingsConfigLoader.getFile(), e); + } + } + + public void tryLoadLang(ErrorCollector errorCollector) { + try { + langConfigLoader.init(); + } catch (ConfigException e) { + logConfigInitException(errorCollector, langConfigLoader.getFile(), e); + } + } + + public CustomPlaceholders tryLoadCustomPlaceholders(ErrorCollector errorCollector) { + CustomPlaceholders placeholders = new CustomPlaceholders(); + + try { + Config placeholdersConfig = placeholdersConfigLoader.init(); + placeholders.load(placeholdersConfig, errorCollector); + } catch (ConfigException t) { + logConfigInitException(errorCollector, placeholdersConfigLoader.getFile(), t); + } + + return placeholders; + } + + public void tryCreateDefault(ErrorCollector errorCollector, ConfigLoader configLoader) { + try { + configLoader.createDefault(); + } catch (ConfigException e) { + logConfigInitException(errorCollector, configLoader.getFile(), e); + } + } + + public Path getMenusFolder() { + return rootDataFolder.resolve("menu"); + } + + public List getMenuFiles() throws IOException { + Preconditions.checkState(Files.isDirectory(getMenusFolder()), "menus folder doesn't exist"); + + try (Stream paths = Files.walk(getMenusFolder(), FileVisitOption.FOLLOW_LINKS)) { + return paths.filter(this::isYamlFile).collect(Collectors.toList()); + } + } + + private void logConfigInitException(ErrorCollector errorCollector, Path file, ConfigException e) { + errorCollector.add(e, Errors.Config.initException(file)); + } + + public List tryLoadMenus(ErrorCollector errorCollector) { + List loadedMenus = new ArrayList<>(); + List menuFiles; + + try { + menuFiles = getMenuFiles(); + } catch (IOException e) { + errorCollector.add(e, Errors.Config.menuListIOException(getMenusFolder())); + return Collections.emptyList(); + } + + for (Path menuFile : menuFiles) { + ConfigLoader menuConfigLoader = new ConfigLoader(rootDataFolder, menuFile); + + try { + Config menuConfig = menuConfigLoader.load(); + loadedMenus.add(MenuParser.loadMenu(menuConfig, errorCollector)); + } catch (ConfigException e) { + logConfigInitException(errorCollector, menuConfigLoader.getFile(), e); + } + } + + return loadedMenus; + } + + private boolean isYamlFile(Path path) { + return Files.isRegularFile(path) && path.getFileName().toString().toLowerCase().endsWith(".yml"); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/config/CustomPlaceholders.java b/plugin/src/main/java/me/filoghost/chestcommands/config/CustomPlaceholders.java new file mode 100644 index 0000000..5c87e1f --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/config/CustomPlaceholders.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.config; + +import me.filoghost.chestcommands.logging.Errors; +import me.filoghost.chestcommands.placeholder.StaticPlaceholder; +import me.filoghost.commons.Colors; +import me.filoghost.commons.config.Config; +import me.filoghost.commons.config.ConfigSection; +import me.filoghost.commons.logging.ErrorCollector; + +import java.util.ArrayList; +import java.util.List; + +public class CustomPlaceholders { + + private final List placeholders = new ArrayList<>(); + + public void load(Config config, ErrorCollector errorCollector) { + placeholders.clear(); + + ConfigSection placeholdersSection = config.getConfigSection("placeholders"); + if (placeholdersSection == null) { + return; + } + + for (String placeholder : placeholdersSection.getKeys()) { + String replacement = Colors.addColors(placeholdersSection.getString(placeholder)); + if (replacement == null) { + return; + } + + if (placeholder.length() == 0) { + errorCollector.add(Errors.Config.emptyPlaceholder(config.getSourceFile())); + continue; + } + + if (placeholder.length() > 100) { + errorCollector.add(Errors.Config.tooLongPlaceholder(config.getSourceFile(), placeholder)); + continue; + } + + placeholders.add(new StaticPlaceholder(placeholder, replacement)); + } + } + + public List getPlaceholders() { + return placeholders; + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/config/Lang.java b/plugin/src/main/java/me/filoghost/chestcommands/config/Lang.java new file mode 100644 index 0000000..44a635f --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/config/Lang.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.config; + +import me.filoghost.chestcommands.logging.Errors; +import me.filoghost.commons.config.mapped.IncludeStatic; +import me.filoghost.commons.config.mapped.MappedConfig; +import me.filoghost.commons.config.mapped.modifier.ChatColors; + +@ChatColors +@IncludeStatic +public class Lang extends MappedConfig { + + public static String no_open_permission = "&cYou don't have permission &e{permission} &cto use this menu."; + public static String default_no_icon_permission = "&cYou don't have permission for this icon."; + public static String no_required_item = "&cYou must have &e{amount}x {material} &c(durability: {durability}) for this."; + public static String no_money = "&cYou need {money}$ for this."; + public static String no_exp = "&cYou need {levels} XP levels for this."; + public static String menu_not_found = "&cMenu not found! " + Errors.User.notifyStaffRequest; + public static String any = "any"; // Used in no_required_item when durability is not restrictive + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/config/Settings.java b/plugin/src/main/java/me/filoghost/chestcommands/config/Settings.java new file mode 100644 index 0000000..86fc1c5 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/config/Settings.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.config; + +import me.filoghost.commons.config.mapped.IncludeStatic; +import me.filoghost.commons.config.mapped.MappedConfig; +import me.filoghost.commons.config.mapped.modifier.ChatColors; + +@ChatColors +@IncludeStatic +public class Settings extends MappedConfig { + + public static String default_color__name = "&f"; + public static String default_color__lore = "&7"; + public static boolean update_notifications = true; + public static int anti_click_spam_delay = 200; + + public Settings() { + setHeader( + "ChestCommands main configuration file.", + "Documentation: https://filoghost.me/docs/chest-commands"); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/hook/BarAPIHook.java b/plugin/src/main/java/me/filoghost/chestcommands/hook/BarAPIHook.java new file mode 100644 index 0000000..70739d7 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/hook/BarAPIHook.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.hook; + +import me.confuser.barapi.BarAPI; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +public enum BarAPIHook implements PluginHook { + + INSTANCE; + + private boolean enabled; + + @Override + public void setup() { + enabled = Bukkit.getPluginManager().getPlugin("BarAPI") != null; + } + + @Override + public boolean isEnabled() { + return enabled; + } + + @SuppressWarnings("deprecation") + public static void setMessage(Player player, String message, int seconds) { + INSTANCE.checkEnabledState(); + + BarAPI.setMessage(player, message, seconds); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/hook/BungeeCordHook.java b/plugin/src/main/java/me/filoghost/chestcommands/hook/BungeeCordHook.java new file mode 100644 index 0000000..07214af --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/hook/BungeeCordHook.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.hook; + +import me.filoghost.chestcommands.ChestCommands; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.entity.Player; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +public enum BungeeCordHook implements PluginHook { + + INSTANCE; + + @Override + public void setup() { + if (!Bukkit.getMessenger().isOutgoingChannelRegistered(ChestCommands.getPluginInstance(), "BungeeCord")) { + Bukkit.getMessenger().registerOutgoingPluginChannel(ChestCommands.getPluginInstance(), "BungeeCord"); + } + } + + @Override + public boolean isEnabled() { + return true; + } + + public static void connect(Player player, String server) { + INSTANCE.checkEnabledState(); + + if (server.length() == 0) { + player.sendMessage(ChatColor.RED + "Target server was an empty string, cannot connect to it."); + return; + } + + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + DataOutputStream dataOutputStream = new DataOutputStream(byteArrayOutputStream); + + try { + dataOutputStream.writeUTF("Connect"); + dataOutputStream.writeUTF(server); // Target Server + } catch (IOException ex) { + throw new AssertionError(); + } + + player.sendPluginMessage(ChestCommands.getPluginInstance(), "BungeeCord", byteArrayOutputStream.toByteArray()); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/hook/PlaceholderAPIHook.java b/plugin/src/main/java/me/filoghost/chestcommands/hook/PlaceholderAPIHook.java new file mode 100644 index 0000000..2712847 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/hook/PlaceholderAPIHook.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.hook; + +import me.clip.placeholderapi.PlaceholderAPI; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +public enum PlaceholderAPIHook implements PluginHook { + + INSTANCE; + + private boolean enabled; + + @Override + public void setup() { + enabled = Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null; + } + + @Override + public boolean isEnabled() { + return enabled; + } + + public static boolean hasPlaceholders(String message) { + INSTANCE.checkEnabledState(); + + return PlaceholderAPI.containsPlaceholders(message); + } + + public static String setPlaceholders(String message, Player viewer) { + INSTANCE.checkEnabledState(); + + return PlaceholderAPI.setPlaceholders(viewer, message); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/hook/PluginHook.java b/plugin/src/main/java/me/filoghost/chestcommands/hook/PluginHook.java new file mode 100644 index 0000000..0d336ed --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/hook/PluginHook.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.hook; + +import me.filoghost.commons.Preconditions; + +public interface PluginHook { + + + void setup(); + + boolean isEnabled(); + + default void checkEnabledState() { + Preconditions.checkState(isEnabled(), "Plugin hook " + getClass().getSimpleName() + " is not enabled"); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/hook/VaultEconomyHook.java b/plugin/src/main/java/me/filoghost/chestcommands/hook/VaultEconomyHook.java new file mode 100644 index 0000000..fbd48b9 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/hook/VaultEconomyHook.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.hook; + +import me.filoghost.commons.Preconditions; +import net.milkbowl.vault.economy.Economy; +import net.milkbowl.vault.economy.EconomyResponse; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.plugin.RegisteredServiceProvider; + +public enum VaultEconomyHook implements PluginHook { + + INSTANCE; + + private Economy economy; + + @Override + public void setup() { + economy = null; + + if (Bukkit.getPluginManager().getPlugin("Vault") == null) { + return; + } + + RegisteredServiceProvider economyServiceProvider = Bukkit.getServicesManager().getRegistration(Economy.class); + if (economyServiceProvider == null) { + return; + } + + economy = economyServiceProvider.getProvider(); + } + + @Override + public boolean isEnabled() { + return economy != null; + } + + public static double getMoney(Player player) { + INSTANCE.checkEnabledState(); + return INSTANCE.economy.getBalance(player, player.getWorld().getName()); + } + + public static boolean hasMoney(Player player, double minimum) { + INSTANCE.checkEnabledState(); + checkPositiveAmount(minimum); + + double balance = INSTANCE.economy.getBalance(player, player.getWorld().getName()); + return balance >= minimum; + } + + /** + * @return true if the operation was successful. + */ + public static boolean takeMoney(Player player, double amount) { + INSTANCE.checkEnabledState(); + checkPositiveAmount(amount); + + EconomyResponse response = INSTANCE.economy.withdrawPlayer(player, player.getWorld().getName(), amount); + return response.transactionSuccess(); + } + + public static boolean giveMoney(Player player, double amount) { + INSTANCE.checkEnabledState(); + checkPositiveAmount(amount); + + EconomyResponse response = INSTANCE.economy.depositPlayer(player, player.getWorld().getName(), amount); + return response.transactionSuccess(); + } + + private static void checkPositiveAmount(double amount) { + Preconditions.checkArgument(amount >= 0.0, "amount cannot be negative"); + } + + public static String formatMoney(double amount) { + if (INSTANCE.economy != null) { + return INSTANCE.economy.format(amount); + } else { + return Double.toString(amount); + } + } +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/icon/APIConfigurableIcon.java b/plugin/src/main/java/me/filoghost/chestcommands/icon/APIConfigurableIcon.java new file mode 100644 index 0000000..8bf5295 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/icon/APIConfigurableIcon.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.icon; + +import me.filoghost.chestcommands.api.ClickHandler; +import me.filoghost.chestcommands.api.ConfigurableIcon; +import org.bukkit.Material; + +public class APIConfigurableIcon extends BaseConfigurableIcon implements ConfigurableIcon { + + private ClickHandler clickHandler; + + public APIConfigurableIcon(Material material) { + super(material); + } + + @Override + public void setClickHandler(ClickHandler clickHandler) { + this.clickHandler = clickHandler; + } + + @Override + public ClickHandler getClickHandler() { + return clickHandler; + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/icon/APIStaticIcon.java b/plugin/src/main/java/me/filoghost/chestcommands/icon/APIStaticIcon.java new file mode 100644 index 0000000..e8ec803 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/icon/APIStaticIcon.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.icon; + +import me.filoghost.chestcommands.api.ClickHandler; +import me.filoghost.chestcommands.api.StaticIcon; +import me.filoghost.commons.Preconditions; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +public class APIStaticIcon implements StaticIcon { + + private ItemStack itemStack; + private ClickHandler clickHandler; + + public APIStaticIcon(ItemStack itemStack) { + Preconditions.notNull(itemStack, "itemStack"); + this.itemStack = itemStack; + } + + @Override + public ItemStack getItemStack() { + return itemStack; + } + + @Override + public void setItemStack(ItemStack itemStack) { + Preconditions.notNull(itemStack, "itemStack"); + this.itemStack = itemStack; + } + + @Override + public ClickHandler getClickHandler() { + return clickHandler; + } + + @Override + public void setClickHandler(ClickHandler clickHandler) { + this.clickHandler = clickHandler; + } + + @Override + public ItemStack render(Player viewer) { + return itemStack; + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/icon/BaseConfigurableIcon.java b/plugin/src/main/java/me/filoghost/chestcommands/icon/BaseConfigurableIcon.java new file mode 100644 index 0000000..6cd9b95 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/icon/BaseConfigurableIcon.java @@ -0,0 +1,325 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.icon; + +import me.filoghost.chestcommands.api.Icon; +import me.filoghost.chestcommands.placeholder.PlaceholderString; +import me.filoghost.chestcommands.placeholder.PlaceholderStringList; +import me.filoghost.chestcommands.util.nbt.parser.MojangsonParseException; +import me.filoghost.chestcommands.util.nbt.parser.MojangsonParser; +import me.filoghost.commons.Preconditions; +import me.filoghost.commons.collection.CollectionUtils; +import me.filoghost.commons.logging.Log; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Color; +import org.bukkit.DyeColor; +import org.bukkit.Material; +import org.bukkit.block.banner.Pattern; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.BannerMeta; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.inventory.meta.LeatherArmorMeta; +import org.bukkit.inventory.meta.SkullMeta; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public abstract class BaseConfigurableIcon implements Icon { + + private Material material; + private int amount; + private short durability; + + private String nbtData; + private PlaceholderString name; + private PlaceholderStringList lore; + private Map enchantments; + private Color leatherColor; + private PlaceholderString skullOwner; + private DyeColor bannerColor; + private List bannerPatterns; + private boolean placeholdersEnabled; + + protected ItemStack cachedRendering; // Cache the rendered item when possible and if state hasn't changed + + public BaseConfigurableIcon(Material material) { + this.material = material; + this.amount = 1; + } + + protected boolean shouldCacheRendering() { + if (placeholdersEnabled && hasDynamicPlaceholders()) { + return false; + } else { + return true; + } + } + + private boolean hasDynamicPlaceholders() { + return (name != null && name.hasDynamicPlaceholders()) + || (lore != null && lore.hasDynamicPlaceholders()) + || (skullOwner != null && skullOwner.hasDynamicPlaceholders()); + } + + public void setMaterial(Material material) { + this.material = material; + cachedRendering = null; + } + + public Material getMaterial() { + return material; + } + + public void setAmount(int amount) { + Preconditions.checkArgument(amount > 0, "amount must be greater than 0"); + this.amount = Math.min(amount, 127); + cachedRendering = null; + } + + public int getAmount() { + return amount; + } + + public void setDurability(short durability) { + Preconditions.checkArgument(durability >= 0, "durability must be 0 or greater"); + this.durability = durability; + cachedRendering = null; + } + + public short getDurability() { + return durability; + } + + public void setNBTData(String nbtData) { + if (nbtData != null) { + try { + MojangsonParser.parse(nbtData); + } catch (MojangsonParseException e) { + throw new IllegalArgumentException("invalid nbtData", e); + } + } + this.nbtData = nbtData; + cachedRendering = null; + } + + public String getNBTData() { + return nbtData; + } + + public void setName(String name) { + this.name = PlaceholderString.of(name); + cachedRendering = null; + } + + public String getName() { + if (name != null) { + return name.getOriginalValue(); + } else { + return null; + } + } + + public void setLore(String... lore) { + if (lore != null) { + setLore(Arrays.asList(lore)); + } + } + + public void setLore(List lore) { + if (lore != null) { + this.lore = new PlaceholderStringList(CollectionUtils.replaceNulls(lore, "")); + } else { + this.lore = null; + } + cachedRendering = null; + } + + public List getLore() { + if (lore != null) { + return new ArrayList<>(lore.getOriginalValue()); + } else { + return null; + } + } + + public void setEnchantments(Map enchantments) { + this.enchantments = CollectionUtils.copy(enchantments); + cachedRendering = null; + } + + public Map getEnchantments() { + return CollectionUtils.copy(enchantments); + } + + public void addEnchantment(Enchantment enchantment) { + addEnchantment(enchantment, 1); + } + + public void addEnchantment(Enchantment enchantment, Integer level) { + if (enchantments == null) { + enchantments = new HashMap<>(); + } + enchantments.put(enchantment, level); + cachedRendering = null; + } + + public void removeEnchantment(Enchantment enchantment) { + if (enchantments == null) { + return; + } + enchantments.remove(enchantment); + cachedRendering = null; + } + + public Color getLeatherColor() { + return leatherColor; + } + + public void setLeatherColor(Color leatherColor) { + this.leatherColor = leatherColor; + cachedRendering = null; + } + + public String getSkullOwner() { + if (skullOwner != null) { + return skullOwner.getOriginalValue(); + } else { + return null; + } + } + + public void setSkullOwner(String skullOwner) { + this.skullOwner = PlaceholderString.of(skullOwner); + cachedRendering = null; + } + + public DyeColor getBannerColor() { + return bannerColor; + } + + public void setBannerColor(DyeColor bannerColor) { + this.bannerColor = bannerColor; + cachedRendering = null; + } + + public List getBannerPatterns() { + return CollectionUtils.copy(bannerPatterns); + } + + public void setBannerPatterns(List bannerPatterns) { + this.bannerPatterns = CollectionUtils.copy(bannerPatterns); + cachedRendering = null; + } + + public void setPlaceholdersEnabled(boolean placeholdersEnabled) { + this.placeholdersEnabled = placeholdersEnabled; + cachedRendering = null; + } + + public String renderName(Player viewer) { + if (name == null) { + return null; + } + if (!placeholdersEnabled) { + return name.getOriginalValue(); + } + + String name = this.name.getValue(viewer); + + if (name.isEmpty()) { + // Add a color to display the name empty + return ChatColor.WHITE.toString(); + } else { + return name; + } + } + + public List renderLore(Player viewer) { + if (lore == null) { + return null; + } + if (!placeholdersEnabled) { + return lore.getOriginalValue(); + } + + return lore.getValue(viewer); + } + + @Override + @SuppressWarnings("deprecation") + public ItemStack render(Player viewer) { + if (shouldCacheRendering() && cachedRendering != null) { + // Performance: return a cached item + return cachedRendering; + } + + ItemStack itemStack = new ItemStack(material, amount, durability); + + // First try to apply NBT data + if (nbtData != null) { + try { + // Note: this method should not throw any exception. It should log directly to the console + Bukkit.getUnsafe().modifyItemStack(itemStack, nbtData); + } catch (Throwable t) { + this.nbtData = null; + Log.warning("Could not apply NBT data to an item.", t); + } + } + + // Then apply data from config nodes, overwriting NBT data if there are conflicting values + ItemMeta itemMeta = itemStack.getItemMeta(); + + if (itemMeta != null) { + itemMeta.setDisplayName(renderName(viewer)); + itemMeta.setLore(renderLore(viewer)); + + if (leatherColor != null && itemMeta instanceof LeatherArmorMeta) { + ((LeatherArmorMeta) itemMeta).setColor(leatherColor); + } + + if (skullOwner != null && itemMeta instanceof SkullMeta) { + String skullOwner = this.skullOwner.getValue(viewer); + ((SkullMeta) itemMeta).setOwner(skullOwner); + } + + if (itemMeta instanceof BannerMeta) { + BannerMeta bannerMeta = (BannerMeta) itemMeta; + if (bannerColor != null) { + bannerMeta.setBaseColor(bannerColor); + } + if (bannerPatterns != null) { + ((BannerMeta) itemMeta).setPatterns(bannerPatterns); + } + } + + // Hide all text details (damage, enchantments, potions, etc,) + if (itemMeta.getItemFlags().isEmpty()) { + itemMeta.addItemFlags(ItemFlag.values()); + } + + itemStack.setItemMeta(itemMeta); + } + + if (enchantments != null) { + enchantments.forEach(itemStack::addUnsafeEnchantment); + } + + + if (shouldCacheRendering()) { + cachedRendering = itemStack; + } + + return itemStack; + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/icon/IconPermission.java b/plugin/src/main/java/me/filoghost/chestcommands/icon/IconPermission.java new file mode 100644 index 0000000..35e6d00 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/icon/IconPermission.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.icon; + +import me.filoghost.commons.Strings; +import org.bukkit.entity.Player; + +public class IconPermission { + + private final String permission; + private final boolean negated; + + public IconPermission(String permission) { + if (permission != null) { + permission = permission.trim(); + } + + if (Strings.isEmpty(permission)) { + this.permission = null; + negated = false; + } else { + if (permission.startsWith("-")) { + this.permission = permission.substring(1); + negated = true; + } else { + this.permission = permission; + negated = false; + } + } + } + + private boolean hasPermission(Player player) { + if (isEmpty()) { + return true; + } + + if (negated) { + return !player.hasPermission(permission); + } else { + return player.hasPermission(permission); + } + } + + public boolean isEmpty() { + return this.permission == null; + } + + public static boolean hasPermission(Player player, IconPermission permission) { + return permission == null || permission.hasPermission(player); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/icon/InternalConfigurableIcon.java b/plugin/src/main/java/me/filoghost/chestcommands/icon/InternalConfigurableIcon.java new file mode 100644 index 0000000..e925b27 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/icon/InternalConfigurableIcon.java @@ -0,0 +1,192 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.icon; + +import com.google.common.collect.ImmutableList; +import me.filoghost.chestcommands.action.Action; +import me.filoghost.chestcommands.action.OpenMenuAction; +import me.filoghost.chestcommands.api.ClickResult; +import me.filoghost.chestcommands.api.MenuView; +import me.filoghost.chestcommands.config.Lang; +import me.filoghost.chestcommands.icon.requirement.RequiredExpLevel; +import me.filoghost.chestcommands.icon.requirement.RequiredMoney; +import me.filoghost.chestcommands.icon.requirement.Requirement; +import me.filoghost.chestcommands.icon.requirement.item.RequiredItem; +import me.filoghost.chestcommands.icon.requirement.item.RequiredItems; +import me.filoghost.commons.Preconditions; +import me.filoghost.commons.collection.CollectionUtils; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.List; + +public class InternalConfigurableIcon extends BaseConfigurableIcon implements RefreshableIcon { + + private IconPermission viewPermission; + private IconPermission clickPermission; + private String noClickPermissionMessage; + + private RequiredMoney requiredMoney; + private RequiredExpLevel requiredExpLevel; + private RequiredItems requiredItems; + + private ImmutableList clickActions; + private ClickResult clickResult; + + public InternalConfigurableIcon(Material material) { + super(material); + setPlaceholdersEnabled(true); + this.clickResult = ClickResult.CLOSE; + } + + public boolean canViewIcon(Player player) { + return IconPermission.hasPermission(player, viewPermission); + } + + public boolean hasViewPermission() { + return viewPermission != null && !viewPermission.isEmpty(); + } + + public void setClickPermission(String permission) { + this.clickPermission = new IconPermission(permission); + } + + public void setNoClickPermissionMessage(String noClickPermissionMessage) { + this.noClickPermissionMessage = noClickPermissionMessage; + } + + public void setViewPermission(String viewPermission) { + this.viewPermission = new IconPermission(viewPermission); + } + + public void setRequiredMoney(double requiredMoney) { + if (requiredMoney > 0.0) { + this.requiredMoney = new RequiredMoney(requiredMoney); + } else { + this.requiredMoney = null; + } + } + + public void setRequiredExpLevel(int requiredLevels) { + if (requiredLevels > 0) { + this.requiredExpLevel = new RequiredExpLevel(requiredLevels); + } else { + this.requiredExpLevel = null; + } + } + + public void setRequiredItems(List requiredItems) { + if (requiredItems != null) { + this.requiredItems = new RequiredItems(requiredItems); + } else { + this.requiredItems = null; + } + } + + public void setClickActions(List clickActions) { + this.clickActions = CollectionUtils.immutableCopy(clickActions); + } + + + @Override + public ItemStack render(Player viewer) { + if (canViewIcon(viewer)) { + return super.render(viewer); + } else { + return null; + } + } + + @Override + protected boolean shouldCacheRendering() { + return super.shouldCacheRendering() && !hasViewPermission(); + } + + + public void setClickResult(ClickResult clickResult) { + Preconditions.notNull(clickResult, "clickResult"); + this.clickResult = clickResult; + } + + @Override + public ClickResult onClick(MenuView menuView, Player player) { + if (!IconPermission.hasPermission(player, viewPermission)) { + return ClickResult.KEEP_OPEN; + } + + if (!IconPermission.hasPermission(player, clickPermission)) { + if (noClickPermissionMessage != null) { + player.sendMessage(noClickPermissionMessage); + } else { + player.sendMessage(Lang.default_no_icon_permission); + } + return clickResult; + } + + // Check all the requirements + Requirement[] requirements = {requiredMoney, requiredExpLevel, requiredItems}; + boolean hasAllRequirements = Requirement.hasAllCosts(player, requirements); + if (!hasAllRequirements) { + return clickResult; + } + + // If all requirements are satisfied, take their cost + boolean takenAllCosts = Requirement.takeAllCosts(player, requirements); + if (!takenAllCosts) { + return clickResult; + } + + boolean hasOpenMenuAction = false; + + if (clickActions != null) { + for (Action action : clickActions) { + action.execute(player); + + if (action instanceof OpenMenuAction) { + hasOpenMenuAction = true; + } + } + } + + // Update the menu after taking requirement costs and executing all actions + menuView.refresh(); + + // Force menu to stay open if actions open another menu + if (hasOpenMenuAction) { + return ClickResult.KEEP_OPEN; + } else { + return clickResult; + } + } + + @Override + public ItemStack updateRendering(Player viewer, ItemStack currentRendering) { + if (currentRendering != null && shouldCacheRendering()) { + // Internal icons do not change, no need to update if the item is already rendered + return currentRendering; + } + + if (!canViewIcon(viewer)) { + // Hide the current item + return null; + } + + if (currentRendering == null) { + // Render item normally + return render(viewer); + } else { + // Internal icons are loaded and then never change, we can safely update only name and lore (for performance) + ItemMeta meta = currentRendering.getItemMeta(); + meta.setDisplayName(renderName(viewer)); + meta.setLore(renderLore(viewer)); + currentRendering.setItemMeta(meta); + return currentRendering; + } + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/icon/RefreshableIcon.java b/plugin/src/main/java/me/filoghost/chestcommands/icon/RefreshableIcon.java new file mode 100644 index 0000000..81d8a04 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/icon/RefreshableIcon.java @@ -0,0 +1,15 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.icon; + +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +public interface RefreshableIcon { + + ItemStack updateRendering(Player viewer, ItemStack currentRendering); + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/icon/requirement/RequiredExpLevel.java b/plugin/src/main/java/me/filoghost/chestcommands/icon/requirement/RequiredExpLevel.java new file mode 100644 index 0000000..e2b2d0c --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/icon/requirement/RequiredExpLevel.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.icon.requirement; + +import com.google.common.base.Preconditions; +import me.filoghost.chestcommands.config.Lang; +import org.bukkit.entity.Player; + +public class RequiredExpLevel implements Requirement { + + private final int levels; + + public RequiredExpLevel(int levels) { + Preconditions.checkArgument(levels > 0, "levels must be positive"); + this.levels = levels; + } + + @Override + public boolean hasCost(Player player) { + if (player.getLevel() < levels) { + player.sendMessage(Lang.no_exp.replace("{levels}", Integer.toString(levels))); + return false; + } + + return true; + } + + @Override + public boolean takeCost(Player player) { + int newLevel = player.getLevel() - levels; + if (newLevel < 0) { + newLevel = 0; + } + + player.setLevel(newLevel); + return true; + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/icon/requirement/RequiredMoney.java b/plugin/src/main/java/me/filoghost/chestcommands/icon/requirement/RequiredMoney.java new file mode 100644 index 0000000..7a60e5f --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/icon/requirement/RequiredMoney.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.icon.requirement; + +import com.google.common.base.Preconditions; +import me.filoghost.chestcommands.config.Lang; +import me.filoghost.chestcommands.hook.VaultEconomyHook; +import me.filoghost.chestcommands.logging.Errors; +import org.bukkit.entity.Player; + +public class RequiredMoney implements Requirement { + + private final double moneyAmount; + + public RequiredMoney(double moneyAmount) { + Preconditions.checkArgument(moneyAmount > 0.0, "money amount must be positive"); + this.moneyAmount = moneyAmount; + } + + @Override + public boolean hasCost(Player player) { + if (!VaultEconomyHook.INSTANCE.isEnabled()) { + player.sendMessage(Errors.User.configurationError( + "the item has a price, but Vault with a compatible economy plugin was not found. " + + "For security, the action has been blocked")); + return false; + } + + if (!VaultEconomyHook.hasMoney(player, moneyAmount)) { + player.sendMessage(Lang.no_money.replace("{money}", VaultEconomyHook.formatMoney(moneyAmount))); + return false; + } + + return true; + } + + @Override + public boolean takeCost(Player player) { + boolean success = VaultEconomyHook.takeMoney(player, moneyAmount); + + if (!success) { + player.sendMessage(Errors.User.configurationError("a money transaction couldn't be executed")); + } + + return success; + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/icon/requirement/Requirement.java b/plugin/src/main/java/me/filoghost/chestcommands/icon/requirement/Requirement.java new file mode 100644 index 0000000..1915e16 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/icon/requirement/Requirement.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.icon.requirement; + +import org.bukkit.entity.Player; + +public interface Requirement { + + boolean hasCost(Player player); + + boolean takeCost(Player player); + + static boolean hasAllCosts(Player player, Requirement... requirements) { + for (Requirement requirement : requirements) { + if (requirement != null && !requirement.hasCost(player)) { + return false; + } + } + + return true; + } + + static boolean takeAllCosts(Player player, Requirement... requirements) { + for (Requirement requirement : requirements) { + if (requirement != null) { + boolean success = requirement.takeCost(player); + if (!success) { + return false; + } + } + } + + return true; + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/icon/requirement/item/InventoryTakeHelper.java b/plugin/src/main/java/me/filoghost/chestcommands/icon/requirement/item/InventoryTakeHelper.java new file mode 100644 index 0000000..71ec463 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/icon/requirement/item/InventoryTakeHelper.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.icon.requirement.item; + +import me.filoghost.commons.MaterialsHelper; +import me.filoghost.commons.Preconditions; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +public class InventoryTakeHelper { + + private final PlayerInventory inventory; + private final List remainingItems; + + private boolean success; + + public InventoryTakeHelper(PlayerInventory inventory) { + this.inventory = inventory; + this.remainingItems = new ArrayList<>(); + + for (int slotIndex = 0; slotIndex < inventory.getSize(); slotIndex++) { + ItemStack item = inventory.getItem(slotIndex); + if (item != null && !MaterialsHelper.isAir(item.getType())) { + remainingItems.add(new RemainingItem(slotIndex, item)); + } + } + } + + public boolean prepareTakeItems(List requiredItems) { + List missingItems = new ArrayList<>(); + + // Sort required items: check required items with a restrictive durability first + List sortedRequiredItems = requiredItems.stream() + .sorted(Comparator.comparing(RequiredItem::hasRestrictiveDurability).reversed()) + .collect(Collectors.toList()); + + for (RequiredItem requiredItem : sortedRequiredItems) { + int remainingRequiredAmount = requiredItem.getAmount(); + + for (RemainingItem remainingItem : remainingItems) { + if (remainingItem.getAmount() > 0 && requiredItem.isMatchingType(remainingItem)) { + int takenAmount = remainingItem.subtract(remainingRequiredAmount); + remainingRequiredAmount -= takenAmount; + if (remainingRequiredAmount == 0) { + break; + } + } + } + + // Couldn't take the required amount of an item + if (remainingRequiredAmount > 0) { + missingItems.add(requiredItem); + } + } + + success = missingItems.isEmpty(); + return success; + } + + public void applyTakeItems() { + Preconditions.checkState(success, "items take preparation was not run or successful"); + + for (RemainingItem remainingItem : remainingItems) { + int slotIndex = remainingItem.getSlotIndex(); + ItemStack inventoryItem = inventory.getItem(slotIndex); + if (remainingItem.getAmount() != inventoryItem.getAmount()) { + if (remainingItem.getAmount() > 0) { + inventoryItem.setAmount(remainingItem.getAmount()); + } else { + inventory.setItem(slotIndex, null); + } + } + } + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/icon/requirement/item/RemainingItem.java b/plugin/src/main/java/me/filoghost/chestcommands/icon/requirement/item/RemainingItem.java new file mode 100644 index 0000000..1944628 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/icon/requirement/item/RemainingItem.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.icon.requirement.item; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; + +public class RemainingItem { + + private final int slotIndex; + private final Material material; + private final short durability; + private int amount; + + public RemainingItem(int slotIndex, ItemStack item) { + this.slotIndex = slotIndex; + this.material = item.getType(); + this.durability = item.getDurability(); + this.amount = item.getAmount(); + } + + public int getSlotIndex() { + return slotIndex; + } + + public Material getMaterial() { + return material; + } + + public short getDurability() { + return durability; + } + + public int getAmount() { + return amount; + } + + public int subtract(int minusAmount) { + int subtractedAmount = Math.min(minusAmount, this.amount); + + this.amount -= subtractedAmount; + return subtractedAmount; + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/icon/requirement/item/RequiredItem.java b/plugin/src/main/java/me/filoghost/chestcommands/icon/requirement/item/RequiredItem.java new file mode 100644 index 0000000..5ab0b5a --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/icon/requirement/item/RequiredItem.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.icon.requirement.item; + +import me.filoghost.commons.Preconditions; +import org.bukkit.Material; + +public class RequiredItem { + + private final Material material; + private final int amount; + private short durability; + private boolean isDurabilityRestrictive = false; + + public RequiredItem(Material material, int amount) { + Preconditions.checkArgumentNotAir(material, "material"); + + this.material = material; + this.amount = amount; + } + + public Material getMaterial() { + return material; + } + + public int getAmount() { + return amount; + } + + public short getDurability() { + return durability; + } + + public void setRestrictiveDurability(short durability) { + Preconditions.checkArgument(durability >= 0, "durability must be 0 or greater"); + + this.durability = durability; + isDurabilityRestrictive = true; + } + + public boolean hasRestrictiveDurability() { + return isDurabilityRestrictive; + } + + public boolean isMatchingType(RemainingItem item) { + return item != null && item.getMaterial() == material && isMatchingDurability(item.getDurability()); + } + + private boolean isMatchingDurability(short durability) { + if (isDurabilityRestrictive) { + return this.durability == durability; + } else { + return true; + } + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/icon/requirement/item/RequiredItems.java b/plugin/src/main/java/me/filoghost/chestcommands/icon/requirement/item/RequiredItems.java new file mode 100644 index 0000000..ce696f2 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/icon/requirement/item/RequiredItems.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.icon.requirement.item; + +import com.google.common.collect.ImmutableList; +import me.filoghost.chestcommands.config.Lang; +import me.filoghost.chestcommands.icon.requirement.Requirement; +import me.filoghost.chestcommands.util.Utils; +import org.bukkit.entity.Player; + +import java.util.List; + +public class RequiredItems implements Requirement { + + private final List items; + + public RequiredItems(List items) { + this.items = ImmutableList.copyOf(items); + } + + @Override + public boolean hasCost(Player player) { + InventoryTakeHelper inventoryTakeHelper = new InventoryTakeHelper(player.getInventory()); + boolean hasItems = inventoryTakeHelper.prepareTakeItems(items); + + if (!hasItems) { + for (RequiredItem item : items) { + player.sendMessage(Lang.no_required_item + .replace("{material}", Utils.formatEnum(item.getMaterial())) + .replace("{amount}", Integer.toString(item.getAmount())) + .replace("{durability}", item.hasRestrictiveDurability() ? Short.toString(item.getDurability()) : Lang.any)); + } + } + + return hasItems; + } + + @Override + public boolean takeCost(Player player) { + InventoryTakeHelper inventoryTakeHelper = new InventoryTakeHelper(player.getInventory()); + boolean hasItems = inventoryTakeHelper.prepareTakeItems(items); + + if (!hasItems) { + return false; + } + + inventoryTakeHelper.applyTakeItems(); + return true; + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/inventory/ArrayGrid.java b/plugin/src/main/java/me/filoghost/chestcommands/inventory/ArrayGrid.java new file mode 100644 index 0000000..29fa339 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/inventory/ArrayGrid.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.inventory; + +public class ArrayGrid extends Grid { + + private final T[] elements; + + @SuppressWarnings("unchecked") + public ArrayGrid(int rows, int columns) { + super(rows, columns); + this.elements = (T[]) new Object[getSize()]; + } + + @Override + protected T getByIndex0(int ordinalIndex) { + return elements[ordinalIndex]; + } + + @Override + protected void setByIndex0(int ordinalIndex, T element) { + elements[ordinalIndex] = element; + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/inventory/DefaultMenuView.java b/plugin/src/main/java/me/filoghost/chestcommands/inventory/DefaultMenuView.java new file mode 100644 index 0000000..89f0a9b --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/inventory/DefaultMenuView.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.inventory; + +import me.filoghost.chestcommands.api.Icon; +import me.filoghost.chestcommands.api.MenuView; +import me.filoghost.chestcommands.icon.RefreshableIcon; +import me.filoghost.chestcommands.menu.BaseMenu; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +/** + * Represents a particular view of a menu. + */ +public class DefaultMenuView implements MenuView { + + private final BaseMenu menu; + private final InventoryGrid bukkitInventory; + private final Player viewer; + + public DefaultMenuView(BaseMenu menu, Player viewer) { + this.menu = menu; + this.viewer = viewer; + this.bukkitInventory = new InventoryGrid(new MenuInventoryHolder(this), menu.getRowCount(), menu.getTitle()); + refresh(); + } + + @Override + public void refresh() { + for (int i = 0; i < menu.getIcons().getSize(); i++) { + Icon icon = menu.getIcons().getByIndex(i); + + if (icon == null) { + bukkitInventory.setByIndex(i, null); + } else if (icon instanceof RefreshableIcon) { + ItemStack newItemStack = ((RefreshableIcon) icon).updateRendering(viewer, bukkitInventory.getByIndex(i)); + bukkitInventory.setByIndex(i, newItemStack); + } else { + bukkitInventory.setByIndex(i, icon.render(viewer)); + } + } + } + + public void open(Player viewer) { + viewer.openInventory(bukkitInventory.getInventory()); + } + + public Icon getIcon(int slot) { + if (slot < 0 || slot >= bukkitInventory.getSize()) { + return null; + } + + return menu.getIcons().getByIndex(slot); + } + + @Override + public BaseMenu getMenu() { + return menu; + } + + @Override + public Player getViewer() { + return viewer; + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/inventory/Grid.java b/plugin/src/main/java/me/filoghost/chestcommands/inventory/Grid.java new file mode 100644 index 0000000..c390267 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/inventory/Grid.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.inventory; + +import me.filoghost.commons.Preconditions; + +/* + * Example: + * There 3 rows and 9 columns. The number inside the cells is the index. + * + * <--- Column ---> + * + * 0 1 2 3 4 5 6 7 8 + * ^ +--------------------------------------------+ + * | 0 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | + * |----+----+----+----+----+----+----+----+----| + * Row 1 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | + * |----+----+----+----+----+----+----+----+----| + * | 2 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | + * v +--------------------------------------------+ + * + */ +public abstract class Grid { + + private final int rows, columns, size; + + public Grid(int rows, int columns) { + this.rows = rows; + this.columns = columns; + this.size = rows * columns; + } + + public final void set(int row, int column, T element) { + setByIndex(toOrdinalIndex(row, column), element); + } + + public final T get(int row, int column) { + return getByIndex(toOrdinalIndex(row, column)); + } + + public final T getByIndex(int ordinalIndex) { + Preconditions.checkIndex(ordinalIndex, getSize(), "ordinalIndex"); + return getByIndex0(ordinalIndex); + } + + protected abstract T getByIndex0(int ordinalIndex); + + public final void setByIndex(int ordinalIndex, T element) { + Preconditions.checkIndex(ordinalIndex, getSize(), "ordinalIndex"); + setByIndex0(ordinalIndex, element); + } + + + protected abstract void setByIndex0(int ordinalIndex, T element); + + private int toOrdinalIndex(int row, int column) { + Preconditions.checkIndex(row, getRows(), "row"); + Preconditions.checkIndex(column, getColumns(), "column"); + + int ordinalIndex = row * getColumns() + column; + Preconditions.checkIndex(ordinalIndex, getSize(), "ordinalIndex"); + return ordinalIndex; + } + + public int getRows() { + return rows; + } + + public int getColumns() { + return columns; + } + + public int getSize() { + return size; + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/inventory/InventoryGrid.java b/plugin/src/main/java/me/filoghost/chestcommands/inventory/InventoryGrid.java new file mode 100644 index 0000000..1616cbc --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/inventory/InventoryGrid.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.inventory; + +import org.bukkit.Bukkit; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; + +public class InventoryGrid extends Grid { + + private final Inventory inventory; + + public InventoryGrid(MenuInventoryHolder inventoryHolder, int rows, String title) { + super(rows, 9); + this.inventory = Bukkit.createInventory(inventoryHolder, getSize(), title); + } + + public Inventory getInventory() { + return inventory; + } + + @Override + protected ItemStack getByIndex0(int ordinalIndex) { + return inventory.getItem(ordinalIndex); + } + + @Override + protected void setByIndex0(int ordinalIndex, ItemStack element) { + inventory.setItem(ordinalIndex, element); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/inventory/MenuInventoryHolder.java b/plugin/src/main/java/me/filoghost/chestcommands/inventory/MenuInventoryHolder.java new file mode 100644 index 0000000..9f53dfc --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/inventory/MenuInventoryHolder.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.inventory; + +import me.filoghost.commons.Preconditions; +import org.bukkit.Bukkit; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryHolder; + +public class MenuInventoryHolder implements InventoryHolder { + + private final DefaultMenuView menuView; + + public MenuInventoryHolder(DefaultMenuView menuView) { + Preconditions.notNull(menuView, "menuView"); + this.menuView = menuView; + } + + @Override + public Inventory getInventory() { + /* + * This inventory will not do anything. + * I'm 90% sure that it doesn't break any other plugin, + * because the only way you can get here is using InventoryClickEvent, + * that is cancelled by ChestCommands, or using InventoryOpenEvent. + */ + return Bukkit.createInventory(null, 9); + } + + public DefaultMenuView getMenuView() { + return menuView; + } +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/legacy/Backup.java b/plugin/src/main/java/me/filoghost/chestcommands/legacy/Backup.java new file mode 100644 index 0000000..12644b4 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/legacy/Backup.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.legacy; + +import me.filoghost.commons.Preconditions; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; + +public class Backup { + + private final Path dataFolder; + private final Path backupFolder; + private final Path infoFile; + + public Backup(Path dataFolder, String backupName) { + this.dataFolder = dataFolder; + Path backupsFolder = dataFolder.resolve("old_files"); + this.backupFolder = backupsFolder.resolve(backupName); + this.infoFile = backupsFolder.resolve("readme.txt"); + } + + public static Backup newTimestampedBackup(Path dataFolder) { + String date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd-HH.mm")); + String backupName = "backup_" + date; + return new Backup(dataFolder, backupName); + } + + public void addFile(Path fileToBackup) throws IOException { + Preconditions.checkArgument(fileToBackup.startsWith(dataFolder), "file is not inside data folder"); + Path destination = backupFolder.resolve(dataFolder.relativize(fileToBackup)); + Files.createDirectories(destination.getParent()); + + // Add backup file if no already present + if (!Files.isRegularFile(destination)) { + Files.copy(fileToBackup, destination); + } + + // Add README file if not already present + if (!Files.isRegularFile(infoFile)) { + Files.write(infoFile, Arrays.asList( + "Files in this folders are copies of original configuration files that have been automatically upgraded.", + "", + "Note: some configuration upgrades remove comments and other formatting (such as empty lines)." + )); + } + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/legacy/UpgradeExecutorException.java b/plugin/src/main/java/me/filoghost/chestcommands/legacy/UpgradeExecutorException.java new file mode 100644 index 0000000..12dca04 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/legacy/UpgradeExecutorException.java @@ -0,0 +1,14 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.legacy; + +public class UpgradeExecutorException extends Exception { + + public UpgradeExecutorException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/legacy/UpgradeList.java b/plugin/src/main/java/me/filoghost/chestcommands/legacy/UpgradeList.java new file mode 100644 index 0000000..d3c4f4a --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/legacy/UpgradeList.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.legacy; + +import com.google.common.collect.ImmutableList; +import me.filoghost.chestcommands.config.ConfigManager; +import me.filoghost.chestcommands.legacy.upgrade.Upgrade; +import me.filoghost.chestcommands.legacy.upgrade.UpgradeTask; +import me.filoghost.chestcommands.legacy.upgrade.UpgradeTaskException; +import me.filoghost.chestcommands.legacy.v4_0.v4_0_LangUpgradeTask; +import me.filoghost.chestcommands.legacy.v4_0.v4_0_MenuNodeRenameUpgradeTask; +import me.filoghost.chestcommands.legacy.v4_0.v4_0_MenuReformatUpgradeTask; +import me.filoghost.chestcommands.legacy.v4_0.v4_0_PlaceholdersUpgradeTask; +import me.filoghost.chestcommands.legacy.v4_0.v4_0_SettingsUpgradeTask; +import me.filoghost.chestcommands.logging.Errors; +import me.filoghost.commons.collection.CollectionUtils; +import me.filoghost.commons.config.ConfigLoader; +import me.filoghost.commons.logging.Log; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.function.Function; + +public class UpgradeList { + + /* + * Note: order of declaration determines order of execution + */ + private static final ImmutableList orderedUpgrades = ImmutableList.of( + multiTaskUpgrade("v4.0-menus-rename", (configManager) -> { + return createMenuTasks(configManager, v4_0_MenuNodeRenameUpgradeTask::new); + }), + + // Reformat after nodes have already been renamed + multiTaskUpgrade("v4.0-menus-reformat", (configManager) -> { + String legacyCommandSeparator = readLegacyCommandSeparator(configManager); + return createMenuTasks(configManager, + file -> new v4_0_MenuReformatUpgradeTask(configManager, file, legacyCommandSeparator)); + }), + + // Upgrade config after reading the command separator for menus + singleTaskUpgrade("v4.0-config", v4_0_SettingsUpgradeTask::new), + singleTaskUpgrade("v4.0-placeholders", v4_0_PlaceholdersUpgradeTask::new), + singleTaskUpgrade("v4.0-lang", v4_0_LangUpgradeTask::new) + ); + + private static Upgrade singleTaskUpgrade(String id, Upgrade.SingleTaskSupplier upgradeTaskSupplier) { + return new Upgrade(id, configManager -> { + return Collections.singletonList(upgradeTaskSupplier.getTask(configManager)); + }); + } + + private static Upgrade multiTaskUpgrade(String id, Upgrade.MultiTaskSupplier upgradeTasksSupplier) { + return new Upgrade(id, upgradeTasksSupplier); + } + + private static List createMenuTasks(ConfigManager configManager, Function menuTaskSupplier) throws UpgradeTaskException { + List menuFiles = getMenuFiles(configManager); + return CollectionUtils.transform(menuFiles, menuTaskSupplier); + } + + private static List getMenuFiles(ConfigManager configManager) throws UpgradeTaskException { + try { + return configManager.getMenuFiles(); + } catch (IOException e) { + throw new UpgradeTaskException(Errors.Upgrade.menuListIOException, e); + } + } + + private static String readLegacyCommandSeparator(ConfigManager configManager) { + ConfigLoader settingsConfigLoader = configManager.getConfigLoader("config.yml"); + + if (!settingsConfigLoader.fileExists()) { + return null; + } + + try { + return settingsConfigLoader.load().getString("multiple-commands-separator"); + } catch (Throwable t) { + Log.warning("Failed to load \"" + settingsConfigLoader.getFile() + "\", assuming default command separator \";\"."); + return null; + } + } + + public static ImmutableList getOrderedUpgrades() { + return orderedUpgrades; + } +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/legacy/UpgradesDoneRegistry.java b/plugin/src/main/java/me/filoghost/chestcommands/legacy/UpgradesDoneRegistry.java new file mode 100644 index 0000000..20e5118 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/legacy/UpgradesDoneRegistry.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.legacy; + +import me.filoghost.chestcommands.legacy.upgrade.Upgrade; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +public class UpgradesDoneRegistry { + + private final Path saveFile; + private final Set upgradesDone; + private boolean needSave; + + public UpgradesDoneRegistry(Path saveFile) throws IOException { + this.saveFile = saveFile; + this.upgradesDone = new HashSet<>(); + + if (Files.isRegularFile(saveFile)) { + try (Stream lines = Files.lines(saveFile)) { + lines.filter(s -> !s.startsWith("#")) + .forEach(upgradesDone::add); + } + } + } + + public void setAllDone() { + for (Upgrade upgrade : UpgradeList.getOrderedUpgrades()) { + setDone(upgrade); + } + } + + public void setDone(Upgrade upgrade) { + if (upgradesDone.add(upgrade.getID())) { + needSave = true; + } + } + + public boolean isDone(Upgrade upgrade) { + return upgradesDone.contains(upgrade.getID()); + } + + public void save() throws IOException { + if (needSave) { + List lines = new ArrayList<>(); + lines.add("#"); + lines.add("# WARNING: manually editing this file is not recommended"); + lines.add("#"); + lines.addAll(upgradesDone); + Files.createDirectories(saveFile.getParent()); + Files.write(saveFile, lines); + needSave = false; + } + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/legacy/UpgradesExecutor.java b/plugin/src/main/java/me/filoghost/chestcommands/legacy/UpgradesExecutor.java new file mode 100644 index 0000000..88da099 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/legacy/UpgradesExecutor.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.legacy; + +import me.filoghost.chestcommands.config.ConfigManager; +import me.filoghost.chestcommands.legacy.upgrade.Upgrade; +import me.filoghost.chestcommands.legacy.upgrade.UpgradeTask; +import me.filoghost.chestcommands.legacy.upgrade.UpgradeTaskException; +import me.filoghost.chestcommands.logging.Errors; +import me.filoghost.commons.logging.ErrorCollector; +import me.filoghost.commons.logging.Log; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; + +public class UpgradesExecutor { + + private final ConfigManager configManager; + private boolean allUpgradesSuccessful; + private UpgradesDoneRegistry upgradesDoneRegistry; + + public UpgradesExecutor(ConfigManager configManager) { + this.configManager = configManager; + } + + public boolean run(boolean isFreshInstall, ErrorCollector errorCollector) throws UpgradeExecutorException { + this.allUpgradesSuccessful = true; + Path upgradesDoneFile = configManager.getRootDataFolder().resolve(".upgrades-done"); + + try { + upgradesDoneRegistry = new UpgradesDoneRegistry(upgradesDoneFile); + } catch (IOException e) { + // Upgrades can't proceed if metadata file is not read correctly + throw new UpgradeExecutorException(Errors.Upgrade.metadataReadError(upgradesDoneFile), e); + } + + if (isFreshInstall) { + // Mark all currently existing upgrades as already done, assuming default configuration files are up to date + upgradesDoneRegistry.setAllDone(); + } else { + // Run missing upgrades + Backup backup = Backup.newTimestampedBackup(configManager.getRootDataFolder()); + runMissingUpgrades(backup, errorCollector); + } + + try { + upgradesDoneRegistry.save(); + } catch (IOException e) { + // Upgrades can't proceed if metadata file is not saved correctly + throw new UpgradeExecutorException(Errors.Upgrade.metadataSaveError(upgradesDoneFile), e); + } + + return allUpgradesSuccessful; + } + + + private void runMissingUpgrades(Backup backup, ErrorCollector errorCollector) { + for (Upgrade upgrade : UpgradeList.getOrderedUpgrades()) { + if (!upgradesDoneRegistry.isDone(upgrade)) { + boolean allTasksSuccessful = tryRunUpgradeTasks(upgrade, backup, errorCollector); + + // Consider an upgrade "done" if all its tasks were completed successfully + if (allTasksSuccessful) { + upgradesDoneRegistry.setDone(upgrade); + } else { + allUpgradesSuccessful = false; + } + } + } + } + + + private boolean tryRunUpgradeTasks(Upgrade upgrade, Backup backup, ErrorCollector errorCollector) { + boolean allTasksSuccessful = true; + + List upgradeTasks; + try { + upgradeTasks = upgrade.createUpgradeTasks(configManager); + } catch (UpgradeTaskException e) { + errorCollector.add(e, Errors.Upgrade.failedToPrepareUpgradeTasks); + return false; + } + + for (UpgradeTask upgradeTask : upgradeTasks) { + try { + boolean modified = upgradeTask.runAndBackupIfNecessary(backup); + if (modified) { + Log.info("Automatically upgraded configuration file \"" + upgradeTask.getUpgradedFile() + "\". " + + "A backup of the old file has been saved."); + } + } catch (UpgradeTaskException e) { + allTasksSuccessful = false; + errorCollector.add(e, Errors.Upgrade.failedSingleUpgrade(upgradeTask.getOriginalFile())); + } + } + + return allTasksSuccessful; + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/legacy/upgrade/RegexReplacer.java b/plugin/src/main/java/me/filoghost/chestcommands/legacy/upgrade/RegexReplacer.java new file mode 100644 index 0000000..184a617 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/legacy/upgrade/RegexReplacer.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.legacy.upgrade; + +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class RegexReplacer implements Function { + + private final Pattern regex; + private final Function replaceCallback; + + public RegexReplacer(Pattern regex, Function replaceCallback) { + this.regex = regex; + this.replaceCallback = replaceCallback; + } + + @Override + public String apply(String string) { + Matcher matcher = regex.matcher(string); + StringBuffer output = new StringBuffer(); + + while (matcher.find()) { + matcher.appendReplacement(output, replaceCallback.apply(matcher)); + } + matcher.appendTail(output); + + return output.toString(); + } +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/legacy/upgrade/RegexUpgradeTask.java b/plugin/src/main/java/me/filoghost/chestcommands/legacy/upgrade/RegexUpgradeTask.java new file mode 100644 index 0000000..5cc52ef --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/legacy/upgrade/RegexUpgradeTask.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.legacy.upgrade; + +import me.filoghost.commons.config.ConfigErrors; +import me.filoghost.commons.config.exception.ConfigLoadException; +import me.filoghost.commons.config.exception.ConfigSaveException; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public abstract class RegexUpgradeTask extends UpgradeTask { + + private final Path file; + private List newContents; + private Stream linesStream; + + public RegexUpgradeTask(Path file) { + this.file = file; + } + + @Override + public final Path getOriginalFile() { + return file; + } + + @Override + public final Path getUpgradedFile() { + return file; + } + + @Override + public final void computeChanges() throws ConfigLoadException { + if (!Files.isRegularFile(file)) { + return; + } + + List lines; + try { + lines = Files.readAllLines(file); + } catch (IOException e) { + throw new ConfigLoadException(ConfigErrors.readIOException, e); + } + + this.linesStream = lines.stream(); + computeRegexChanges(); + + newContents = linesStream.collect(Collectors.toList()); + + if (!newContents.equals(lines)) { + setSaveRequired(); + } + } + + @Override + public final void saveChanges() throws ConfigSaveException { + try { + Files.write(file, newContents); + } catch (IOException e) { + throw new ConfigSaveException(ConfigErrors.writeDataIOException, e); + } + } + + protected abstract void computeRegexChanges(); + + protected void replaceString(String target, String replacement) { + replaceRegex( + Pattern.compile(Pattern.quote(target)), + matcher -> replacement + ); + } + + protected void replaceSubNode(String oldNode, String newNode) { + replaceRegex( + Pattern.compile("(^\\s+)" + Pattern.quote(oldNode) + "(:)"), + matcher -> matcher.group(1) + newNode + matcher.group(2) + ); + } + + protected void replaceRegex(Pattern regex, Function replaceCallback) { + linesStream = linesStream.map(new RegexReplacer(regex, replaceCallback)); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/legacy/upgrade/Upgrade.java b/plugin/src/main/java/me/filoghost/chestcommands/legacy/upgrade/Upgrade.java new file mode 100644 index 0000000..02f2af4 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/legacy/upgrade/Upgrade.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.legacy.upgrade; + +import me.filoghost.chestcommands.config.ConfigManager; + +import java.util.List; + +public class Upgrade { + + private final String id; + private final MultiTaskSupplier upgradeTasksSupplier; + + public Upgrade(String id, MultiTaskSupplier taskSupplier) { + this.id = id; + this.upgradeTasksSupplier = taskSupplier; + } + + public String getID() { + return id; + } + + public List createUpgradeTasks(ConfigManager configManager) throws UpgradeTaskException { + return upgradeTasksSupplier.getTasks(configManager); + } + + @FunctionalInterface + public interface SingleTaskSupplier { + + UpgradeTask getTask(ConfigManager configManager) throws UpgradeTaskException; + + } + + @FunctionalInterface + public interface MultiTaskSupplier { + + List getTasks(ConfigManager configManager) throws UpgradeTaskException; + + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/legacy/upgrade/UpgradeTask.java b/plugin/src/main/java/me/filoghost/chestcommands/legacy/upgrade/UpgradeTask.java new file mode 100644 index 0000000..4b9c2ae --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/legacy/upgrade/UpgradeTask.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.legacy.upgrade; + +import me.filoghost.chestcommands.legacy.Backup; +import me.filoghost.chestcommands.logging.Errors; +import me.filoghost.commons.Preconditions; +import me.filoghost.commons.config.exception.ConfigLoadException; +import me.filoghost.commons.config.exception.ConfigSaveException; + +import java.io.IOException; +import java.nio.file.Path; + +public abstract class UpgradeTask { + + private boolean saveRequired; + private boolean hasRun; + + protected void setSaveRequired() { + this.saveRequired = true; + } + + public boolean runAndBackupIfNecessary(Backup backup) throws UpgradeTaskException { + Preconditions.checkState(!hasRun, "Upgrade task has already run"); + hasRun = true; + + try { + computeChanges(); + } catch (ConfigLoadException e) { + throw new UpgradeTaskException(Errors.Upgrade.loadError(getOriginalFile()), e); + } + + if (saveRequired) { + try { + backup.addFile(getOriginalFile()); + } catch (IOException e) { + throw new UpgradeTaskException(Errors.Upgrade.backupError(getOriginalFile()), e); + } + + try { + saveChanges(); + } catch (ConfigSaveException e) { + throw new UpgradeTaskException(Errors.Upgrade.saveError(getUpgradedFile()), e); + } + + return true; + + } else { + return false; + } + } + + public abstract Path getOriginalFile(); + + public abstract Path getUpgradedFile(); + + protected abstract void computeChanges() throws ConfigLoadException; + + protected abstract void saveChanges() throws ConfigSaveException; + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/legacy/upgrade/UpgradeTaskException.java b/plugin/src/main/java/me/filoghost/chestcommands/legacy/upgrade/UpgradeTaskException.java new file mode 100644 index 0000000..d0ae06c --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/legacy/upgrade/UpgradeTaskException.java @@ -0,0 +1,14 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.legacy.upgrade; + +public class UpgradeTaskException extends Exception { + + public UpgradeTaskException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/legacy/upgrade/YamlUpgradeTask.java b/plugin/src/main/java/me/filoghost/chestcommands/legacy/upgrade/YamlUpgradeTask.java new file mode 100644 index 0000000..b357e36 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/legacy/upgrade/YamlUpgradeTask.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.legacy.upgrade; + +import me.filoghost.commons.config.Config; +import me.filoghost.commons.config.ConfigLoader; +import me.filoghost.commons.config.exception.ConfigLoadException; +import me.filoghost.commons.config.exception.ConfigSaveException; + +import java.nio.file.Path; + +public abstract class YamlUpgradeTask extends UpgradeTask { + + private final ConfigLoader configLoader; + private Config updatedConfig; + + public YamlUpgradeTask(ConfigLoader configLoader) { + this.configLoader = configLoader; + } + + @Override + public final Path getOriginalFile() { + return configLoader.getFile(); + } + + @Override + public final Path getUpgradedFile() { + return configLoader.getFile(); + } + + @Override + public final void computeChanges() throws ConfigLoadException { + if (!configLoader.fileExists()) { + return; + } + Config config = configLoader.load(); + computeYamlChanges(config); + this.updatedConfig = config; + } + + @Override + public final void saveChanges() throws ConfigSaveException { + configLoader.save(updatedConfig); + } + + protected abstract void computeYamlChanges(Config config); + + protected void removeNode(Config config, String node) { + if (config.contains(node)) { + config.remove(node); + setSaveRequired(); + } + } + + protected void replaceStringValue(Config settingsConfig, String node, String target, String replacement) { + String value = settingsConfig.getString(node); + if (value.contains(target)) { + settingsConfig.setString(node, value.replace(target, replacement)); + setSaveRequired(); + } + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/legacy/v4_0/v4_0_LangUpgradeTask.java b/plugin/src/main/java/me/filoghost/chestcommands/legacy/v4_0/v4_0_LangUpgradeTask.java new file mode 100644 index 0000000..2cc0c33 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/legacy/v4_0/v4_0_LangUpgradeTask.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.legacy.v4_0; + +import me.filoghost.chestcommands.config.ConfigManager; +import me.filoghost.chestcommands.legacy.upgrade.YamlUpgradeTask; +import me.filoghost.commons.config.Config; + +public class v4_0_LangUpgradeTask extends YamlUpgradeTask { + + public v4_0_LangUpgradeTask(ConfigManager configManager) { + super(configManager.getConfigLoader("lang.yml")); + } + + @Override + public void computeYamlChanges(Config settingsConfig) { + removeNode(settingsConfig, "open-menu"); + removeNode(settingsConfig, "open-menu-others"); + replaceStringValue(settingsConfig, "no-required-item", "{datavalue}", "{durability}"); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/legacy/v4_0/v4_0_MenuNodeRenameUpgradeTask.java b/plugin/src/main/java/me/filoghost/chestcommands/legacy/v4_0/v4_0_MenuNodeRenameUpgradeTask.java new file mode 100644 index 0000000..51e2884 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/legacy/v4_0/v4_0_MenuNodeRenameUpgradeTask.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.legacy.v4_0; + +import me.filoghost.chestcommands.legacy.upgrade.RegexUpgradeTask; +import me.filoghost.chestcommands.parsing.icon.AttributeType; + +import java.nio.file.Path; + +public class v4_0_MenuNodeRenameUpgradeTask extends RegexUpgradeTask { + + public v4_0_MenuNodeRenameUpgradeTask(Path menuFile) { + super(menuFile); + } + @Override + protected void computeRegexChanges() { + replaceSubNode("command", "commands"); + replaceSubNode("open-action", "open-actions"); + replaceSubNode("id", "material"); + + replaceSubNode("ID", AttributeType.MATERIAL.getAttributeName()); + replaceSubNode("DATA-VALUE", AttributeType.DURABILITY.getAttributeName()); + replaceSubNode("NBT", AttributeType.NBT_DATA.getAttributeName()); + replaceSubNode("ENCHANTMENT", AttributeType.ENCHANTMENTS.getAttributeName()); + replaceSubNode("COMMAND", AttributeType.ACTIONS.getAttributeName()); + replaceSubNode("COMMANDS", AttributeType.ACTIONS.getAttributeName()); + replaceSubNode("REQUIRED-ITEM", AttributeType.REQUIRED_ITEMS.getAttributeName()); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/legacy/v4_0/v4_0_MenuReformatUpgradeTask.java b/plugin/src/main/java/me/filoghost/chestcommands/legacy/v4_0/v4_0_MenuReformatUpgradeTask.java new file mode 100644 index 0000000..b92b84d --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/legacy/v4_0/v4_0_MenuReformatUpgradeTask.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.legacy.v4_0; + +import me.filoghost.chestcommands.config.ConfigManager; +import me.filoghost.chestcommands.legacy.upgrade.YamlUpgradeTask; +import me.filoghost.chestcommands.parsing.icon.AttributeType; +import me.filoghost.chestcommands.parsing.menu.MenuSettingsNode; +import me.filoghost.commons.Strings; +import me.filoghost.commons.config.Config; +import me.filoghost.commons.config.ConfigSection; +import me.filoghost.commons.config.ConfigValueType; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.regex.Pattern; + +public class v4_0_MenuReformatUpgradeTask extends YamlUpgradeTask { + + private final String legacyCommandSeparator; + + public v4_0_MenuReformatUpgradeTask(ConfigManager configManager, Path menuFile, String legacyCommandSeparator) { + super(configManager.getConfigLoader(menuFile)); + this.legacyCommandSeparator = legacyCommandSeparator; + } + + @Override + public void computeYamlChanges(Config menuConfig) { + menuConfig.setHeader(null); + + for (String key : menuConfig.getKeys()) { + ConfigSection section = menuConfig.getConfigSection(key); + if (section == null) { + continue; + } + + if (key.equals(MenuSettingsNode.ROOT_SECTION)) { + upgradeMenuSettings(section); + } else { + upgradeIcon(section); + } + } + } + + private void upgradeMenuSettings(ConfigSection section) { + expandInlineList(section, MenuSettingsNode.COMMANDS, ";"); + expandInlineList(section, MenuSettingsNode.OPEN_ACTIONS, legacyCommandSeparator); + updateActionPrefixes(section, MenuSettingsNode.OPEN_ACTIONS); + } + + private void upgradeIcon(ConfigSection section) { + expandInlineList(section, AttributeType.ENCHANTMENTS.getAttributeName(), ";"); + expandInlineList(section, AttributeType.ACTIONS.getAttributeName(), legacyCommandSeparator); + updateActionPrefixes(section, AttributeType.ACTIONS.getAttributeName()); + expandSingletonList(section, AttributeType.REQUIRED_ITEMS.getAttributeName()); + expandInlineItemstack(section); + } + + private void updateActionPrefixes(ConfigSection config, String node) { + List actions = config.getStringList(node); + if (actions == null) { + return; + } + + for (int i = 0; i < actions.size(); i++) { + String oldAction = actions.get(i); + String newAction = oldAction; + newAction = replacePrefix(newAction, "menu:", "open:"); + newAction = replacePrefix(newAction, "givemoney:", "give-money:"); + newAction = replacePrefix(newAction, "dragonbar:", "dragon-bar:"); + newAction = replacePrefix(newAction, "server ", "server: "); + + if (!newAction.equals(oldAction)) { + setSaveRequired(); + actions.set(i, newAction); + } + } + + config.setStringList(node, actions); + } + + private String replacePrefix(String action, String oldPrefix, String newPrefix) { + if (action.startsWith(oldPrefix)) { + setSaveRequired(); + return newPrefix + action.substring(oldPrefix.length()); + } else { + return action; + } + } + + private void expandInlineItemstack(ConfigSection section) { + String material = section.getString(AttributeType.MATERIAL.getAttributeName()); + if (material == null) { + return; + } + + if (material.contains(",")) { + String[] parts = Strings.trimmedSplit(material, ",", 2); + if (!section.contains(AttributeType.AMOUNT.getAttributeName())) { + try { + section.setInt(AttributeType.AMOUNT.getAttributeName(), Integer.parseInt(parts[1])); + } catch (NumberFormatException e) { + section.setString(AttributeType.AMOUNT.getAttributeName(), parts[1]); + } + } + material = parts[0]; + section.setString(AttributeType.MATERIAL.getAttributeName(), material); + setSaveRequired(); + } + + if (material.contains(":")) { + String[] parts = Strings.trimmedSplit(material, ":", 2); + if (!section.contains(AttributeType.DURABILITY.getAttributeName())) { + try { + section.setInt(AttributeType.DURABILITY.getAttributeName(), Integer.parseInt(parts[1])); + } catch (NumberFormatException e) { + section.setString(AttributeType.DURABILITY.getAttributeName(), parts[1]); + } + } + material = parts[0]; + section.setString(AttributeType.MATERIAL.getAttributeName(), material); + setSaveRequired(); + } + } + + private void expandInlineList(ConfigSection config, String node, String separator) { + if (config.get(node).isValidAs(ConfigValueType.STRING)) { + config.setStringList(node, splitListElements(config.getString(node), separator)); + setSaveRequired(); + } + } + + private void expandSingletonList(ConfigSection config, String node) { + if (config.get(node).isValidAs(ConfigValueType.STRING)) { + config.setStringList(node, Collections.singletonList(config.getString(node))); + setSaveRequired(); + } + } + + private List splitListElements(String input, String separator) { + if (separator == null || separator.length() == 0) { + separator = ";"; + } + + String[] splitValues = Strings.trimmedSplit(input, Pattern.quote(separator)); + List values = new ArrayList<>(); + + for (String value : splitValues) { + if (!value.isEmpty()) { + values.add(value); + } + } + + // Return a list with an empty value to avoid displaying the empty list value "[]" in the YML file + if (values.isEmpty()) { + values.add(""); + } + + return values; + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/legacy/v4_0/v4_0_PlaceholdersUpgradeTask.java b/plugin/src/main/java/me/filoghost/chestcommands/legacy/v4_0/v4_0_PlaceholdersUpgradeTask.java new file mode 100644 index 0000000..67d0fa3 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/legacy/v4_0/v4_0_PlaceholdersUpgradeTask.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.legacy.v4_0; + +import me.filoghost.chestcommands.config.ConfigManager; +import me.filoghost.chestcommands.legacy.upgrade.UpgradeTask; +import me.filoghost.commons.Strings; +import me.filoghost.commons.config.Config; +import me.filoghost.commons.config.ConfigErrors; +import me.filoghost.commons.config.ConfigLoader; +import me.filoghost.commons.config.exception.ConfigLoadException; +import me.filoghost.commons.config.exception.ConfigSaveException; +import org.apache.commons.lang.StringEscapeUtils; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +public class v4_0_PlaceholdersUpgradeTask extends UpgradeTask { + + private final Path oldPlaceholdersFile; + private final ConfigLoader newPlaceholdersConfigLoader; + private Config updatedConfig; + + public v4_0_PlaceholdersUpgradeTask(ConfigManager configManager) { + this.oldPlaceholdersFile = configManager.getRootDataFolder().resolve("placeholders.yml"); + this.newPlaceholdersConfigLoader = configManager.getConfigLoader("custom-placeholders.yml"); + } + + @Override + public Path getOriginalFile() { + return oldPlaceholdersFile; + } + + @Override + public Path getUpgradedFile() { + return newPlaceholdersConfigLoader.getFile(); + } + + @Override + public void computeChanges() throws ConfigLoadException { + if (!Files.isRegularFile(oldPlaceholdersFile)) { + return; + } + + // Do NOT load the new placeholder configuration from disk, as it should only contain placeholders imported from the old file + Config newPlaceholdersConfig = new Config(newPlaceholdersConfigLoader.getFile()); + List lines; + try { + lines = Files.readAllLines(oldPlaceholdersFile); + } catch (IOException e) { + throw new ConfigLoadException(ConfigErrors.readIOException, e); + } + + for (String line : lines) { + // Comment or empty line + if (line.isEmpty() || line.startsWith("#")) { + continue; + } + + // Ignore bad line + if (!line.contains(":")) { + continue; + } + + String[] parts = Strings.trimmedSplit(line, ":", 2); + String placeholder = unquote(parts[0]); + String replacement = StringEscapeUtils.unescapeJava(unquote(parts[1])); + + newPlaceholdersConfig.setString("placeholders." + placeholder, replacement); + setSaveRequired(); + } + + this.updatedConfig = newPlaceholdersConfig; + } + + @Override + public void saveChanges() throws ConfigSaveException { + try { + Files.deleteIfExists(oldPlaceholdersFile); + } catch (IOException ignored) {} + newPlaceholdersConfigLoader.save(updatedConfig); + } + + private static String unquote(String input) { + if (input.length() < 2) { + // Too short, cannot be a quoted string + return input; + } + if (input.startsWith("'") && input.endsWith("'")) { + return input.substring(1, input.length() - 1); + } else if (input.startsWith("\"") && input.endsWith("\"")) { + return input.substring(1, input.length() - 1); + } + + return input; + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/legacy/v4_0/v4_0_SettingsUpgradeTask.java b/plugin/src/main/java/me/filoghost/chestcommands/legacy/v4_0/v4_0_SettingsUpgradeTask.java new file mode 100644 index 0000000..054354d --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/legacy/v4_0/v4_0_SettingsUpgradeTask.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.legacy.v4_0; + +import me.filoghost.chestcommands.config.ConfigManager; +import me.filoghost.chestcommands.legacy.upgrade.YamlUpgradeTask; +import me.filoghost.commons.config.Config; + +public class v4_0_SettingsUpgradeTask extends YamlUpgradeTask { + + public v4_0_SettingsUpgradeTask(ConfigManager configManager) { + super(configManager.getConfigLoader("config.yml")); + } + + @Override + public void computeYamlChanges(Config settingsConfig) { + removeNode(settingsConfig, "use-only-commands-without-args"); + removeNode(settingsConfig, "use-console-colors"); + removeNode(settingsConfig, "multiple-commands-separator"); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/listener/CommandListener.java b/plugin/src/main/java/me/filoghost/chestcommands/listener/CommandListener.java new file mode 100644 index 0000000..883ef95 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/listener/CommandListener.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.listener; + +import me.filoghost.chestcommands.menu.InternalMenu; +import me.filoghost.chestcommands.menu.MenuManager; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerCommandPreprocessEvent; + +public class CommandListener implements Listener { + + private final MenuManager menuManager; + + public CommandListener(MenuManager menuManager) { + this.menuManager = menuManager; + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onCommand(PlayerCommandPreprocessEvent event) { + String command = getCommandName(event.getMessage()); + if (command == null) { + return; + } + + InternalMenu menu = menuManager.getMenuByOpenCommand(command); + if (menu == null) { + return; + } + + event.setCancelled(true); + menu.openCheckingPermission(event.getPlayer()); + } + + private static String getCommandName(String fullCommand) { + if (!fullCommand.startsWith("/")) { + return null; + } + + int firstSpace = fullCommand.indexOf(' '); + if (firstSpace >= 1) { + return fullCommand.substring(1, firstSpace); + } else { + return fullCommand.substring(1); + } + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/listener/InventoryListener.java b/plugin/src/main/java/me/filoghost/chestcommands/listener/InventoryListener.java new file mode 100644 index 0000000..faee40a --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/listener/InventoryListener.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.listener; + +import me.filoghost.chestcommands.ChestCommands; +import me.filoghost.chestcommands.api.ClickResult; +import me.filoghost.chestcommands.api.Icon; +import me.filoghost.chestcommands.config.Settings; +import me.filoghost.chestcommands.inventory.DefaultMenuView; +import me.filoghost.chestcommands.menu.MenuManager; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.Action; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.inventory.Inventory; + +import java.util.Map; +import java.util.WeakHashMap; + +public class InventoryListener implements Listener { + + private final MenuManager menuManager; + private final Map antiClickSpam; + + public InventoryListener(MenuManager menuManager) { + this.menuManager = menuManager; + this.antiClickSpam = new WeakHashMap<>(); + } + + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = false) + public void onInteract(PlayerInteractEvent event) { + if (event.hasItem() && event.getAction() != Action.PHYSICAL) { + menuManager.openMenuByItem(event.getPlayer(), event.getItem(), event.getAction()); + } + } + + @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = false) + public void onEarlyInventoryClick(InventoryClickEvent event) { + if (MenuManager.isMenuInventory(event.getInventory())) { + // Cancel the event as early as possible + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = false) + public void onLateInventoryClick(InventoryClickEvent event) { + Inventory inventory = event.getInventory(); + DefaultMenuView menuView = MenuManager.getOpenMenuView(inventory); + if (menuView == null) { + return; + } + + // Cancel the event again just in case a plugin un-cancels it + event.setCancelled(true); + + int slot = event.getRawSlot(); + Player clicker = (Player) event.getWhoClicked(); + Icon icon = menuView.getIcon(slot); + if (icon == null) { + return; + } + + Long cooldownUntil = antiClickSpam.get(clicker); + long now = System.currentTimeMillis(); + int minDelay = Settings.anti_click_spam_delay; + + if (minDelay > 0) { + if (cooldownUntil != null && cooldownUntil > now) { + return; + } else { + antiClickSpam.put(clicker, now + minDelay); + } + } + + // Only handle the click AFTER the event has finished + Bukkit.getScheduler().runTask(ChestCommands.getPluginInstance(), () -> { + ClickResult result = icon.onClick(menuView, clicker); + + if (result == ClickResult.CLOSE) { + clicker.closeInventory(); + } + }); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/listener/JoinListener.java b/plugin/src/main/java/me/filoghost/chestcommands/listener/JoinListener.java new file mode 100644 index 0000000..61b986f --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/listener/JoinListener.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.listener; + +import me.filoghost.chestcommands.ChestCommands; +import me.filoghost.chestcommands.Permissions; +import me.filoghost.chestcommands.config.Settings; +import org.bukkit.ChatColor; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; + +public class JoinListener implements Listener { + + @EventHandler + public void onJoin(PlayerJoinEvent event) { + Player player = event.getPlayer(); + + if (ChestCommands.getLastLoadErrors().hasErrors() && player.hasPermission(Permissions.SEE_ERRORS)) { + player.sendMessage( + ChestCommands.CHAT_PREFIX + ChatColor.RED + "The plugin found " + ChestCommands.getLastLoadErrors().getErrorsCount() + + " error(s) last time it was loaded. You can see them by doing \"/cc reload\" in the console."); + } + + if (ChestCommands.hasNewVersion() && Settings.update_notifications && player.hasPermission(Permissions.UPDATE_NOTIFICATIONS)) { + player.sendMessage(ChestCommands.CHAT_PREFIX + "Found an update: " + ChestCommands.getNewVersion() + ". Download:"); + player.sendMessage(ChatColor.DARK_GREEN + ">> " + ChatColor.GREEN + "http://dev.bukkit.org/bukkit-plugins/chest-commands"); + } + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/listener/SignListener.java b/plugin/src/main/java/me/filoghost/chestcommands/listener/SignListener.java new file mode 100644 index 0000000..2a16834 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/listener/SignListener.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.listener; + +import me.filoghost.chestcommands.Permissions; +import me.filoghost.chestcommands.config.Lang; +import me.filoghost.chestcommands.menu.InternalMenu; +import me.filoghost.chestcommands.menu.MenuManager; +import me.filoghost.chestcommands.util.Utils; +import org.bukkit.ChatColor; +import org.bukkit.block.BlockState; +import org.bukkit.block.Sign; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.Action; +import org.bukkit.event.block.SignChangeEvent; +import org.bukkit.event.player.PlayerInteractEvent; + +public class SignListener implements Listener { + + private static final int HEADER_LINE = 0; + private static final int FILENAME_LINE = 1; + + private static final String SIGN_CREATION_TRIGGER = "[menu]"; + + private static final ChatColor VALID_SIGN_COLOR = ChatColor.DARK_BLUE; + private static final String VALID_SIGN_HEADER = VALID_SIGN_COLOR + SIGN_CREATION_TRIGGER; + + private final MenuManager menuManager; + + public SignListener(MenuManager menuManager) { + this.menuManager = menuManager; + } + + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void onSignClick(PlayerInteractEvent event) { + if (event.getAction() != Action.RIGHT_CLICK_BLOCK) { + return; + } + + BlockState clickedBlockState = event.getClickedBlock().getState(); + + if (!(clickedBlockState instanceof Sign)) { + return; + } + + Sign sign = (Sign) clickedBlockState; + + if (!sign.getLine(HEADER_LINE).equalsIgnoreCase(VALID_SIGN_HEADER)) { + return; + } + + String menuFileName = Utils.addYamlExtension(sign.getLine(FILENAME_LINE).trim()); + InternalMenu menu = menuManager.getMenuByFileName(menuFileName); + + if (menu == null) { + event.getPlayer().sendMessage(Lang.menu_not_found); + return; + } + + menu.openCheckingPermission(event.getPlayer()); + } + + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void onCreateMenuSign(SignChangeEvent event) { + Player player = event.getPlayer(); + + if (isCreatingMenuSign(event.getLine(HEADER_LINE)) && canCreateMenuSign(player)) { + String menuFileName = event.getLine(FILENAME_LINE).trim(); + + if (menuFileName.isEmpty()) { + event.setCancelled(true); + player.sendMessage(ChatColor.RED + "You must write a menu name in the second line."); + return; + } + + menuFileName = Utils.addYamlExtension(menuFileName); + + InternalMenu menu = menuManager.getMenuByFileName(menuFileName); + if (menu == null) { + event.setCancelled(true); + player.sendMessage(ChatColor.RED + "Menu \"" + menuFileName + "\" was not found."); + return; + } + + event.setLine(HEADER_LINE, VALID_SIGN_COLOR + event.getLine(HEADER_LINE)); + player.sendMessage(ChatColor.GREEN + "Successfully created a sign for the menu " + menuFileName + "."); + } + } + + + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onSignChangeMonitor(SignChangeEvent event) { + // Prevent players without permissions from creating menu signs + if (isValidMenuSign(event.getLine(HEADER_LINE)) && !canCreateMenuSign(event.getPlayer())) { + event.setLine(HEADER_LINE, ChatColor.stripColor(event.getLine(HEADER_LINE))); + } + } + + private boolean isCreatingMenuSign(String headerLine) { + return headerLine.equalsIgnoreCase(SIGN_CREATION_TRIGGER); + } + + private boolean isValidMenuSign(String headerLine) { + return headerLine.equalsIgnoreCase(VALID_SIGN_HEADER); + } + + private boolean canCreateMenuSign(Player player) { + return player.hasPermission(Permissions.SIGN_CREATE); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/logging/ErrorPrintInfo.java b/plugin/src/main/java/me/filoghost/chestcommands/logging/ErrorPrintInfo.java new file mode 100644 index 0000000..8d966c8 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/logging/ErrorPrintInfo.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.logging; + +import java.util.List; + +class ErrorPrintInfo { + + private final int index; + private final List message; + private final String details; + private final Throwable cause; + + public ErrorPrintInfo(int index, List message, String details, Throwable cause) { + this.index = index; + this.message = message; + this.details = details; + this.cause = cause; + } + + public int getIndex() { + return index; + } + + public List getMessage() { + return message; + } + + public String getDetails() { + return details; + } + + public Throwable getCause() { + return cause; + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/logging/Errors.java b/plugin/src/main/java/me/filoghost/chestcommands/logging/Errors.java new file mode 100644 index 0000000..c527286 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/logging/Errors.java @@ -0,0 +1,220 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.logging; + +import me.filoghost.chestcommands.ChestCommands; +import me.filoghost.chestcommands.parsing.icon.AttributeType; +import me.filoghost.chestcommands.parsing.icon.IconSettings; +import me.filoghost.commons.config.ConfigErrors; +import org.bukkit.ChatColor; + +import java.nio.file.Path; + +public class Errors { + + public static class Config { + + public static final String createDataFolderIOException = "plugin failed to load, couldn't create data folder"; + + public static String menuListIOException(Path menuFolder) { + return "couldn't fetch menu files inside the folder \"" + menuFolder + "\""; + } + + public static String initException(Path file) { + return "error while initializing config file \"" + formatPath(file) + "\""; + } + + public static String emptyPlaceholder(Path configFile) { + return "error in \"" + configFile + "\": placeholder cannot be empty (skipped)."; + } + + public static String tooLongPlaceholder(Path configFile, String placeholder) { + return "error in \"" + configFile + "\": placeholder cannot be longer than 100 character (" + placeholder + ")."; + } + + } + + + public static class Upgrade { + + public static final String genericExecutorError = "error while running automatic configuration upgrades"; + public static final String menuListIOException = "couldn't obtain a list of menu files"; + public static final String failedSomeUpgrades = "note: one or more automatic upgrades may have not been applied, configuration files or menus may require manual changes"; + public static final String failedToPrepareUpgradeTasks = "error while trying to prepare an automatic configuration upgrade"; + + public static String metadataReadError(Path metadataFile) { + return "couldn't read upgrades metadata file \"" + formatPath(metadataFile) + "\""; + } + + public static String metadataSaveError(Path metadataFile) { + return "couldn't save upgrades metadata file \"" + formatPath(metadataFile) + "\""; + } + + public static String failedSingleUpgrade(Path file) { + return "error while trying to automatically upgrade \"" + formatPath(file) + "\""; + } + + public static String loadError(Path file) { + return "couldn't load file to upgrade \"" + formatPath(file) + "\""; + } + + public static String backupError(Path file) { + return "couldn't create backup of file \"" + formatPath(file) + "\""; + } + + public static String saveError(Path file) { + return "couldn't save upgraded file \"" + formatPath(file) + "\""; + } + + } + + + public static class Parsing { + + public static final String invalidDecimal = "value is not a valid decimal"; + public static final String invalidShort = "value is not a valid short integer"; + public static final String invalidInteger = "value is not a valid integer"; + + public static final String strictlyPositive = "value must be greater than zero"; + public static final String zeroOrPositive = "value must be zero or greater"; + + public static final String invalidColorFormat = "value must match the format \"red, green, blue\""; + public static final String invalidPatternFormat = "value must match the format \"pattern:color\""; + + public static final String unknownAttribute = "unknown attribute"; + public static final String materialCannotBeAir = "material cannot be air"; + + public static String unknownMaterial(String materialString) { + return "unknown material \"" + materialString + "\""; + } + + public static String unknownPatternType(String patternTypeString) { + return "unknown pattern type \"" + patternTypeString + "\""; + } + + public static String unknownDyeColor(String dyeColorString) { + return "unknown dye color \"" + dyeColorString + "\""; + } + + public static String unknownEnchantmentType(String typeString) { + return "unknown enchantment type \"" + typeString + "\""; + } + + public static String invalidEnchantmentLevel(String levelString) { + return "invalid enchantment level \"" + levelString + "\","; + } + + public static String invalidDurability(String durabilityString) { + return "invalid durability \"" + durabilityString + "\""; + } + + public static String invalidAmount(String amountString) { + return "invalid amount \"" + amountString + "\""; + } + + public static String invalidColorNumber(String numberString, String colorName) { + return "invalid " + colorName + " color \"" + numberString + "\""; + } + + public static String invalidColorRange(String valueString, String colorName) { + return "invalid " + colorName + " color \"" + valueString + "\", value must be between 0 and 255"; + } + + public static String invalidBossBarTime(String timeString) { + return "invalid dragon bar time \"" + timeString + "\""; + } + + public static String invalidSoundPitch(String pitchString) { + return "invalid sound pitch \"" + pitchString + "\""; + } + + public static String invalidSoundVolume(String volumeString) { + return "invalid sound volume \"" + volumeString + "\""; + } + + public static String unknownSound(String soundString) { + return "unknown sound \"" + soundString + "\""; + } + + } + + public static class Menu { + + public static String invalidSetting(Path menuFile, String invalidSetting) { + return menuError(menuFile, "has an invalid menu setting \"" + invalidSetting + "\""); + } + + public static String missingSetting(Path menuFile, String missingSetting) { + return menuError(menuFile, "is missing the menu setting \"" + missingSetting + "\""); + } + + public static String missingSettingsSection(Path menuFile) { + return menuError(menuFile, "is missing the menu setting section"); + } + + public static String invalidSettingListElement(Path menuFile, String invalidSetting, String listElement) { + return menuError(menuFile, + "contains an invalid list element (\"" + listElement + "\") " + + "in the menu setting \"" + invalidSetting + "\""); + } + + private static String menuError(Path menuFile, String errorMessage) { + return "the menu \"" + formatPath(menuFile) + "\" " + errorMessage; + } + + public static String invalidAttribute(IconSettings iconSettings, AttributeType attributeType) { + return invalidAttribute(iconSettings, attributeType.getAttributeName()); + } + + public static String invalidAttribute(IconSettings iconSettings, String attributeName) { + return iconError(iconSettings, "has an invalid attribute \"" + attributeName + "\""); + } + + public static String missingAttribute(IconSettings iconSettings, AttributeType attributeType) { + return iconError(iconSettings, "is missing the attribute \"" + attributeType.getAttributeName() + "\""); + } + + public static String invalidAttributeListElement(IconSettings iconSettings, String attributeName, String listElement) { + return iconError(iconSettings, + "contains an invalid list element (\"" + listElement + "\") " + + "in the attribute \"" + attributeName + "\""); + } + + public static String iconOverridesAnother(IconSettings iconSettings) { + return iconError(iconSettings, "is overriding another icon with the same position"); + } + + private static String iconError(IconSettings iconSettings, String errorMessage) { + return "the icon \"" + iconSettings.getIconName() + "\" in the menu \"" + + formatPath(iconSettings.getMenuFile()) + "\" " + errorMessage; + } + + public static String duplicateMenuName(Path menuFile1, Path menuFile2) { + return "two menus (\"" + menuFile1 + "\" and \"" + menuFile2 + "\") " + + "have the same file name. Only of them will work when referenced by name"; + } + + public static String duplicateMenuCommand(Path menuFile1, Path menuFile2, String command) { + return "two menus (\"" + menuFile1 + "\" and \"" + menuFile2 + "\") " + + "have the same command \"" + command + "\". Only one will be opened when the command is executed"; + } + } + + public static class User { + + public static final String notifyStaffRequest = "Please inform the staff."; + + public static String configurationError(String errorMessage) { + return ChatColor.RED + "Error: " + errorMessage + ". " + Errors.User.notifyStaffRequest; + } + + } + + private static String formatPath(Path path) { + return ConfigErrors.formatPath(ChestCommands.getDataFolderPath(), path); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/logging/MessagePartJoiner.java b/plugin/src/main/java/me/filoghost/chestcommands/logging/MessagePartJoiner.java new file mode 100644 index 0000000..9d50b5c --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/logging/MessagePartJoiner.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.logging; + +import me.filoghost.commons.Strings; + +import java.util.List; + +class MessagePartJoiner { + + private final StringBuilder output; + + private String previousMessagePart; + private boolean appendedFirstSentenceSeparator; + + public static String join(List messageParts) { + int estimateLength = getEstimateLength(messageParts); + MessagePartJoiner errorMessageBuilder = new MessagePartJoiner(estimateLength); + for (String messagePart : messageParts) { + errorMessageBuilder.append(messagePart); + } + return errorMessageBuilder.build(); + } + + private static int getEstimateLength(List messageParts) { + int estimateLength = 0; + + // Length of message parts + for (String messagePart : messageParts) { + estimateLength += messagePart.length(); + } + + // Length of separators in between + estimateLength += (messageParts.size() - 1) * 2; + + return estimateLength; + } + + private MessagePartJoiner(int estimateLength) { + output = new StringBuilder(estimateLength); + } + + private void append(String messagePart) { + appendSeparator(); + appendMessagePart(messagePart); + + previousMessagePart = messagePart; + } + + private void appendMessagePart(String messagePart) { + if (previousMessagePart == null || previousMessagePart.endsWith(".")) { + output.append(Strings.capitalizeFirst(messagePart)); + } else { + output.append(messagePart); + } + } + + private void appendSeparator() { + if (previousMessagePart != null) { + if (previousMessagePart.endsWith(".")) { + output.append(" "); + this.appendedFirstSentenceSeparator = false; + + } else if (!appendedFirstSentenceSeparator) { + output.append(": "); + this.appendedFirstSentenceSeparator = true; + + } else { + output.append(", "); + } + } + } + + private String build() { + return output.toString(); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/logging/PrintableErrorCollector.java b/plugin/src/main/java/me/filoghost/chestcommands/logging/PrintableErrorCollector.java new file mode 100644 index 0000000..64adbc3 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/logging/PrintableErrorCollector.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.logging; + +import me.filoghost.chestcommands.ChestCommands; +import me.filoghost.chestcommands.legacy.UpgradeExecutorException; +import me.filoghost.chestcommands.legacy.upgrade.UpgradeTaskException; +import me.filoghost.chestcommands.parsing.ParseException; +import me.filoghost.commons.CommonsUtil; +import me.filoghost.commons.config.exception.ConfigException; +import me.filoghost.commons.config.exception.ConfigSyntaxException; +import me.filoghost.commons.logging.ErrorCollector; +import me.filoghost.commons.logging.ErrorLog; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; + +import java.util.ArrayList; +import java.util.List; + +public class PrintableErrorCollector extends ErrorCollector { + + + @Override + public void logToConsole() { + StringBuilder output = new StringBuilder(); + + if (errors.size() > 0) { + output.append(ChestCommands.CHAT_PREFIX).append(ChatColor.RED).append("Encountered ").append(errors.size()).append(" error(s) on load:\n"); + output.append(" \n"); + + int index = 1; + for (ErrorLog error : errors) { + ErrorPrintInfo printFormat = getErrorPrintInfo(index, error); + printError(output, printFormat); + index++; + } + } + + Bukkit.getConsoleSender().sendMessage(output.toString()); + } + + private ErrorPrintInfo getErrorPrintInfo(int index, ErrorLog error) { + List message = new ArrayList<>(error.getMessage().asList()); + String details = null; + Throwable cause = error.getCause(); + + // Recursively inspect the cause until an unknown or null exception is found + while (true) { + if (cause instanceof ConfigSyntaxException) { + message.add(cause.getMessage()); + details = ((ConfigSyntaxException) cause).getParsingErrorDetails(); + cause = null; // Do not print stacktrace for syntax exceptions + + } else if (cause instanceof ConfigException + || cause instanceof ParseException + || cause instanceof UpgradeTaskException + || cause instanceof UpgradeExecutorException) { + message.add(cause.getMessage()); + cause = cause.getCause(); // Print the cause (or nothing if null), not our "known" exception + + } else { + return new ErrorPrintInfo(index, message, details, cause); + } + } + } + + private static void printError(StringBuilder output, ErrorPrintInfo error) { + output.append(ChatColor.YELLOW).append(error.getIndex()).append(") "); + output.append(ChatColor.WHITE).append(MessagePartJoiner.join(error.getMessage())); + + if (error.getDetails() != null) { + output.append(". Details:\n"); + output.append(ChatColor.YELLOW).append(error.getDetails()).append("\n"); + } else { + output.append(".\n"); + } + if (error.getCause() != null) { + output.append(ChatColor.DARK_GRAY); + output.append("--------[ Exception details ]--------\n"); + output.append(CommonsUtil.getStackTraceString(error.getCause())); + output.append("-------------------------------------\n"); + } + output.append(" \n"); + output.append(ChatColor.RESET); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/menu/APIMenu.java b/plugin/src/main/java/me/filoghost/chestcommands/menu/APIMenu.java new file mode 100644 index 0000000..17b4826 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/menu/APIMenu.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.menu; + +import me.filoghost.commons.Preconditions; +import org.bukkit.plugin.Plugin; + +public class APIMenu extends BaseMenu { + + private final Plugin owner; + + public APIMenu(Plugin owner, String title, int rows) { + super(title, rows); + Preconditions.notNull(owner, "owner"); + this.owner = owner; + } + + public Plugin getOwner() { + return owner; + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/menu/BaseMenu.java b/plugin/src/main/java/me/filoghost/chestcommands/menu/BaseMenu.java new file mode 100644 index 0000000..4bb153c --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/menu/BaseMenu.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.menu; + +import me.filoghost.chestcommands.api.Icon; +import me.filoghost.chestcommands.api.Menu; +import me.filoghost.chestcommands.api.MenuView; +import me.filoghost.chestcommands.inventory.ArrayGrid; +import me.filoghost.chestcommands.inventory.DefaultMenuView; +import me.filoghost.chestcommands.inventory.Grid; +import me.filoghost.commons.Preconditions; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +public abstract class BaseMenu implements Menu { + + + protected final String title; + protected final Grid icons; + + + public BaseMenu(String title, int rows) { + Preconditions.notNull(title, "title"); + Preconditions.checkArgument(rows > 0, "rows must be greater than 0"); + this.title = title; + this.icons = new ArrayGrid<>(rows, 9); + } + + @Override + public void setIcon(int row, int column, Icon icon) { + icons.set(row, column, icon); + } + + @Override + public Icon getIcon(int row, int column) { + return icons.get(row, column); + } + + @Override + public int getRowCount() { + return icons.getRows(); + } + + @Override + public int getColumnCount() { + return icons.getColumns(); + } + + @Override + public String getTitle() { + return title; + } + + public Grid getIcons() { + return icons; + } + + @Override + public MenuView open(Player player) { + Preconditions.notNull(player, "player"); + + DefaultMenuView menuView = new DefaultMenuView(this, player); + menuView.open(player); + return menuView; + } + + @Override + public void refreshMenuViews() { + for (Player player : Bukkit.getOnlinePlayers()) { + DefaultMenuView menuView = MenuManager.getOpenMenuView(player); + if (menuView != null && menuView.getMenu() == this) { + menuView.refresh(); + } + } + } +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/menu/InternalMenu.java b/plugin/src/main/java/me/filoghost/chestcommands/menu/InternalMenu.java new file mode 100644 index 0000000..34fddf1 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/menu/InternalMenu.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.menu; + +import com.google.common.collect.ImmutableList; +import me.filoghost.chestcommands.Permissions; +import me.filoghost.chestcommands.action.Action; +import me.filoghost.chestcommands.api.MenuView; +import me.filoghost.chestcommands.config.Lang; +import me.filoghost.commons.collection.CollectionUtils; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.nio.file.Path; +import java.util.List; + +public class InternalMenu extends BaseMenu { + + private final Path sourceFile; + private final String openPermission; + + private ImmutableList openActions; + private int refreshTicks; + + public InternalMenu(String title, int rows, Path sourceFile) { + super(title, rows); + this.sourceFile = sourceFile; + this.openPermission = Permissions.OPEN_MENU_PREFIX + sourceFile.getFileName(); + } + + public Path getSourceFile() { + return sourceFile; + } + + public void setOpenActions(List openAction) { + this.openActions = CollectionUtils.immutableCopy(openAction); + } + + public String getOpenPermission() { + return openPermission; + } + + public int getRefreshTicks() { + return refreshTicks; + } + + public void setRefreshTicks(int refreshTicks) { + this.refreshTicks = refreshTicks; + } + + @Override + public MenuView open(Player player) { + if (openActions != null) { + for (Action openAction : openActions) { + openAction.execute(player); + } + } + + return super.open(player); + } + + public void openCheckingPermission(Player player) { + if (player.hasPermission(openPermission)) { + open(player); + } else { + sendNoOpenPermissionMessage(player); + } + } + + public void sendNoOpenPermissionMessage(CommandSender sender) { + String noPermMessage = Lang.no_open_permission; + if (noPermMessage != null && !noPermMessage.isEmpty()) { + sender.sendMessage(noPermMessage.replace("{permission}", this.openPermission)); + } + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/menu/MenuManager.java b/plugin/src/main/java/me/filoghost/chestcommands/menu/MenuManager.java new file mode 100644 index 0000000..5310a31 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/menu/MenuManager.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.menu; + +import me.filoghost.chestcommands.inventory.DefaultMenuView; +import me.filoghost.chestcommands.inventory.MenuInventoryHolder; +import me.filoghost.chestcommands.logging.Errors; +import me.filoghost.chestcommands.parsing.menu.LoadedMenu; +import me.filoghost.chestcommands.parsing.menu.MenuOpenItem; +import me.filoghost.commons.collection.CaseInsensitiveMap; +import me.filoghost.commons.logging.ErrorCollector; +import org.bukkit.entity.Player; +import org.bukkit.event.block.Action; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryView; +import org.bukkit.inventory.ItemStack; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class MenuManager { + + private static Map menusByFile; + private static Map menusByOpenCommand; + private static Map menusByOpenItem; + + public MenuManager() { + menusByFile = new CaseInsensitiveMap<>(); + menusByOpenCommand = new CaseInsensitiveMap<>(); + menusByOpenItem = new HashMap<>(); + } + + public void clear() { + menusByFile.clear(); + menusByOpenCommand.clear(); + menusByOpenItem.clear(); + } + + public InternalMenu getMenuByFileName(String fileName) { + return menusByFile.get(fileName); + } + + public void registerMenu(LoadedMenu loadedMenu, ErrorCollector errorCollector) { + InternalMenu menu = loadedMenu.getMenu(); + + String fileName = loadedMenu.getSourceFile().getFileName().toString(); + InternalMenu sameNameMenu = menusByFile.get(fileName); + if (sameNameMenu != null) { + errorCollector.add(Errors.Menu.duplicateMenuName(sameNameMenu.getSourceFile(), loadedMenu.getSourceFile())); + } + menusByFile.put(fileName, menu); + + if (loadedMenu.getOpenCommands() != null) { + for (String openCommand : loadedMenu.getOpenCommands()) { + if (!openCommand.isEmpty()) { + InternalMenu sameCommandMenu = menusByOpenCommand.get(openCommand); + if (sameCommandMenu != null) { + errorCollector.add(Errors.Menu.duplicateMenuCommand(sameCommandMenu.getSourceFile(), loadedMenu.getSourceFile(), openCommand)); + } + menusByOpenCommand.put(openCommand, menu); + } + } + } + + if (loadedMenu.getOpenItem() != null) { + menusByOpenItem.put(loadedMenu.getOpenItem(), menu); + } + } + + public void openMenuByItem(Player player, ItemStack itemInHand, Action clickAction) { + menusByOpenItem.forEach((openItem, menu) -> { + if (openItem.matches(itemInHand, clickAction)) { + menu.openCheckingPermission(player); + } + }); + } + + public InternalMenu getMenuByOpenCommand(String openCommand) { + return menusByOpenCommand.get(openCommand); + } + + public Collection getMenuFileNames() { + return Collections.unmodifiableCollection(menusByFile.keySet()); + } + + public static boolean isMenuInventory(Inventory inventory) { + return getMenuInventoryHolder(inventory) != null; + } + + public static DefaultMenuView getOpenMenuView(Player player) { + InventoryView view = player.getOpenInventory(); + if (view == null) { + return null; + } + + DefaultMenuView menuView = getOpenMenuView(view.getTopInventory()); + if (menuView == null) { + menuView = getOpenMenuView(view.getBottomInventory()); + } + + return menuView; + } + + + public static DefaultMenuView getOpenMenuView(Inventory inventory) { + MenuInventoryHolder inventoryHolder = getMenuInventoryHolder(inventory); + if (inventoryHolder != null) { + return inventoryHolder.getMenuView(); + } else { + return null; + } + } + + private static MenuInventoryHolder getMenuInventoryHolder(Inventory inventory) { + if (inventory.getHolder() instanceof MenuInventoryHolder) { + return (MenuInventoryHolder) inventory.getHolder(); + } else { + return null; + } + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/parsing/ActionParser.java b/plugin/src/main/java/me/filoghost/chestcommands/parsing/ActionParser.java new file mode 100644 index 0000000..dac98ac --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/parsing/ActionParser.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.parsing; + +import me.filoghost.chestcommands.action.Action; +import me.filoghost.chestcommands.action.BroadcastAction; +import me.filoghost.chestcommands.action.ChangeServerAction; +import me.filoghost.chestcommands.action.ConsoleCommandAction; +import me.filoghost.chestcommands.action.DragonBarAction; +import me.filoghost.chestcommands.action.GiveItemAction; +import me.filoghost.chestcommands.action.GiveMoneyAction; +import me.filoghost.chestcommands.action.OpCommandAction; +import me.filoghost.chestcommands.action.OpenMenuAction; +import me.filoghost.chestcommands.action.PlaySoundAction; +import me.filoghost.chestcommands.action.PlayerCommandAction; +import me.filoghost.chestcommands.action.SendMessageAction; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ActionParser { + + public static Action parse(String serializedAction) throws ParseException { + for (ActionType actionType : ActionType.values()) { + Matcher matcher = actionType.prefixPattern.matcher(serializedAction); + if (matcher.find()) { + // Remove the action prefix and trim the spaces + serializedAction = matcher.replaceFirst("").trim(); + return actionType.actionFactory.create(serializedAction); + } + } + + return new PlayerCommandAction(serializedAction); // Default action, no match found + } + + + private enum ActionType { + + CONSOLE_COMMAND("console", ConsoleCommandAction::new), + OP_COMMAND("op", OpCommandAction::new), + OPEN("open", OpenMenuAction::new), + SERVER("server", ChangeServerAction::new), // The colon is optional + TELL("tell", SendMessageAction::new), + BROADCAST("broadcast", BroadcastAction::new), + GIVE_ITEM("give", GiveItemAction::new), + GIVE_MONEY("give-money", GiveMoneyAction::new), + SOUND("sound", PlaySoundAction::new), + BOSS_BAR("dragon-bar", DragonBarAction::new); + + + private final Pattern prefixPattern; + private final ActionFactory actionFactory; + + + ActionType(String prefix, ActionFactory actionFactory) { + // Non-default actions must match the format "{prefix}: {content}" + this.prefixPattern = Pattern.compile("^" + Pattern.quote(prefix) + ":", Pattern.CASE_INSENSITIVE); + this.actionFactory = actionFactory; + } + + @FunctionalInterface + private interface ActionFactory { + + Action create(String serializedAction) throws ParseException; + + } + + } +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/parsing/EnchantmentParser.java b/plugin/src/main/java/me/filoghost/chestcommands/parsing/EnchantmentParser.java new file mode 100644 index 0000000..43765c4 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/parsing/EnchantmentParser.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.parsing; + +import me.filoghost.chestcommands.logging.Errors; +import me.filoghost.commons.Strings; +import me.filoghost.commons.collection.Registry; +import org.bukkit.enchantments.Enchantment; + +import java.util.Optional; + +public class EnchantmentParser { + + private static final Registry ENCHANTMENTS_REGISTRY; + + static { + ENCHANTMENTS_REGISTRY = Registry.fromValues(Enchantment.values(), Enchantment::getName); + + // Add aliases + ENCHANTMENTS_REGISTRY.put("Protection", Enchantment.PROTECTION_ENVIRONMENTAL); + ENCHANTMENTS_REGISTRY.put("Fire Protection", Enchantment.PROTECTION_FIRE); + ENCHANTMENTS_REGISTRY.put("Feather Falling", Enchantment.PROTECTION_FALL); + ENCHANTMENTS_REGISTRY.put("Blast Protection", Enchantment.PROTECTION_EXPLOSIONS); + ENCHANTMENTS_REGISTRY.put("Projectile Protection", Enchantment.PROTECTION_PROJECTILE); + ENCHANTMENTS_REGISTRY.put("Respiration", Enchantment.OXYGEN); + ENCHANTMENTS_REGISTRY.put("Aqua Affinity", Enchantment.WATER_WORKER); + ENCHANTMENTS_REGISTRY.put("Thorns", Enchantment.THORNS); + ENCHANTMENTS_REGISTRY.put("Sharpness", Enchantment.DAMAGE_ALL); + ENCHANTMENTS_REGISTRY.put("Smite", Enchantment.DAMAGE_UNDEAD); + ENCHANTMENTS_REGISTRY.put("Bane Of Arthropods", Enchantment.DAMAGE_ARTHROPODS); + ENCHANTMENTS_REGISTRY.put("Knockback", Enchantment.KNOCKBACK); + ENCHANTMENTS_REGISTRY.put("Fire Aspect", Enchantment.FIRE_ASPECT); + ENCHANTMENTS_REGISTRY.put("Looting", Enchantment.LOOT_BONUS_MOBS); + ENCHANTMENTS_REGISTRY.put("Efficiency", Enchantment.DIG_SPEED); + ENCHANTMENTS_REGISTRY.put("Silk Touch", Enchantment.SILK_TOUCH); + ENCHANTMENTS_REGISTRY.put("Unbreaking", Enchantment.DURABILITY); + ENCHANTMENTS_REGISTRY.put("Fortune", Enchantment.LOOT_BONUS_BLOCKS); + ENCHANTMENTS_REGISTRY.put("Power", Enchantment.ARROW_DAMAGE); + ENCHANTMENTS_REGISTRY.put("Punch", Enchantment.ARROW_KNOCKBACK); + ENCHANTMENTS_REGISTRY.put("Flame", Enchantment.ARROW_FIRE); + ENCHANTMENTS_REGISTRY.put("Infinity", Enchantment.ARROW_INFINITE); + ENCHANTMENTS_REGISTRY.put("Lure", Enchantment.LURE); + ENCHANTMENTS_REGISTRY.put("Luck Of The Sea", Enchantment.LUCK); + } + + public static EnchantmentDetails parseEnchantment(String input) throws ParseException { + int level = 1; + + if (input.contains(",")) { + String[] levelSplit = Strings.trimmedSplit(input, ",", 2); + + try { + level = NumberParser.getStrictlyPositiveInteger(levelSplit[1]); + } catch (ParseException e) { + throw new ParseException(Errors.Parsing.invalidEnchantmentLevel(levelSplit[1]), e); + } + input = levelSplit[0]; + } + + Optional enchantment = ENCHANTMENTS_REGISTRY.find(input); + + if (enchantment.isPresent()) { + return new EnchantmentDetails(enchantment.get(), level); + } else { + throw new ParseException(Errors.Parsing.unknownEnchantmentType(input)); + } + } + + + public static class EnchantmentDetails { + + private final Enchantment enchantment; + private final int level; + + private EnchantmentDetails(Enchantment enchantment, int level) { + this.enchantment = enchantment; + this.level = level; + } + + public Enchantment getEnchantment() { + return enchantment; + } + + public int getLevel() { + return level; + } + + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/parsing/ItemMetaParser.java b/plugin/src/main/java/me/filoghost/chestcommands/parsing/ItemMetaParser.java new file mode 100644 index 0000000..603e20a --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/parsing/ItemMetaParser.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.parsing; + +import me.filoghost.chestcommands.logging.Errors; +import me.filoghost.commons.Strings; +import me.filoghost.commons.collection.Registry; +import org.bukkit.Color; +import org.bukkit.DyeColor; +import org.bukkit.block.banner.Pattern; +import org.bukkit.block.banner.PatternType; + +public final class ItemMetaParser { + + private static final Registry DYE_COLORS_REGISTRY = Registry.fromEnumValues(DyeColor.class); + private static final Registry PATTERN_TYPES_REGISTRY = Registry.fromEnumValues(PatternType.class); + + private ItemMetaParser() {} + + + public static Color parseRGBColor(String input) throws ParseException { + String[] split = Strings.trimmedSplit(input, ","); + + if (split.length != 3) { + throw new ParseException(Errors.Parsing.invalidColorFormat); + } + + int red = parseColor(split[0], "red"); + int green = parseColor(split[1], "green"); + int blue = parseColor(split[2], "blue"); + + return Color.fromRGB(red, green, blue); + } + + private static int parseColor(String valueString, String colorName) throws ParseException { + int value; + + try { + value = NumberParser.getInteger(valueString); + } catch (ParseException e) { + throw new ParseException(Errors.Parsing.invalidColorNumber(valueString, colorName), e); + } + + if (value < 0 || value > 255) { + throw new ParseException(Errors.Parsing.invalidColorRange(valueString, colorName)); + } + + return value; + } + + public static DyeColor parseDyeColor(String input) throws ParseException { + return DYE_COLORS_REGISTRY.find(input) + .orElseThrow(() -> new ParseException(Errors.Parsing.unknownDyeColor(input))); + } + + public static Pattern parseBannerPattern(String input) throws ParseException { + String[] split = Strings.trimmedSplit(input, ":"); + if (split.length != 2) { + throw new ParseException(Errors.Parsing.invalidPatternFormat); + } + + PatternType patternType = PATTERN_TYPES_REGISTRY.find(split[0]) + .orElseThrow(() -> new ParseException(Errors.Parsing.unknownPatternType(split[0]))); + DyeColor patternColor = parseDyeColor(split[1]); + + return new Pattern(patternColor, patternType); + } +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/parsing/ItemStackParser.java b/plugin/src/main/java/me/filoghost/chestcommands/parsing/ItemStackParser.java new file mode 100644 index 0000000..2729fcc --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/parsing/ItemStackParser.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.parsing; + +import me.filoghost.chestcommands.logging.Errors; +import me.filoghost.commons.MaterialsHelper; +import me.filoghost.commons.Preconditions; +import me.filoghost.commons.Strings; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; + +public class ItemStackParser { + + private final Material material; + private int amount = 1; + private short durability = 0; + private boolean hasExplicitDurability = false; + + /** + * Reads item in the format "material:durability, amount". + */ + public ItemStackParser(String input, boolean parseAmount) throws ParseException { + Preconditions.notNull(input, "input"); + + if (parseAmount) { + // Read the optional amount + String[] splitAmount = Strings.trimmedSplit(input, ",", 2); + + if (splitAmount.length > 1) { + try { + this.amount = NumberParser.getStrictlyPositiveInteger(splitAmount[1]); + } catch (ParseException e) { + throw new ParseException(Errors.Parsing.invalidAmount(splitAmount[1]), e); + } + + // Only keep the first part as input + input = splitAmount[0]; + } + } + + + // Read the optional durability + String[] splitByColons = Strings.trimmedSplit(input, ":", 2); + + if (splitByColons.length > 1) { + try { + this.durability = NumberParser.getPositiveShort(splitByColons[1]); + } catch (ParseException e) { + throw new ParseException(Errors.Parsing.invalidDurability(splitByColons[1]), e); + } + + this.hasExplicitDurability = true; + + // Only keep the first part as input + input = splitByColons[0]; + } + + this.material = MaterialParser.parseMaterial(input); + } + + public void checkNotAir() throws ParseException { + if (MaterialsHelper.isAir(material)) { + throw new ParseException(Errors.Parsing.materialCannotBeAir); + } + } + + public Material getMaterial() { + return material; + } + + public int getAmount() { + return amount; + } + + public short getDurability() { + return durability; + } + + public boolean hasExplicitDurability() { + return hasExplicitDurability; + } + + public ItemStack createStack() { + return new ItemStack(material, amount, durability); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/parsing/MaterialParser.java b/plugin/src/main/java/me/filoghost/chestcommands/parsing/MaterialParser.java new file mode 100644 index 0000000..13900f5 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/parsing/MaterialParser.java @@ -0,0 +1,14 @@ +package me.filoghost.chestcommands.parsing; + +import me.filoghost.chestcommands.logging.Errors; +import me.filoghost.commons.MaterialsHelper; +import org.bukkit.Material; + +public class MaterialParser { + + public static Material parseMaterial(String materialName) throws ParseException { + return MaterialsHelper.matchMaterial(materialName) + .orElseThrow(() -> new ParseException(Errors.Parsing.unknownMaterial(materialName))); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/parsing/NumberParser.java b/plugin/src/main/java/me/filoghost/chestcommands/parsing/NumberParser.java new file mode 100644 index 0000000..db377b9 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/parsing/NumberParser.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.parsing; + +import me.filoghost.chestcommands.logging.Errors; + +public class NumberParser { + + public static double getStrictlyPositiveDouble(String input) throws ParseException { + double value = getDouble(input); + check(value > 0.0, Errors.Parsing.strictlyPositive); + return value; + } + + private static double getDouble(String input) throws ParseException { + try { + return Double.parseDouble(input); + } catch (NumberFormatException ex) { + throw new ParseException(Errors.Parsing.invalidDecimal); + } + } + + public static float getFloat(String input) throws ParseException { + try { + return Float.parseFloat(input); + } catch (NumberFormatException ex) { + throw new ParseException(Errors.Parsing.invalidDecimal); + } + } + + public static short getPositiveShort(String input) throws ParseException { + short value = getShort(input); + check(value >= 0, Errors.Parsing.zeroOrPositive); + return value; + } + + private static short getShort(String input) throws ParseException { + try { + return Short.parseShort(input); + } catch (NumberFormatException ex) { + throw new ParseException(Errors.Parsing.invalidShort); + } + } + + public static int getStrictlyPositiveInteger(String input) throws ParseException { + int value = getInteger(input); + check(value > 0, Errors.Parsing.strictlyPositive); + return value; + } + + public static int getInteger(String input) throws ParseException { + try { + return Integer.parseInt(input); + } catch (NumberFormatException ex) { + throw new ParseException(Errors.Parsing.invalidInteger); + } + } + + private static void check(boolean expression, String errorMessage) throws ParseException { + if (!expression) { + throw new ParseException(errorMessage); + } + } +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/parsing/ParseException.java b/plugin/src/main/java/me/filoghost/chestcommands/parsing/ParseException.java new file mode 100644 index 0000000..93746ab --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/parsing/ParseException.java @@ -0,0 +1,20 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.parsing; + +public class ParseException extends Exception { + + private static final long serialVersionUID = 1L; + + public ParseException(String message) { + super(message); + } + + public ParseException(String message, ParseException cause) { + super(message, cause); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/parsing/icon/AttributeType.java b/plugin/src/main/java/me/filoghost/chestcommands/parsing/icon/AttributeType.java new file mode 100644 index 0000000..0e64b1b --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/parsing/icon/AttributeType.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.parsing.icon; + +import me.filoghost.chestcommands.attribute.ActionsAttribute; +import me.filoghost.chestcommands.attribute.AmountAttribute; +import me.filoghost.chestcommands.attribute.AttributeErrorHandler; +import me.filoghost.chestcommands.attribute.BannerColorAttribute; +import me.filoghost.chestcommands.attribute.BannerPatternsAttribute; +import me.filoghost.chestcommands.attribute.ClickPermissionAttribute; +import me.filoghost.chestcommands.attribute.ClickPermissionMessageAttribute; +import me.filoghost.chestcommands.attribute.DurabilityAttribute; +import me.filoghost.chestcommands.attribute.EnchantmentsAttribute; +import me.filoghost.chestcommands.attribute.ExpLevelsAttribute; +import me.filoghost.chestcommands.attribute.IconAttribute; +import me.filoghost.chestcommands.attribute.KeepOpenAttribute; +import me.filoghost.chestcommands.attribute.LeatherColorAttribute; +import me.filoghost.chestcommands.attribute.LoreAttribute; +import me.filoghost.chestcommands.attribute.MaterialAttribute; +import me.filoghost.chestcommands.attribute.NBTDataAttribute; +import me.filoghost.chestcommands.attribute.NameAttribute; +import me.filoghost.chestcommands.attribute.PositionAttribute; +import me.filoghost.chestcommands.attribute.PriceAttribute; +import me.filoghost.chestcommands.attribute.RequiredItemsAttribute; +import me.filoghost.chestcommands.attribute.SkullOwnerAttribute; +import me.filoghost.chestcommands.attribute.ViewPermissionAttribute; +import me.filoghost.chestcommands.parsing.ParseException; +import me.filoghost.commons.config.ConfigValue; +import me.filoghost.commons.config.ConfigValueType; +import me.filoghost.commons.config.exception.ConfigValueException; + +import java.util.HashMap; +import java.util.Map; + +public enum AttributeType { + + POSITION_X("POSITION-X", ConfigValueType.INTEGER, PositionAttribute::new), + POSITION_Y("POSITION-Y", ConfigValueType.INTEGER, PositionAttribute::new), + MATERIAL("MATERIAL", ConfigValueType.STRING, MaterialAttribute::new), + DURABILITY("DURABILITY", ConfigValueType.SHORT, DurabilityAttribute::new), + AMOUNT("AMOUNT", ConfigValueType.INTEGER, AmountAttribute::new), + NAME("NAME", ConfigValueType.STRING, NameAttribute::new), + LORE("LORE", ConfigValueType.STRING_LIST, LoreAttribute::new), + NBT_DATA("NBT-DATA", ConfigValueType.STRING, NBTDataAttribute::new), + LEATHER_COLOR("COLOR", ConfigValueType.STRING, LeatherColorAttribute::new), + SKULL_OWNER("SKULL-OWNER", ConfigValueType.STRING, SkullOwnerAttribute::new), + BANNER_COLOR("BANNER-COLOR", ConfigValueType.STRING, BannerColorAttribute::new), + BANNER_PATTERNS("BANNER-PATTERNS", ConfigValueType.STRING_LIST, BannerPatternsAttribute::new), + PRICE("PRICE", ConfigValueType.DOUBLE, PriceAttribute::new), + EXP_LEVELS("LEVELS", ConfigValueType.INTEGER, ExpLevelsAttribute::new), + CLICK_PERMISSION("PERMISSION", ConfigValueType.STRING, ClickPermissionAttribute::new), + CLICK_PERMISSION_MESSAGE("PERMISSION-MESSAGE", ConfigValueType.STRING, ClickPermissionMessageAttribute::new), + VIEW_PERMISSION("VIEW-PERMISSION", ConfigValueType.STRING, ViewPermissionAttribute::new), + KEEP_OPEN("KEEP-OPEN", ConfigValueType.BOOLEAN, KeepOpenAttribute::new), + ACTIONS("ACTIONS", ConfigValueType.STRING_LIST, ActionsAttribute::new), + ENCHANTMENTS("ENCHANTMENTS", ConfigValueType.STRING_LIST, EnchantmentsAttribute::new), + REQUIRED_ITEMS("REQUIRED-ITEMS", ConfigValueType.STRING_LIST, RequiredItemsAttribute::new); + + private static final Map parsersByAttributeName; + static { + parsersByAttributeName = new HashMap<>(); + for (AttributeType attributeParser : values()) { + parsersByAttributeName.put(attributeParser.getAttributeName(), attributeParser); + } + } + + private final String attributeName; + private final AttributeParser attributeParser; + + AttributeType(String attributeName, ConfigValueType configValueType, AttributeFactory attributeFactory) { + this.attributeName = attributeName; + this.attributeParser = (ConfigValue configValue, AttributeErrorHandler errorHandler) -> { + return attributeFactory.create(configValue.asRequired(configValueType), errorHandler); + }; + } + + public String getAttributeName() { + return attributeName; + } + + public AttributeParser getParser() { + return attributeParser; + } + + public static AttributeType fromAttributeName(String attributeName) { + return parsersByAttributeName.get(attributeName); + } + + + @FunctionalInterface + private interface AttributeFactory { + + A create(V value, AttributeErrorHandler errorHandler) throws ParseException; + + } + + + @FunctionalInterface + public interface AttributeParser { + + IconAttribute parse(ConfigValue configValue, AttributeErrorHandler errorHandler) throws ParseException, ConfigValueException; + + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/parsing/icon/IconSettings.java b/plugin/src/main/java/me/filoghost/chestcommands/parsing/icon/IconSettings.java new file mode 100644 index 0000000..bebeb88 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/parsing/icon/IconSettings.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.parsing.icon; + +import me.filoghost.chestcommands.attribute.AttributeErrorHandler; +import me.filoghost.chestcommands.attribute.IconAttribute; +import me.filoghost.chestcommands.icon.InternalConfigurableIcon; +import me.filoghost.chestcommands.logging.Errors; +import me.filoghost.chestcommands.parsing.ParseException; +import me.filoghost.commons.config.ConfigSection; +import me.filoghost.commons.config.ConfigValue; +import me.filoghost.commons.config.exception.ConfigValueException; +import me.filoghost.commons.logging.ErrorCollector; +import org.bukkit.Material; + +import java.nio.file.Path; +import java.util.EnumMap; +import java.util.Map; + +public class IconSettings { + + private final Path menuFile; + private final String iconName; + private final Map attributes; + + public IconSettings(Path menuFile, String iconName) { + this.menuFile = menuFile; + this.iconName = iconName; + this.attributes = new EnumMap<>(AttributeType.class); + } + + public InternalConfigurableIcon createIcon() { + InternalConfigurableIcon icon = new InternalConfigurableIcon(Material.BEDROCK); + + for (IconAttribute attribute : attributes.values()) { + attribute.apply(icon); + } + + return icon; + } + + public IconAttribute getAttributeValue(AttributeType attributeType) { + return attributes.get(attributeType); + } + + public void loadFrom(ConfigSection config, ErrorCollector errorCollector) { + for (String attributeName : config.getKeys()) { + try { + AttributeType attributeType = AttributeType.fromAttributeName(attributeName); + if (attributeType == null) { + throw new ParseException(Errors.Parsing.unknownAttribute); + } + + AttributeErrorHandler errorHandler = (String listElement, ParseException e) -> { + errorCollector.add(e, Errors.Menu.invalidAttributeListElement(this, attributeName, listElement)); + }; + + ConfigValue configValue = config.get(attributeName); + IconAttribute iconAttribute = attributeType.getParser().parse(configValue, errorHandler); + attributes.put(attributeType, iconAttribute); + + } catch (ParseException | ConfigValueException e) { + errorCollector.add(e, Errors.Menu.invalidAttribute(this, attributeName)); + } + } + } + + public Path getMenuFile() { + return menuFile; + } + + public String getIconName() { + return iconName; + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/parsing/menu/ClickType.java b/plugin/src/main/java/me/filoghost/chestcommands/parsing/menu/ClickType.java new file mode 100644 index 0000000..dcca7fd --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/parsing/menu/ClickType.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.parsing.menu; + +import org.bukkit.event.block.Action; + +public enum ClickType { + + LEFT, + RIGHT, + BOTH; + + public static ClickType fromOptions(boolean left, boolean right) { + if (left && right) { + return BOTH; + } else if (left) { + return LEFT; + } else if (right) { + return RIGHT; + } else { + return null; + } + } + + public boolean isValidInteract(Action action) { + if (action == Action.LEFT_CLICK_AIR || action == Action.LEFT_CLICK_BLOCK) { + return this == LEFT || this == BOTH; + } else if (action == Action.RIGHT_CLICK_AIR || action == Action.RIGHT_CLICK_BLOCK) { + return this == RIGHT || this == BOTH; + } else { + return false; + } + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/parsing/menu/LoadedMenu.java b/plugin/src/main/java/me/filoghost/chestcommands/parsing/menu/LoadedMenu.java new file mode 100644 index 0000000..ea77b32 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/parsing/menu/LoadedMenu.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.parsing.menu; + +import com.google.common.collect.ImmutableList; +import me.filoghost.chestcommands.menu.InternalMenu; +import me.filoghost.commons.collection.CollectionUtils; + +import java.nio.file.Path; +import java.util.List; + +public class LoadedMenu { + + private final InternalMenu menu; + private final Path sourceFile; + private final ImmutableList openCommands; + private final MenuOpenItem openItem; + + public LoadedMenu(InternalMenu menu, Path menuFile, List openCommands, MenuOpenItem openItem) { + this.menu = menu; + this.sourceFile = menuFile; + this.openCommands = CollectionUtils.immutableCopy(openCommands); + this.openItem = openItem; + } + + public InternalMenu getMenu() { + return menu; + } + + public Path getSourceFile() { + return sourceFile; + } + + public ImmutableList getOpenCommands() { + return openCommands; + } + + public MenuOpenItem getOpenItem() { + return openItem; + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/parsing/menu/MenuOpenItem.java b/plugin/src/main/java/me/filoghost/chestcommands/parsing/menu/MenuOpenItem.java new file mode 100644 index 0000000..567b296 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/parsing/menu/MenuOpenItem.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.parsing.menu; + +import me.filoghost.commons.Preconditions; +import org.bukkit.Material; +import org.bukkit.event.block.Action; +import org.bukkit.inventory.ItemStack; + +public class MenuOpenItem { + + private final Material material; + private final ClickType clickType; + private short durability; + private boolean isRestrictiveDurability; + + public MenuOpenItem(Material material, ClickType clickType) { + Preconditions.checkArgumentNotAir(material, "material"); + Preconditions.notNull(clickType, "clickType"); + + this.material = material; + this.clickType = clickType; + } + + public void setRestrictiveDurability(short durability) { + this.durability = durability; + this.isRestrictiveDurability = true; + } + + public boolean matches(ItemStack item, Action action) { + if (item == null) { + return false; + } + + if (this.material != item.getType()) { + return false; + } + + if (isRestrictiveDurability && this.durability != item.getDurability()) { + return false; + } + + return clickType.isValidInteract(action); + } +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/parsing/menu/MenuParser.java b/plugin/src/main/java/me/filoghost/chestcommands/parsing/menu/MenuParser.java new file mode 100644 index 0000000..10ba38b --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/parsing/menu/MenuParser.java @@ -0,0 +1,209 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.parsing.menu; + +import me.filoghost.chestcommands.action.Action; +import me.filoghost.chestcommands.action.DisabledAction; +import me.filoghost.chestcommands.attribute.PositionAttribute; +import me.filoghost.chestcommands.logging.Errors; +import me.filoghost.chestcommands.menu.InternalMenu; +import me.filoghost.chestcommands.parsing.ActionParser; +import me.filoghost.chestcommands.parsing.ItemStackParser; +import me.filoghost.chestcommands.parsing.ParseException; +import me.filoghost.chestcommands.parsing.icon.AttributeType; +import me.filoghost.chestcommands.parsing.icon.IconSettings; +import me.filoghost.commons.Colors; +import me.filoghost.commons.config.Config; +import me.filoghost.commons.config.ConfigSection; +import me.filoghost.commons.config.EmptyConfigSection; +import me.filoghost.commons.config.exception.ConfigValueException; +import me.filoghost.commons.config.exception.MissingConfigValueException; +import me.filoghost.commons.logging.ErrorCollector; +import org.bukkit.ChatColor; + +import java.util.ArrayList; +import java.util.List; + +public class MenuParser { + + + public static LoadedMenu loadMenu(Config menuConfig, ErrorCollector errorCollector) { + MenuSettings menuSettings = loadMenuSettings(menuConfig, errorCollector); + List iconSettingsList = loadIconSettingsList(menuConfig, errorCollector); + + InternalMenu menu = new InternalMenu(menuSettings.getTitle(), menuSettings.getRows(), menuConfig.getSourceFile()); + + for (IconSettings iconSettings : iconSettingsList) { + tryAddIconToMenu(menu, iconSettings, errorCollector); + } + + menu.setRefreshTicks(menuSettings.getRefreshTicks()); + menu.setOpenActions(menuSettings.getOpenActions()); + + return new LoadedMenu(menu, menuConfig.getSourceFile(), menuSettings.getCommands(), menuSettings.getOpenItem()); + } + + + private static void tryAddIconToMenu(InternalMenu menu, IconSettings iconSettings, ErrorCollector errorCollector) { + PositionAttribute positionX = (PositionAttribute) iconSettings.getAttributeValue(AttributeType.POSITION_X); + PositionAttribute positionY = (PositionAttribute) iconSettings.getAttributeValue(AttributeType.POSITION_Y); + + if (positionX == null) { + errorCollector.add(Errors.Menu.missingAttribute(iconSettings, AttributeType.POSITION_X)); + return; + } + + if (positionY == null) { + errorCollector.add(Errors.Menu.missingAttribute(iconSettings, AttributeType.POSITION_Y)); + return; + } + + int row = positionY.getPosition() - 1; + int column = positionX.getPosition() - 1; + + if (row < 0 || row >= menu.getRowCount()) { + errorCollector.add( + Errors.Menu.invalidAttribute(iconSettings, AttributeType.POSITION_Y), + "it must be between 1 and " + menu.getRowCount()); + return; + } + if (column < 0 || column >= menu.getColumnCount()) { + errorCollector.add( + Errors.Menu.invalidAttribute(iconSettings, AttributeType.POSITION_X), + "it must be between 1 and " + menu.getColumnCount()); + return; + } + + if (menu.getIcon(row, column) != null) { + errorCollector.add(Errors.Menu.iconOverridesAnother(iconSettings)); + } + + if (iconSettings.getAttributeValue(AttributeType.MATERIAL) == null) { + errorCollector.add(Errors.Menu.missingAttribute(iconSettings, AttributeType.MATERIAL)); + } + + menu.setIcon(row, column, iconSettings.createIcon()); + } + + + private static MenuSettings loadMenuSettings(Config config, ErrorCollector errorCollector) { + ConfigSection settingsSection = config.getConfigSection(MenuSettingsNode.ROOT_SECTION); + if (settingsSection == null) { + errorCollector.add(Errors.Menu.missingSettingsSection(config.getSourceFile())); + settingsSection = new EmptyConfigSection(); + } + + String title; + try { + title = Colors.addColors(settingsSection.getRequiredString(MenuSettingsNode.NAME)); + if (title.length() > 32) { + title = title.substring(0, 32); + } + } catch (ConfigValueException e) { + title = ChatColor.DARK_RED + "No name set"; + addMenuSettingError(errorCollector, config, MenuSettingsNode.NAME, e); + } + + int rows; + try { + rows = settingsSection.getRequiredInt(MenuSettingsNode.ROWS); + if (rows <= 0) { + rows = 1; + } + } catch (ConfigValueException e) { + rows = 6; // Defaults to 6 rows + addMenuSettingError(errorCollector, config, MenuSettingsNode.ROWS, e); + } + + MenuSettings menuSettings = new MenuSettings(title, rows); + + List openCommands = settingsSection.getStringList(MenuSettingsNode.COMMANDS); + menuSettings.setCommands(openCommands); + + List serializedOpenActions = settingsSection.getStringList(MenuSettingsNode.OPEN_ACTIONS); + + if (serializedOpenActions != null) { + List openActions = new ArrayList<>(); + + for (String serializedAction : serializedOpenActions) { + if (serializedAction != null && !serializedAction.isEmpty()) { + try { + openActions.add(ActionParser.parse(serializedAction)); + } catch (ParseException e) { + errorCollector.add(e, Errors.Menu.invalidSettingListElement( + config.getSourceFile(), MenuSettingsNode.OPEN_ACTIONS, serializedAction)); + openActions.add(new DisabledAction(Errors.User.configurationError( + "an action linked to opening this menu was not executed because it was not valid"))); + } + } + } + + menuSettings.setOpenActions(openActions); + } + + String openItemMaterial = settingsSection.getString(MenuSettingsNode.OPEN_ITEM_MATERIAL); + if (openItemMaterial != null) { + boolean leftClick = settingsSection.getBoolean(MenuSettingsNode.OPEN_ITEM_LEFT_CLICK); + boolean rightClick = settingsSection.getBoolean(MenuSettingsNode.OPEN_ITEM_RIGHT_CLICK); + + if (leftClick || rightClick) { + try { + ItemStackParser itemReader = new ItemStackParser(openItemMaterial, false); + itemReader.checkNotAir(); + ClickType clickType = ClickType.fromOptions(leftClick, rightClick); + + MenuOpenItem openItem = new MenuOpenItem(itemReader.getMaterial(), clickType); + + if (itemReader.hasExplicitDurability()) { + openItem.setRestrictiveDurability(itemReader.getDurability()); + } + + menuSettings.setOpenItem(openItem); + + } catch (ParseException e) { + errorCollector.add(e, Errors.Menu.invalidSetting(config.getSourceFile(), MenuSettingsNode.OPEN_ITEM_MATERIAL)); + } + } + } + + if (settingsSection.contains(MenuSettingsNode.AUTO_REFRESH)) { + int refreshTicks = (int) (settingsSection.getDouble(MenuSettingsNode.AUTO_REFRESH) * 20.0); + if (refreshTicks < 1) { + refreshTicks = 1; + } + menuSettings.setRefreshTicks(refreshTicks); + } + + return menuSettings; + } + + private static void addMenuSettingError(ErrorCollector errorCollector, Config config, String missingSetting, ConfigValueException e) { + if (e instanceof MissingConfigValueException) { + errorCollector.add(Errors.Menu.missingSetting(config.getSourceFile(), missingSetting)); + } else { + errorCollector.add(e, Errors.Menu.invalidSetting(config.getSourceFile(), missingSetting)); + } + } + + + private static List loadIconSettingsList(Config config, ErrorCollector errorCollector) { + List iconSettingsList = new ArrayList<>(); + + for (String iconSectionName : config.getKeys()) { + if (iconSectionName.equals(MenuSettingsNode.ROOT_SECTION)) { + continue; + } + + ConfigSection iconSection = config.getConfigSection(iconSectionName); + IconSettings iconSettings = new IconSettings(config.getSourceFile(), iconSectionName); + iconSettings.loadFrom(iconSection, errorCollector); + iconSettingsList.add(iconSettings); + } + + return iconSettingsList; + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/parsing/menu/MenuSettings.java b/plugin/src/main/java/me/filoghost/chestcommands/parsing/menu/MenuSettings.java new file mode 100644 index 0000000..16ec6a5 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/parsing/menu/MenuSettings.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.parsing.menu; + +import com.google.common.collect.ImmutableList; +import me.filoghost.chestcommands.action.Action; +import me.filoghost.commons.collection.CollectionUtils; + +import java.util.List; + +public class MenuSettings { + + // Required settings + private final String title; + private final int rows; + + // Optional settings + private ImmutableList commands; + private ImmutableList openActions; + private int refreshTicks; + + private MenuOpenItem openItem; + + public MenuSettings(String title, int rows) { + this.title = title; + this.rows = rows; + } + + public String getTitle() { + return title; + } + + public int getRows() { + return rows; + } + + public void setCommands(List commands) { + this.commands = CollectionUtils.immutableCopy(commands); + } + + public ImmutableList getCommands() { + return commands; + } + + public ImmutableList getOpenActions() { + return openActions; + } + + public void setOpenActions(List openAction) { + this.openActions = CollectionUtils.immutableCopy(openAction); + } + + public int getRefreshTicks() { + return refreshTicks; + } + + public void setRefreshTicks(int refreshTicks) { + this.refreshTicks = refreshTicks; + } + + public MenuOpenItem getOpenItem() { + return openItem; + } + + public void setOpenItem(MenuOpenItem openItem) { + this.openItem = openItem; + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/parsing/menu/MenuSettingsNode.java b/plugin/src/main/java/me/filoghost/chestcommands/parsing/menu/MenuSettingsNode.java new file mode 100644 index 0000000..144d5db --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/parsing/menu/MenuSettingsNode.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.parsing.menu; + +public class MenuSettingsNode { + + public static final String + ROOT_SECTION = "menu-settings", + + NAME = "name", + ROWS = "rows", + + COMMANDS = "commands", + OPEN_ACTIONS = "open-actions", + AUTO_REFRESH = "auto-refresh", + + OPEN_ITEM_MATERIAL = "open-with-item.material", + OPEN_ITEM_LEFT_CLICK = "open-with-item.left-click", + OPEN_ITEM_RIGHT_CLICK = "open-with-item.right-click"; + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/placeholder/DefaultPlaceholder.java b/plugin/src/main/java/me/filoghost/chestcommands/placeholder/DefaultPlaceholder.java new file mode 100644 index 0000000..af5c084 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/placeholder/DefaultPlaceholder.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.placeholder; + +import me.filoghost.chestcommands.api.PlaceholderReplacer; +import me.filoghost.chestcommands.hook.VaultEconomyHook; +import org.bukkit.Bukkit; + +public enum DefaultPlaceholder { + + PLAYER("player", (player, argument) -> player.getName()), + + ONLINE("online", (player, argument) -> String.valueOf(Bukkit.getOnlinePlayers().size())), + + MAX_PLAYERS("max_players", (player, argument) -> String.valueOf(Bukkit.getMaxPlayers())), + + WORLD("world", (player, argument) -> player.getWorld().getName()), + + MONEY("money", (player, argument) -> { + if (VaultEconomyHook.INSTANCE.isEnabled()) { + return VaultEconomyHook.formatMoney(VaultEconomyHook.getMoney(player)); + } else { + return "[ECONOMY PLUGIN NOT FOUND]"; + } + }); + + + private final String identifier; + private final PlaceholderReplacer replacer; + + DefaultPlaceholder(String identifier, PlaceholderReplacer replacer) { + this.identifier = identifier; + this.replacer = replacer; + } + + public String getIdentifier() { + return identifier; + } + + public PlaceholderReplacer getReplacer() { + return replacer; + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/placeholder/PlaceholderCache.java b/plugin/src/main/java/me/filoghost/chestcommands/placeholder/PlaceholderCache.java new file mode 100644 index 0000000..a51b0a3 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/placeholder/PlaceholderCache.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.placeholder; + +import me.filoghost.chestcommands.placeholder.scanner.PlaceholderMatch; +import org.bukkit.entity.Player; + +import java.util.HashMap; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.function.Supplier; + +public class PlaceholderCache { + + private final Map> cachedReplacements; + + public PlaceholderCache() { + cachedReplacements = new WeakHashMap<>(); + } + + public String computeIfAbsent(PlaceholderMatch placeholderMatch, Player player, Supplier replacementGetter) { + return cachedReplacements + .computeIfAbsent(player, key -> new HashMap<>()) + .computeIfAbsent(placeholderMatch, key -> replacementGetter.get()); + } + + public void onTick() { + cachedReplacements.forEach((player, placeholderMap) -> placeholderMap.clear()); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/placeholder/PlaceholderManager.java b/plugin/src/main/java/me/filoghost/chestcommands/placeholder/PlaceholderManager.java new file mode 100644 index 0000000..27888bc --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/placeholder/PlaceholderManager.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.placeholder; + +import me.filoghost.chestcommands.api.PlaceholderReplacer; +import me.filoghost.chestcommands.hook.PlaceholderAPIHook; +import me.filoghost.chestcommands.placeholder.scanner.PlaceholderMatch; +import me.filoghost.chestcommands.placeholder.scanner.PlaceholderScanner; +import me.filoghost.commons.Preconditions; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +import java.util.ArrayList; +import java.util.List; + +public class PlaceholderManager { + + private static final List staticPlaceholders = new ArrayList<>(); + private static final PlaceholderRegistry relativePlaceholderRegistry = new PlaceholderRegistry(); + static { + for (DefaultPlaceholder placeholder : DefaultPlaceholder.values()) { + relativePlaceholderRegistry.registerInternalPlaceholder(placeholder.getIdentifier(), placeholder.getReplacer()); + } + } + + private static final PlaceholderCache placeholderCache = new PlaceholderCache(); + + public static boolean hasRelativePlaceholders(List list) { + for (String element : list) { + if (hasRelativePlaceholders(element)) { + return true; + } + } + return false; + } + + public static boolean hasRelativePlaceholders(String text) { + if (new PlaceholderScanner(text).anyMatch(PlaceholderManager::isValidPlaceholder)) { + return true; + } + + if (PlaceholderAPIHook.INSTANCE.isEnabled() && PlaceholderAPIHook.hasPlaceholders(text)) { + return true; + } + + return false; + } + + public static String replaceRelativePlaceholders(String text, Player player) { + text = new PlaceholderScanner(text).replace(match -> getReplacement(match, player)); + + if (PlaceholderAPIHook.INSTANCE.isEnabled()) { + text = PlaceholderAPIHook.setPlaceholders(text, player); + } + + return text; + } + + private static boolean isValidPlaceholder(PlaceholderMatch placeholderMatch) { + return relativePlaceholderRegistry.getPlaceholderReplacer(placeholderMatch) != null; + } + + private static String getReplacement(PlaceholderMatch placeholderMatch, Player player) { + PlaceholderReplacer placeholderReplacer = relativePlaceholderRegistry.getPlaceholderReplacer(placeholderMatch); + + if (placeholderReplacer == null) { + return null; // Placeholder not found + } + + return placeholderCache.computeIfAbsent(placeholderMatch, player, () -> { + return placeholderReplacer.getReplacement(player, placeholderMatch.getArgument()); + }); + } + + public static void setStaticPlaceholders(List staticPlaceholders) { + PlaceholderManager.staticPlaceholders.clear(); + PlaceholderManager.staticPlaceholders.addAll(staticPlaceholders); + } + + public static boolean hasStaticPlaceholders(List list) { + for (String element : list) { + if (hasStaticPlaceholders(element)) { + return true; + } + } + return false; + } + + public static boolean hasStaticPlaceholders(String text) { + for (StaticPlaceholder staticPlaceholder : staticPlaceholders) { + if (text.contains(staticPlaceholder.getIdentifier())) { + return true; + } + } + return false; + } + + public static String replaceStaticPlaceholders(String text) { + for (StaticPlaceholder staticPlaceholder : staticPlaceholders) { + text = text.replace(staticPlaceholder.getIdentifier(), staticPlaceholder.getReplacement()); + } + return text; + } + + public static void registerPluginPlaceholder(Plugin plugin, String identifier, PlaceholderReplacer placeholderReplacer) { + Preconditions.notNull(plugin, "plugin"); + Preconditions.notNull(identifier, "identifier"); + Preconditions.checkArgument(1 <= identifier.length() && identifier.length() <= 30, "identifier length must be between 1 and 30"); + Preconditions.checkArgument(identifier.matches("[a-zA-Z0-9_]+"), "identifier must contain only letters, numbers and underscores"); + Preconditions.notNull(placeholderReplacer, "placeholderReplacer"); + + relativePlaceholderRegistry.registerExternalPlaceholder(plugin, identifier, placeholderReplacer); + } + + public static void onTick() { + placeholderCache.onTick(); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/placeholder/PlaceholderRegistry.java b/plugin/src/main/java/me/filoghost/chestcommands/placeholder/PlaceholderRegistry.java new file mode 100644 index 0000000..73a9ea0 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/placeholder/PlaceholderRegistry.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.placeholder; + +import me.filoghost.chestcommands.api.PlaceholderReplacer; +import me.filoghost.chestcommands.placeholder.scanner.PlaceholderMatch; +import org.bukkit.plugin.Plugin; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +public class PlaceholderRegistry { + + private final Map internalPlaceholders = new HashMap<>(); + private final Map> externalPlaceholders = new HashMap<>(); + + public void registerInternalPlaceholder(String identifier, PlaceholderReplacer replacer) { + internalPlaceholders.put(identifier, replacer); + } + + public void registerExternalPlaceholder(Plugin plugin, String identifier, PlaceholderReplacer placeholderReplacer) { + externalPlaceholders + .computeIfAbsent(identifier, key -> new LinkedHashMap<>()) + .put(plugin.getName(), placeholderReplacer); + } + + public PlaceholderReplacer getPlaceholderReplacer(PlaceholderMatch placeholderMatch) { + if (placeholderMatch.getPluginNamespace() == null) { + PlaceholderReplacer internalReplacer = internalPlaceholders.get(placeholderMatch.getIdentifier()); + if (internalReplacer != null) { + return internalReplacer; + } + } + + Map externalReplacers = externalPlaceholders.get(placeholderMatch.getIdentifier()); + + // Find exact replacer if plugin name is specified + if (placeholderMatch.getPluginNamespace() != null) { + return externalReplacers.get(placeholderMatch.getPluginNamespace()); + } + + if (externalReplacers != null && !externalReplacers.isEmpty()) { + return externalReplacers.values().iterator().next(); + } + + return null; + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/placeholder/PlaceholderString.java b/plugin/src/main/java/me/filoghost/chestcommands/placeholder/PlaceholderString.java new file mode 100644 index 0000000..fd1ef4e --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/placeholder/PlaceholderString.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.placeholder; + +import org.bukkit.entity.Player; + +public class PlaceholderString { + + private final String originalString; + private final String stringWithStaticPlaceholders; + private final boolean hasDynamicPlaceholders; + + public static PlaceholderString of(String string) { + if (string != null) { + return new PlaceholderString(string); + } else { + return null; + } + } + + private PlaceholderString(String originalString) { + this.originalString = originalString; + this.stringWithStaticPlaceholders = PlaceholderManager.replaceStaticPlaceholders(originalString); + this.hasDynamicPlaceholders = PlaceholderManager.hasRelativePlaceholders(stringWithStaticPlaceholders); + } + + public String getValue(Player player) { + if (hasDynamicPlaceholders) { + return PlaceholderManager.replaceRelativePlaceholders(stringWithStaticPlaceholders, player); + } else { + return stringWithStaticPlaceholders; + } + } + + public String getOriginalValue() { + return originalString; + } + + public boolean hasDynamicPlaceholders() { + return hasDynamicPlaceholders; + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/placeholder/PlaceholderStringList.java b/plugin/src/main/java/me/filoghost/chestcommands/placeholder/PlaceholderStringList.java new file mode 100644 index 0000000..c7b4a5f --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/placeholder/PlaceholderStringList.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.placeholder; + +import com.google.common.collect.ImmutableList; +import me.filoghost.commons.Preconditions; +import me.filoghost.commons.collection.CollectionUtils; +import org.bukkit.entity.Player; + +import java.util.List; + +public class PlaceholderStringList { + + private final ImmutableList originalList; + private final ImmutableList listWithStaticPlaceholders; + private final ImmutableList placeholderStringList; + private final boolean hasDynamicPlaceholders; + + public PlaceholderStringList(List list) { + Preconditions.notNull(list, "list"); + this.originalList = ImmutableList.copyOf(list); + + // Replace static placeholders only once, if present + if (PlaceholderManager.hasStaticPlaceholders(originalList)) { + this.listWithStaticPlaceholders = CollectionUtils.transformImmutable(originalList, PlaceholderManager::replaceStaticPlaceholders); + } else { + this.listWithStaticPlaceholders = originalList; + } + + this.hasDynamicPlaceholders = PlaceholderManager.hasRelativePlaceholders(listWithStaticPlaceholders); + if (hasDynamicPlaceholders) { + this.placeholderStringList = CollectionUtils.transformImmutable(listWithStaticPlaceholders, PlaceholderString::of); + } else { + this.placeholderStringList = null; + } + } + + public ImmutableList getOriginalValue() { + return originalList; + } + + public ImmutableList getValue(Player player) { + if (hasDynamicPlaceholders) { + return CollectionUtils.transformImmutable(placeholderStringList, element -> element.getValue(player)); + } else { + return listWithStaticPlaceholders; + } + } + + public boolean hasDynamicPlaceholders() { + return hasDynamicPlaceholders; + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/placeholder/StaticPlaceholder.java b/plugin/src/main/java/me/filoghost/chestcommands/placeholder/StaticPlaceholder.java new file mode 100644 index 0000000..247fe9d --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/placeholder/StaticPlaceholder.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.placeholder; + +public class StaticPlaceholder { + + private final String identifier; + private final String replacement; + + public StaticPlaceholder(String identifier, String replacement) { + this.identifier = identifier; + this.replacement = replacement; + } + + public String getIdentifier() { + return identifier; + } + + public String getReplacement() { + return replacement; + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/placeholder/scanner/PlaceholderMatch.java b/plugin/src/main/java/me/filoghost/chestcommands/placeholder/scanner/PlaceholderMatch.java new file mode 100644 index 0000000..1eeb896 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/placeholder/scanner/PlaceholderMatch.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.placeholder.scanner; + +import me.filoghost.commons.Strings; + +import java.util.Objects; + +public class PlaceholderMatch { + + private final String pluginNamespace; + private final String identifier; + private final String argument; + + private PlaceholderMatch(String pluginNamespace, String identifier, String argument) { + this.pluginNamespace = pluginNamespace; + this.identifier = identifier; + this.argument = argument; + } + + public String getPluginNamespace() { + return pluginNamespace; + } + + public String getIdentifier() { + return identifier; + } + + public String getArgument() { + return argument; + } + + /** + * Valid formats: + * {pluginName/placeholder: argument} + * {placeholder: argument} + * {placeholder} + */ + public static PlaceholderMatch parse(String placeholderContent) { + String explicitPluginName = null; + String identifier; + String argument = null; + + if (placeholderContent.contains(":")) { + String[] parts = Strings.trimmedSplit(placeholderContent, ":", 2); + identifier = parts[0]; + argument = parts[1]; + } else { + identifier = placeholderContent; + } + + if (identifier.contains("/")) { + String[] parts = Strings.trimmedSplit(identifier, "\\/", 2); + identifier = parts[0]; + explicitPluginName = parts[1]; + } + + return new PlaceholderMatch(explicitPluginName, identifier, argument); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || this.getClass() != obj.getClass()) { + return false; + } + + PlaceholderMatch other = (PlaceholderMatch) obj; + return Objects.equals(this.pluginNamespace, other.pluginNamespace) && + Objects.equals(this.identifier, other.identifier) && + Objects.equals(this.argument, other.argument); + } + + @Override + public int hashCode() { + return Objects.hash(pluginNamespace, identifier, argument); + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/placeholder/scanner/PlaceholderScanner.java b/plugin/src/main/java/me/filoghost/chestcommands/placeholder/scanner/PlaceholderScanner.java new file mode 100644 index 0000000..15ca214 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/placeholder/scanner/PlaceholderScanner.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.placeholder.scanner; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; + +public class PlaceholderScanner { + + private final String input; + private final int inputLength; + + private int lastAppendIndex; + private int placeholderStartIndex; + private int index; + private boolean stopExecution; + + public PlaceholderScanner(String input) { + this.input = input; + this.inputLength = input.length(); + } + + public boolean anyMatch(Predicate predicate) { + AtomicBoolean matchFound = new AtomicBoolean(false); + + scan(identifier -> { + if (predicate.test(identifier)) { + matchFound.set(true); + stopExecution = true; + } + }); + + return matchFound.get(); + } + + public String replace(Function replaceFunction) { + StringBuilder output = new StringBuilder(); + + scan(identifier -> { + String replacement = replaceFunction.apply(identifier); + + if (replacement != null) { + // Append preceding text and replacement + output.append(input, lastAppendIndex, placeholderStartIndex); + output.append(replacement); + lastAppendIndex = index + 1; // Start next append after the closing tag + } + + // Else, if no replacement is found, ignore the placeholder replacement and proceed normally + }); + + // Append trailing text + if (lastAppendIndex < inputLength) { + output.append(input, lastAppendIndex, inputLength); + } + + return output.toString(); + } + + private void scan(Consumer matchCallback) { + index = 0; + placeholderStartIndex = 0; + lastAppendIndex = 0; + + boolean insidePlaceholder = false; + + while (index < inputLength) { + char currentChar = input.charAt(index); + + if (insidePlaceholder) { + if (currentChar == '}') { + // If the placeholder is "{player}" then the identifier is "player" + String placeholderContent = input.substring(placeholderStartIndex + 1, index); // Skip the opening tag + matchCallback.accept(PlaceholderMatch.parse(placeholderContent)); + if (!stopExecution) { + return; + } + + insidePlaceholder = false; + placeholderStartIndex = 0; + + } else if (currentChar == '{') { + // Nested placeholder, ignore wrapping placeholder and update placeholder start index + placeholderStartIndex = index; + } + } else { + if (currentChar == '{') { + insidePlaceholder = true; + placeholderStartIndex = index; + } + } + + index++; + } + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/task/TickingTask.java b/plugin/src/main/java/me/filoghost/chestcommands/task/TickingTask.java new file mode 100644 index 0000000..7627b9e --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/task/TickingTask.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.task; + +import me.filoghost.chestcommands.inventory.DefaultMenuView; +import me.filoghost.chestcommands.menu.InternalMenu; +import me.filoghost.chestcommands.menu.MenuManager; +import me.filoghost.chestcommands.placeholder.PlaceholderManager; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +public class TickingTask implements Runnable { + + private long currentTick; + + @Override + public void run() { + updateMenus(); + PlaceholderManager.onTick(); + + currentTick++; + } + + private void updateMenus() { + for (Player player : Bukkit.getOnlinePlayers()) { + DefaultMenuView menuView = MenuManager.getOpenMenuView(player); + + if (menuView == null || !(menuView.getMenu() instanceof InternalMenu)) { + continue; + } + + int refreshTicks = ((InternalMenu) menuView.getMenu()).getRefreshTicks(); + + if (refreshTicks > 0 && currentTick % refreshTicks == 0) { + menuView.refresh(); + } + } + } + +} diff --git a/plugin/src/main/java/me/filoghost/chestcommands/util/Utils.java b/plugin/src/main/java/me/filoghost/chestcommands/util/Utils.java new file mode 100644 index 0000000..f7a4727 --- /dev/null +++ b/plugin/src/main/java/me/filoghost/chestcommands/util/Utils.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) filoghost and contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package me.filoghost.chestcommands.util; + +import me.filoghost.commons.Strings; + +public class Utils { + + public static String formatEnum(Enum enumValue) { + return Strings.capitalizeFully(enumValue.name().replace("_", " ")); + } + + public static String addYamlExtension(String fileName) { + if (fileName == null) { + return null; + } + if (fileName.toLowerCase().endsWith(".yml")) { + return fileName; + } else { + return fileName + ".yml"; + } + } + +} diff --git a/Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/NBTByte.java b/plugin/src/main/java/me/filoghost/chestcommands/util/nbt/NBTByte.java similarity index 87% rename from Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/NBTByte.java rename to plugin/src/main/java/me/filoghost/chestcommands/util/nbt/NBTByte.java index e6488b8..91be0e2 100644 --- a/Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/NBTByte.java +++ b/plugin/src/main/java/me/filoghost/chestcommands/util/nbt/NBTByte.java @@ -1,4 +1,9 @@ -package com.gmail.filoghost.chestcommands.util.nbt; +/* + * Copyright (C) Jan Schultke + * + * SPDX-License-Identifier: MIT + */ +package me.filoghost.chestcommands.util.nbt; /** * The {@code TAG_Byte} tag. diff --git a/Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/NBTByteArray.java b/plugin/src/main/java/me/filoghost/chestcommands/util/nbt/NBTByteArray.java similarity index 91% rename from Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/NBTByteArray.java rename to plugin/src/main/java/me/filoghost/chestcommands/util/nbt/NBTByteArray.java index d58ae18..bf71e86 100644 --- a/Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/NBTByteArray.java +++ b/plugin/src/main/java/me/filoghost/chestcommands/util/nbt/NBTByteArray.java @@ -1,4 +1,9 @@ -package com.gmail.filoghost.chestcommands.util.nbt; +/* + * Copyright (C) Jan Schultke + * + * SPDX-License-Identifier: MIT + */ +package me.filoghost.chestcommands.util.nbt; import java.util.Arrays; diff --git a/Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/NBTCompound.java b/plugin/src/main/java/me/filoghost/chestcommands/util/nbt/NBTCompound.java similarity index 96% rename from Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/NBTCompound.java rename to plugin/src/main/java/me/filoghost/chestcommands/util/nbt/NBTCompound.java index aeec997..23341a4 100644 --- a/Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/NBTCompound.java +++ b/plugin/src/main/java/me/filoghost/chestcommands/util/nbt/NBTCompound.java @@ -1,6 +1,17 @@ -package com.gmail.filoghost.chestcommands.util.nbt; +/* + * Copyright (C) Jan Schultke + * + * SPDX-License-Identifier: MIT + */ +package me.filoghost.chestcommands.util.nbt; -import java.util.*; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Set; import java.util.function.BiConsumer; import java.util.regex.Pattern; @@ -14,11 +25,11 @@ public final class NBTCompound extends NBTTag { private final Map value; public NBTCompound(Map value) { - this.value = new LinkedHashMap(value); + this.value = new LinkedHashMap<>(value); } public NBTCompound() { - this.value = new LinkedHashMap(); + this.value = new LinkedHashMap<>(); } // GETTERS diff --git a/Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/NBTDouble.java b/plugin/src/main/java/me/filoghost/chestcommands/util/nbt/NBTDouble.java similarity index 88% rename from Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/NBTDouble.java rename to plugin/src/main/java/me/filoghost/chestcommands/util/nbt/NBTDouble.java index b5b1e2b..2245a48 100644 --- a/Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/NBTDouble.java +++ b/plugin/src/main/java/me/filoghost/chestcommands/util/nbt/NBTDouble.java @@ -1,4 +1,9 @@ -package com.gmail.filoghost.chestcommands.util.nbt; +/* + * Copyright (C) Jan Schultke + * + * SPDX-License-Identifier: MIT + */ +package me.filoghost.chestcommands.util.nbt; /** * The {@code TAG_Double} tag. diff --git a/Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/NBTFloat.java b/plugin/src/main/java/me/filoghost/chestcommands/util/nbt/NBTFloat.java similarity index 87% rename from Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/NBTFloat.java rename to plugin/src/main/java/me/filoghost/chestcommands/util/nbt/NBTFloat.java index 11176a3..75cb101 100644 --- a/Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/NBTFloat.java +++ b/plugin/src/main/java/me/filoghost/chestcommands/util/nbt/NBTFloat.java @@ -1,4 +1,9 @@ -package com.gmail.filoghost.chestcommands.util.nbt; +/* + * Copyright (C) Jan Schultke + * + * SPDX-License-Identifier: MIT + */ +package me.filoghost.chestcommands.util.nbt; /** * The {@code TAG_Float} tag. diff --git a/Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/NBTInt.java b/plugin/src/main/java/me/filoghost/chestcommands/util/nbt/NBTInt.java similarity index 87% rename from Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/NBTInt.java rename to plugin/src/main/java/me/filoghost/chestcommands/util/nbt/NBTInt.java index c7c459f..7fa95eb 100644 --- a/Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/NBTInt.java +++ b/plugin/src/main/java/me/filoghost/chestcommands/util/nbt/NBTInt.java @@ -1,4 +1,9 @@ -package com.gmail.filoghost.chestcommands.util.nbt; +/* + * Copyright (C) Jan Schultke + * + * SPDX-License-Identifier: MIT + */ +package me.filoghost.chestcommands.util.nbt; /** * The {@code TAG_Int} tag. diff --git a/Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/NBTIntArray.java b/plugin/src/main/java/me/filoghost/chestcommands/util/nbt/NBTIntArray.java similarity index 92% rename from Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/NBTIntArray.java rename to plugin/src/main/java/me/filoghost/chestcommands/util/nbt/NBTIntArray.java index 412d3b1..bf07c9d 100644 --- a/Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/NBTIntArray.java +++ b/plugin/src/main/java/me/filoghost/chestcommands/util/nbt/NBTIntArray.java @@ -1,4 +1,9 @@ -package com.gmail.filoghost.chestcommands.util.nbt; +/* + * Copyright (C) Jan Schultke + * + * SPDX-License-Identifier: MIT + */ +package me.filoghost.chestcommands.util.nbt; import java.util.Arrays; import java.util.Objects; diff --git a/Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/NBTList.java b/plugin/src/main/java/me/filoghost/chestcommands/util/nbt/NBTList.java similarity index 91% rename from Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/NBTList.java rename to plugin/src/main/java/me/filoghost/chestcommands/util/nbt/NBTList.java index 687ab75..7c8d5c7 100644 --- a/Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/NBTList.java +++ b/plugin/src/main/java/me/filoghost/chestcommands/util/nbt/NBTList.java @@ -1,6 +1,16 @@ -package com.gmail.filoghost.chestcommands.util.nbt; +/* + * Copyright (C) Jan Schultke + * + * SPDX-License-Identifier: MIT + */ +package me.filoghost.chestcommands.util.nbt; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; /** * The {@code TAG_List} tag. @@ -9,7 +19,7 @@ public final class NBTList extends NBTTag implements Iterable, Cloneable private NBTType type; - private final List list = new ArrayList(); + private final List list = new ArrayList<>(); /** * Creates the list with a type and a series of elements. diff --git a/Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/NBTLong.java b/plugin/src/main/java/me/filoghost/chestcommands/util/nbt/NBTLong.java similarity index 87% rename from Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/NBTLong.java rename to plugin/src/main/java/me/filoghost/chestcommands/util/nbt/NBTLong.java index 0d2a50a..33cca56 100644 --- a/Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/NBTLong.java +++ b/plugin/src/main/java/me/filoghost/chestcommands/util/nbt/NBTLong.java @@ -1,4 +1,9 @@ -package com.gmail.filoghost.chestcommands.util.nbt; +/* + * Copyright (C) Jan Schultke + * + * SPDX-License-Identifier: MIT + */ +package me.filoghost.chestcommands.util.nbt; /** * The {@code TAG_Long} tag. diff --git a/Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/NBTLongArray.java b/plugin/src/main/java/me/filoghost/chestcommands/util/nbt/NBTLongArray.java similarity index 91% rename from Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/NBTLongArray.java rename to plugin/src/main/java/me/filoghost/chestcommands/util/nbt/NBTLongArray.java index 9a4ff37..d605bc1 100644 --- a/Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/NBTLongArray.java +++ b/plugin/src/main/java/me/filoghost/chestcommands/util/nbt/NBTLongArray.java @@ -1,4 +1,9 @@ -package com.gmail.filoghost.chestcommands.util.nbt; +/* + * Copyright (C) Jan Schultke + * + * SPDX-License-Identifier: MIT + */ +package me.filoghost.chestcommands.util.nbt; import java.util.Arrays; diff --git a/Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/NBTShort.java b/plugin/src/main/java/me/filoghost/chestcommands/util/nbt/NBTShort.java similarity index 87% rename from Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/NBTShort.java rename to plugin/src/main/java/me/filoghost/chestcommands/util/nbt/NBTShort.java index c998834..5287a78 100644 --- a/Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/NBTShort.java +++ b/plugin/src/main/java/me/filoghost/chestcommands/util/nbt/NBTShort.java @@ -1,4 +1,9 @@ -package com.gmail.filoghost.chestcommands.util.nbt; +/* + * Copyright (C) Jan Schultke + * + * SPDX-License-Identifier: MIT + */ +package me.filoghost.chestcommands.util.nbt; /** * The {@code TAG_Short} tag. diff --git a/Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/NBTString.java b/plugin/src/main/java/me/filoghost/chestcommands/util/nbt/NBTString.java similarity index 90% rename from Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/NBTString.java rename to plugin/src/main/java/me/filoghost/chestcommands/util/nbt/NBTString.java index 4c21ec0..7a8572a 100644 --- a/Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/NBTString.java +++ b/plugin/src/main/java/me/filoghost/chestcommands/util/nbt/NBTString.java @@ -1,4 +1,9 @@ -package com.gmail.filoghost.chestcommands.util.nbt; +/* + * Copyright (C) Jan Schultke + * + * SPDX-License-Identifier: MIT + */ +package me.filoghost.chestcommands.util.nbt; /** * The {@code TAG_String} tag. diff --git a/Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/NBTTag.java b/plugin/src/main/java/me/filoghost/chestcommands/util/nbt/NBTTag.java similarity index 89% rename from Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/NBTTag.java rename to plugin/src/main/java/me/filoghost/chestcommands/util/nbt/NBTTag.java index 89619ae..e57168c 100644 --- a/Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/NBTTag.java +++ b/plugin/src/main/java/me/filoghost/chestcommands/util/nbt/NBTTag.java @@ -1,4 +1,9 @@ -package com.gmail.filoghost.chestcommands.util.nbt; +/* + * Copyright (C) Jan Schultke + * + * SPDX-License-Identifier: MIT + */ +package me.filoghost.chestcommands.util.nbt; /** * An abstract NBT-Tag. diff --git a/Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/NBTType.java b/plugin/src/main/java/me/filoghost/chestcommands/util/nbt/NBTType.java similarity index 96% rename from Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/NBTType.java rename to plugin/src/main/java/me/filoghost/chestcommands/util/nbt/NBTType.java index 218e650..636d9e9 100644 --- a/Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/NBTType.java +++ b/plugin/src/main/java/me/filoghost/chestcommands/util/nbt/NBTType.java @@ -1,4 +1,9 @@ -package com.gmail.filoghost.chestcommands.util.nbt; +/* + * Copyright (C) Jan Schultke + * + * SPDX-License-Identifier: MIT + */ +package me.filoghost.chestcommands.util.nbt; /** *

@@ -167,7 +172,7 @@ public enum NBTType { /** * Returns whether this tag type is primitive, meaning that it is not a {@link NBTByteArray}, {@link NBTIntArray}, - * {@link NBTList}, {@link NBTCompound} or {@link NBTEnd}. + * {@link NBTList}, {@link NBTCompound}. * * @return whether this type is numeric */ diff --git a/Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/parser/MojangsonParseException.java b/plugin/src/main/java/me/filoghost/chestcommands/util/nbt/parser/MojangsonParseException.java similarity index 72% rename from Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/parser/MojangsonParseException.java rename to plugin/src/main/java/me/filoghost/chestcommands/util/nbt/parser/MojangsonParseException.java index c061619..3fd8cac 100644 --- a/Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/parser/MojangsonParseException.java +++ b/plugin/src/main/java/me/filoghost/chestcommands/util/nbt/parser/MojangsonParseException.java @@ -1,6 +1,9 @@ -package com.gmail.filoghost.chestcommands.util.nbt.parser; - -import org.bukkit.ChatColor; +/* + * Copyright (C) Jan Schultke + * + * SPDX-License-Identifier: MIT + */ +package me.filoghost.chestcommands.util.nbt.parser; import java.io.IOException; @@ -18,8 +21,8 @@ public class MojangsonParseException extends IOException { if (i > 35) { builder.append("..."); } - builder.append(content.substring(Math.max(0, i - 35), i)); - builder.append(ChatColor.GOLD + "<--[HERE]"); + builder.append(content, Math.max(0, i - 35), i); + builder.append("<--[HERE]"); return builder.toString(); } diff --git a/Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/parser/MojangsonParser.java b/plugin/src/main/java/me/filoghost/chestcommands/util/nbt/parser/MojangsonParser.java similarity index 91% rename from Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/parser/MojangsonParser.java rename to plugin/src/main/java/me/filoghost/chestcommands/util/nbt/parser/MojangsonParser.java index a08a744..e07a864 100644 --- a/Plugin/src/main/java/com/gmail/filoghost/chestcommands/util/nbt/parser/MojangsonParser.java +++ b/plugin/src/main/java/me/filoghost/chestcommands/util/nbt/parser/MojangsonParser.java @@ -1,6 +1,24 @@ -package com.gmail.filoghost.chestcommands.util.nbt.parser; +/* + * Copyright (C) Jan Schultke + * + * SPDX-License-Identifier: MIT + */ +package me.filoghost.chestcommands.util.nbt.parser; -import com.gmail.filoghost.chestcommands.util.nbt.*; +import me.filoghost.chestcommands.util.nbt.NBTByte; +import me.filoghost.chestcommands.util.nbt.NBTByteArray; +import me.filoghost.chestcommands.util.nbt.NBTCompound; +import me.filoghost.chestcommands.util.nbt.NBTDouble; +import me.filoghost.chestcommands.util.nbt.NBTFloat; +import me.filoghost.chestcommands.util.nbt.NBTInt; +import me.filoghost.chestcommands.util.nbt.NBTIntArray; +import me.filoghost.chestcommands.util.nbt.NBTList; +import me.filoghost.chestcommands.util.nbt.NBTLong; +import me.filoghost.chestcommands.util.nbt.NBTLongArray; +import me.filoghost.chestcommands.util.nbt.NBTShort; +import me.filoghost.chestcommands.util.nbt.NBTString; +import me.filoghost.chestcommands.util.nbt.NBTTag; +import me.filoghost.chestcommands.util.nbt.NBTType; import java.util.ArrayList; import java.util.List; @@ -230,7 +248,7 @@ public final class MojangsonParser { } private Number[] parseNumArray(NBTType arrayType, NBTType primType) throws MojangsonParseException { - List result = new ArrayList(); + List result = new ArrayList<>(); while (currentChar() != ']') { NBTTag element = parseAnything(); NBTType elementType = element.getType(); @@ -254,7 +272,7 @@ public final class MojangsonParser { } expectChar(']'); - return result.toArray(new Number[result.size()]); + return result.toArray(new Number[0]); } // CHARACTER NAVIGATION diff --git a/plugin/src/main/resources/custom-placeholders.yml b/plugin/src/main/resources/custom-placeholders.yml new file mode 100644 index 0000000..afd0b34 --- /dev/null +++ b/plugin/src/main/resources/custom-placeholders.yml @@ -0,0 +1,14 @@ +# +# This is the configuration file for custom placeholders. +# Dynamic placeholders are {online}, {max_players}, ... +# +# You can use these to avoid repeating text multiple times, +# and edit it from a single location. +# +# Another use would be for defining colors, symbols, or text decorations. +# +placeholders: + '{custom_placeholder}': 'This line is a custom placeholder.' + '{primary_color}': '&b' + '{secondary_color}': '&7' + '{separator}': '&8[====|====|====|====|====|====|====|====]' \ No newline at end of file diff --git a/Plugin/src/main/resources/menu/example.yml b/plugin/src/main/resources/menu/example.yml similarity index 86% rename from Plugin/src/main/resources/menu/example.yml rename to plugin/src/main/resources/menu/example.yml index e33680e..ec71c79 100644 --- a/Plugin/src/main/resources/menu/example.yml +++ b/plugin/src/main/resources/menu/example.yml @@ -21,12 +21,12 @@ menu-settings: # OPTIONAL # How frequently the menu will be refreshed, in seconds. - # Useful if you have variables in icon descriptions. + # Useful if you have placeholders in icon descriptions. auto-refresh: 5 # OPTIONAL - # This command command will be executed when the menu is opened. - # Supports all the icon command types. + # These actions will be executed when the menu is opened. + # Supports all the icon action types. open-actions: - 'tell: &eYou opened the example menu.' @@ -129,16 +129,18 @@ economy-give: MATERIAL: gold ingot POSITION-X: 8 POSITION-Y: 1 - NAME: '&eEconomy & Give command' + NAME: '&eEconomy & Give action' LORE: - - 'This command will be executed' + - 'This action will be executed' - 'only if you have at least 50$.' - 'It gives you a gold ingot.' + - '' + - 'Current money: {money}' PRICE: 50 ACTIONS: - 'tell: &aYou have paid 50$' - 'give: gold ingot' - + KEEP-OPEN: true economy-take: MATERIAL: gold ingot @@ -146,13 +148,16 @@ economy-take: POSITION-Y: 1 NAME: '&eEconomy & Required item' LORE: - - 'This command is the opposite of the previous.' + - 'This icon is the opposite of the previous.' - 'It will take you a gold ingot and give you $50.' + - '' + - 'Current money: {money}' REQUIRED-ITEMS: - gold ingot ACTIONS: - 'tell: &aYou have been paid 50$' - 'give-money: 50' + KEEP-OPEN: true formatting-codes: @@ -219,6 +224,19 @@ nbt-tags: NBT-DATA: '{display: {color: 8910400}}' +custom-placeholder: + MATERIAL: compass + POSITION-X: 6 + POSITION-Y: 2 + NAME: '{primary_color}Custom placeholders' + LORE: + - '{separator}' + - '{secondary_color}This item contains custom placeholders,' + - '{secondary_color}defined in custom-placeholders.yml.' + - '' + - '{custom_placeholder}' + + close-without-actions: MATERIAL: redstone lamp POSITION-X: 9 diff --git a/Plugin/src/main/resources/plugin.yml b/plugin/src/main/resources/plugin.yml similarity index 79% rename from Plugin/src/main/resources/plugin.yml rename to plugin/src/main/resources/plugin.yml index 5ad1b3c..eb33aed 100644 --- a/Plugin/src/main/resources/plugin.yml +++ b/plugin/src/main/resources/plugin.yml @@ -1,6 +1,6 @@ name: ChestCommands -main: com.gmail.filoghost.chestcommands.ChestCommands -version: ${project.version} +main: me.filoghost.chestcommands.ChestCommands +version: ${project.pluginVersion} api-version: 1.13 softdepend: [Vault, BarAPI, PlaceholderAPI] diff --git a/pom.xml b/pom.xml index aaba725..04da293 100644 --- a/pom.xml +++ b/pom.xml @@ -3,10 +3,10 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - com.gmail.filoghost.chestcommands + me.filoghost.chestcommands chestcommands-parent ChestCommands Parent - 3.3.1 + 4.0.0-SNAPSHOT pom https://dev.bukkit.org/bukkit-plugins/chest-commands @@ -17,24 +17,119 @@ scm:git:git@github.com:filoghost/ChestCommands.git + + + GNU General Public License, Version 3 + https://www.gnu.org/licenses/gpl-3.0.txt + repo + + + UTF-8 - 1.6 - 1.6 + 1.8 + 1.8 true - 1.8.8-R0.1-SNAPSHOT - Plugin - + commons + api + plugin + + + + + spigot-repo + https://hub.spigotmc.org/nexus/content/repositories/snapshots/ + + + + vault-repo + http://nexus.hc.to/content/repositories/pub_releases + + + + confuser-repo + https://ci.frostcast.net/plugin/repository/everything/ + + + + codemc-repo + https://repo.codemc.io/repository/maven-public/ + + + + placeholderapi-repo + https://repo.extendedclip.com/content/repositories/placeholderapi/ + + + + + + + org.bukkit + bukkit + 1.8-R0.1-SNAPSHOT + provided + + + + net.milkbowl.vault + VaultAPI + 1.6 + provided + + + + me.confuser + BarAPI + 3.5 + provided + + + + me.clip + placeholderapi + 2.9.2 + provided + + + + org.bstats + bstats-bukkit-lite + 1.7 + + + + me.filoghost.updatechecker + updatechecker + 1.0.1 + + + + ${project.groupId} + chestcommands-api + ${project.version} + + + + ${project.groupId} + chestcommands-commons + ${project.version} + + + + clean package + org.apache.maven.plugins maven-jar-plugin + 3.2.0 false @@ -44,8 +139,77 @@ + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.3 + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.0.0-M3 + + + + org.apache.maven.plugins + maven-antrun-plugin + 3.0.0 + + + + + org.eclipse.m2e + lifecycle-mapping + 1.0.0 + + + + + + org.apache.maven.plugins + maven-antrun-plugin + [0.0,) + + run + + + + + + + + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + + + + enforce + + + + + + [3.6.0,) + + + [1.8,) + + + + + + +