From 6f9301a0bbcca4f360059609e835e9ba5bfbca94 Mon Sep 17 00:00:00 2001 From: Alexander Date: Tue, 22 Jun 2021 10:03:16 +0100 Subject: [PATCH] Refactor config parsing --- .../mc/civmodcore/CivModCorePlugin.java | 6 +- .../mc/civmodcore/CoreConfigManager.java | 134 ----- .../mc/civmodcore/config/ConfigHelper.java | 539 +++++++++++++++++- .../mc/civmodcore/config/ConfigParser.java | 82 +++ .../mc/civmodcore/config/ConfigSection.java | 24 + .../civmodcore/utilities/ConfigParsing.java | 472 --------------- .../src/main/resources/config.yml | 24 +- .../mc/civmodcore/config/ConfigTests.java | 26 + 8 files changed, 681 insertions(+), 626 deletions(-) delete mode 100644 plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/CoreConfigManager.java create mode 100644 plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/config/ConfigParser.java create mode 100644 plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/config/ConfigSection.java delete mode 100644 plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/utilities/ConfigParsing.java create mode 100644 plugins/civmodcore-paper/src/test/java/vg/civcraft/mc/civmodcore/config/ConfigTests.java diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/CivModCorePlugin.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/CivModCorePlugin.java index fc1ecc8df..fecd8db8f 100644 --- a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/CivModCorePlugin.java +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/CivModCorePlugin.java @@ -3,10 +3,10 @@ package vg.civcraft.mc.civmodcore; import java.sql.SQLException; import net.md_5.bungee.api.ChatColor; import org.bukkit.Bukkit; -import org.bukkit.configuration.serialization.ConfigurationSerialization; import org.bukkit.entity.HumanEntity; import vg.civcraft.mc.civmodcore.chat.dialog.DialogManager; import vg.civcraft.mc.civmodcore.commands.CommandManager; +import vg.civcraft.mc.civmodcore.dao.DatabaseCredentials; import vg.civcraft.mc.civmodcore.dao.ManagedDatasource; import vg.civcraft.mc.civmodcore.events.CustomEventMapper; import vg.civcraft.mc.civmodcore.inventory.items.EnchantUtils; @@ -53,7 +53,7 @@ public final class CivModCorePlugin extends ACivMod { super.onEnable(); // Load Database try { - this.database = (ManagedDatasource) getConfig().get("database"); + this.database = ManagedDatasource.construct(this, (DatabaseCredentials) getConfig().get("database")); if (this.database != null) { CMCWorldDAO dao = new CMCWorldDAO(this.database, this); if (dao.updateDatabase()) { @@ -66,7 +66,7 @@ public final class CivModCorePlugin extends ACivMod { } } } - catch (Exception error) { + catch (final Exception error) { warning("Cannot get database from config.", error); this.database = null; } diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/CoreConfigManager.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/CoreConfigManager.java deleted file mode 100644 index db7b4aef8..000000000 --- a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/CoreConfigManager.java +++ /dev/null @@ -1,134 +0,0 @@ -package vg.civcraft.mc.civmodcore; - -import com.google.common.base.Strings; -import java.util.ArrayList; -import java.util.List; -import java.util.function.Function; -import java.util.logging.Logger; -import org.bukkit.ChatColor; -import org.bukkit.Material; -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.configuration.file.FileConfiguration; -import vg.civcraft.mc.civmodcore.inventory.items.MaterialUtils; - -/** - * This is a config parsing class intended to make handling configs a little easier, and automatically parse commonly - * seen things within civ configs. - */ -public class CoreConfigManager { - - protected final ACivMod plugin; - protected final Logger logger; - - private boolean debug; - private boolean logReplies; - - public CoreConfigManager(ACivMod plugin) { - this.plugin = plugin; - this.logger = plugin.getLogger(); - } - - /** - * Parses this civ plugin's config. It will also save a default config (if one is not already present) and reload - * the config, so there's no need to do so yourself beforehand. - * - * @return Returns true if the config was successfully parsed. - */ - public final boolean parse() { - this.plugin.info(ChatColor.BLUE + "Parsing config."); - this.plugin.saveDefaultConfig(); - FileConfiguration config = this.plugin.getConfig(); - // Parse debug value - this.debug = config.getBoolean("debug", false); - this.plugin.info("Debug mode: " + (this.debug ? "enabled" : "disabled")); - // Parse reply logging value - this.logReplies = config.getBoolean("logReplies", false); - this.plugin.info("Logging replies: " + (this.logReplies ? "enabled" : "disabled")); - // Allow child class parsing - boolean worked = parseInternal(config); - if (worked) { - plugin.info(ChatColor.BLUE + "Config parsed."); - } - else { - plugin.warning("Failed to parse config!"); - } - return worked; - } - - /** - * An internal parser method intended to be overriden by child classes. - * - * @param config The root config section. - * @return Return true if the - */ - protected boolean parseInternal(ConfigurationSection config) { - return true; - } - - /** - * This should reset all config values back to their defaults. Child classes should override this if they parse - * additional values that should be reset. - */ - public void reset() { - this.debug = false; - this.logReplies = false; - } - - // ------------------------------------------------------------ // - // Getters - // ------------------------------------------------------------ // - - public final boolean isDebugEnabled() { - return this.debug; - } - - public final boolean logReplies() { - return this.logReplies; - } - - // ------------------------------------------------------------ // - // Predefined parsing utilities - // ------------------------------------------------------------ // - - /** - * Attempts to retrieve a list of materials from a config section. - * - * @param config The config section. - * @param key The key of the list. - * @return Returns a list of materials, or null. - */ - protected final List parseMaterialList(ConfigurationSection config, String key) { - return parseList(config, key, slug -> { - Material found = MaterialUtils.getMaterial(slug); - if (found == null) { - this.logger.warning("Could not parse material \"" + slug + "\" at: " + config.getCurrentPath()); - return null; - } - return found; - }); - } - - /** - * Attempts to retrieve a list from a config section. - * - * @param The type to parse the list into. - * @param config The config section. - * @param key The key of the list. - * @param parser The parser to convert the string value into the correct type. - * @return Returns a list, or null. - */ - protected static List parseList(ConfigurationSection config, String key, Function parser) { - if (config == null || Strings.isNullOrEmpty(key) || !config.isList(key) || parser == null) { - return null; - } - List result = new ArrayList<>(); - for (String entry : config.getStringList(key)) { - T item = parser.apply(entry); - if (item != null) { - result.add(item); - } - } - return result; - } - -} diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/config/ConfigHelper.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/config/ConfigHelper.java index 253f7ca3e..9f26a8331 100644 --- a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/config/ConfigHelper.java +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/config/ConfigHelper.java @@ -1,13 +1,65 @@ package vg.civcraft.mc.civmodcore.config; -import com.google.common.base.Preconditions; +import com.google.common.collect.Lists; import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.logging.Logger; import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import lombok.NonNull; +import lombok.experimental.UtilityClass; +import org.bukkit.Bukkit; +import org.bukkit.Color; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.World; import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.EnchantmentStorageMeta; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.inventory.meta.LeatherArmorMeta; +import org.bukkit.inventory.meta.PotionMeta; +import org.bukkit.potion.PotionData; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; +import org.bukkit.potion.PotionType; +import vg.civcraft.mc.civmodcore.inventory.items.EnchantUtils; +import vg.civcraft.mc.civmodcore.inventory.items.ItemMap; +import vg.civcraft.mc.civmodcore.inventory.items.MaterialUtils; +import vg.civcraft.mc.civmodcore.world.model.EllipseArea; +import vg.civcraft.mc.civmodcore.world.model.GlobalYLimitedArea; +import vg.civcraft.mc.civmodcore.world.model.IArea; +import vg.civcraft.mc.civmodcore.world.model.RectangleArea; +@UtilityClass public final class ConfigHelper { + private static final Logger LOGGER = Bukkit.getLogger(); + + /** + * Retrieves the configuration section at the given key on the given configuration section. + * + * @param config The config to get the keyed section from. + * @param key The key of the section to retrieve. + * @return Returns the configuration section at the given key, or returns a new, empty section. + */ + @Nonnull + public static ConfigurationSection getSection(@NonNull final ConfigurationSection config, + @NonNull final String key) { + ConfigurationSection found = config.getConfigurationSection(key); + if (found == null) { + found = config.createSection(key); + } + return found; + } + /** * Retrieves a string list from a given config section. If the keyed value is a standalone string instead of a * list, that value will be converted to a list. @@ -16,9 +68,9 @@ public final class ConfigHelper { * @param key The key to get the list of. * @return Returns a list of strings, which is never null. */ - public static List getStringList(@Nonnull final ConfigurationSection config, @Nonnull final String key) { - Preconditions.checkNotNull(config, "Config cannot be null!"); - Preconditions.checkNotNull(key, "Key cannot be null!"); + @Nonnull + public static List getStringList(@NonNull final ConfigurationSection config, + @NonNull final String key) { if (config.isString(key)) { final var list = new ArrayList(1); list.add(config.getString(key)); @@ -27,4 +79,483 @@ public final class ConfigHelper { return config.getStringList(key); } + /** + * Attempts to retrieve a list from a config section. + * + * @param The type to parse the list into. + * @param config The config section. + * @param key The key of the list. + * @param parser The parser to convert the string value into the correct type. + * @return Returns a list, or null. + */ + @Nonnull + public static List parseList(@NonNull final ConfigurationSection config, + @NonNull final String key, + @NonNull final Function parser) { + if (!config.isList(key)) { + return new ArrayList<>(0); + } + final var entries = getStringList(config, key); + final var result = new ArrayList(entries.size()); + for (final String entry : entries) { + final T item = parser.apply(entry); + if (item != null) { + result.add(item); + } + } + return result; + } + + /** + * Attempts to retrieve a list of materials from a config section. + * + * @param config The config section. + * @param key The key of the list. + * @return Returns a list of materials, or null. + */ + @Nonnull + public static List parseMaterialList(@Nonnull final ConfigurationSection config, + @Nonnull final String key) { + return parseList(config, key, MaterialUtils::getMaterial); + } + + /** + * Creates an item map containing all the items listed in the given config + * section + * + * @param config ConfigurationSection to parse the items from + * @return The item map created + */ + @Nonnull + public static ItemMap parseItemMap(@Nullable final ConfigurationSection config) { + final var result = new ItemMap(); + if (config == null) { + return result; + } + for (final String key : config.getKeys(false)) { + ConfigurationSection current = config.getConfigurationSection(key); + ItemMap partMap = parseItemMapDirectly(current); + result.merge(partMap); + } + return result; + } + + @Nonnull + public static ItemMap parseItemMapDirectly(@Nullable final ConfigurationSection current) { + ItemMap im = new ItemMap(); + if (current == null) { + return im; + } + Material m = null; + try { + m = Material.valueOf(current.getString("material")); + } catch (IllegalArgumentException iae) { + m = null; + } finally { + if (m == null) { + LOGGER.severe("Failed to find material " + current.getString("material") + " in section " + current.getCurrentPath()); + return im; + } + } + ItemStack toAdd = new ItemStack(m); + if (current.isInt("durability")) { + LOGGER.warning("Item durability as specified at " + current.getCurrentPath() + " is no longer supported"); + } + ItemMeta meta = toAdd.getItemMeta(); + if (meta == null) { + LOGGER.severe("No item meta found for" + current.getCurrentPath()); + } else { + String name = current.getString("name"); + if (name != null) { + meta.setDisplayName(name); + } + List lore = current.getStringList("lore"); + if (lore != null) { + meta.setLore(lore); + } + if (current.isBoolean("unbreakable")) { + meta.setUnbreakable(current.getBoolean("unbreakable")); + } + if (current.isBoolean("hideFlags") && current.getBoolean("hideFlags")) { + for (ItemFlag flag : ItemFlag.values()) { + meta.addItemFlags(flag); + } + } + if (current.contains("enchants")) { + for (String enchantKey : current.getConfigurationSection("enchants").getKeys(false)) { + ConfigurationSection enchantConfig = current.getConfigurationSection("enchants") + .getConfigurationSection(enchantKey); + if (!enchantConfig.isString("enchant")) { + LOGGER.warning("No enchant specified for enchantment entry at " + enchantConfig.getCurrentPath() + + ". Entry was ignored"); + continue; + } + Enchantment enchant; + enchant = Enchantment + .getByKey(NamespacedKey.minecraft((enchantConfig.getString("enchant").toLowerCase()))); + if (enchant == null) { + LOGGER.severe("Failed to parse enchantment " + enchantConfig.getString("enchant") + + ", the entry was ignored"); + continue; + } + int level = enchantConfig.getInt("level", 1); + meta.addEnchant(enchant, level, true); + } + } + if (m == Material.LEATHER_BOOTS || m == Material.LEATHER_CHESTPLATE || m == Material.LEATHER_HELMET + || m == Material.LEATHER_LEGGINGS) { + ConfigurationSection color = current.getConfigurationSection("color"); + Color leatherColor = null; + if (color != null) { + int red = color.getInt("red"); + int blue = color.getInt("blue"); + int green = color.getInt("green"); + leatherColor = Color.fromRGB(red, green, blue); + } else { + String hexColorCode = current.getString("color"); + if (hexColorCode != null) { + Integer hexColor = Integer.parseInt(hexColorCode, 16); + if (hexColor != null) { + leatherColor = Color.fromRGB(hexColor); + } + } + } + if (leatherColor != null) { + ((LeatherArmorMeta) meta).setColor(leatherColor); + } + } + if (m == Material.ENCHANTED_BOOK) { + ConfigurationSection storedEnchantSection = current.getConfigurationSection("stored_enchants"); + if (storedEnchantSection != null) { + EnchantmentStorageMeta enchantMeta = (EnchantmentStorageMeta) meta; + for (String sEKey : storedEnchantSection.getKeys(false)) { + ConfigurationSection currentStoredEnchantSection = storedEnchantSection + .getConfigurationSection(sEKey); + if (currentStoredEnchantSection != null) { + Enchantment enchant = EnchantUtils.getEnchantment(currentStoredEnchantSection.getString("enchant")); + int level = currentStoredEnchantSection.getInt("level", 1); + if (enchant != null) { + enchantMeta.addStoredEnchant(enchant, level, true); + } + else { + LOGGER.severe("Failed to parse enchantment at " + currentStoredEnchantSection.getCurrentPath() + + ", it was not applied"); + } + } + } + } + } + if (m == Material.POTION || m == Material.SPLASH_POTION || m == Material.LINGERING_POTION + || m == Material.TIPPED_ARROW) { + ConfigurationSection potion = current.getConfigurationSection("potion_effects"); + if (potion != null) { + PotionType potType; + try { + potType = PotionType.valueOf(potion.getString("type", "AWKWARD")); + } catch (IllegalArgumentException e) { + LOGGER.warning("Expected potion type at " + potion.getCurrentPath() + ", but " + + potion.getString("type") + " is not a valid potion type"); + potType = PotionType.AWKWARD; + } + boolean upgraded = potion.getBoolean("upgraded", false); + boolean extended = potion.getBoolean("extended", false); + PotionMeta potMeta = (PotionMeta) meta; + potMeta.setBasePotionData(new PotionData(potType, extended, upgraded)); + ConfigurationSection customEffects = potion.getConfigurationSection("custom_effects"); + if (customEffects != null) { + List pots = parsePotionEffects(potion); + for (PotionEffect pe : pots) { + potMeta.addCustomEffect(pe, true); + } + } + } + + } + toAdd.setItemMeta(meta); + if (current.contains("nbt")) { + toAdd = ItemMap.enrichWithNBT(toAdd, 1, current.getConfigurationSection("nbt").getValues(true)); + } + } + // Setting amount must be last just in cast enrichWithNBT is called, + // which + // resets the amount to 1. + int amount = current.getInt("amount", 1); + toAdd.setAmount(amount); + im.addItemStack(toAdd); + return im; + } + + public static int parseTimeAsTicks(@Nonnull final String arg) { + return (int) (parseTime(arg, TimeUnit.MILLISECONDS) / 50L); + } + + public static long parseTime(@Nonnull final String arg, + @Nonnull final TimeUnit unit) { + long millis = parseTime(arg); + return unit.convert(millis, TimeUnit.MILLISECONDS); + } + + /** + * Parses a time value specified in a config. This allows to specify human + * readable time values easily, instead of having to specify every amount in + * ticks or seconds. The unit of a number specifed by the letter added after it, + * for example 5h means 5 hours or 34s means 34 seconds. Possible modifiers are: + * t (ticks), s (seconds), m (minutes), h (hours) and d (days) + *

+ * Additionally you can combine those amounts in any way you want, for example + * you can specify 3h5m43s as 3 hours, 5 minutes and 43 seconds. This doesn't + * have to be sorted and may even list the same unit multiple times for + * different values, but the values are not allowed to be separated by anything + * + * @param input Parsed string containing the time format + * @return How many milliseconds the given time value is + */ + public static long parseTime(@Nonnull String input) { + input = input.replace(" ", "").replace(",", "").toLowerCase(); + long result = 0; + try { + result += Long.parseLong(input); + return result; + } catch (NumberFormatException e) { + } + while (!input.equals("")) { + String typeSuffix = getSuffix(input, Character::isLetter); + input = input.substring(0, input.length() - typeSuffix.length()); + String numberSuffix = getSuffix(input, Character::isDigit); + input = input.substring(0, input.length() - numberSuffix.length()); + long duration; + if (numberSuffix.length() == 0) { + duration = 1; + } else { + duration = Long.parseLong(numberSuffix); + } + switch (typeSuffix) { + case "ms": + case "milli": + case "millis": + result += duration; + break; + case "s": // seconds + case "sec": + case "second": + case "seconds": + result += TimeUnit.SECONDS.toMillis(duration); + break; + case "m": // minutes + case "min": + case "minute": + case "minutes": + result += TimeUnit.MINUTES.toMillis(duration); + break; + case "h": // hours + case "hour": + case "hours": + result += TimeUnit.HOURS.toMillis(duration); + break; + case "d": // days + case "day": + case "days": + result += TimeUnit.DAYS.toMillis(duration); + break; + case "w": // weeks + case "week": + case "weeks": + result += TimeUnit.DAYS.toMillis(duration * 7); + break; + case "month": // weeks + case "months": + result += TimeUnit.DAYS.toMillis(duration * 30); + break; + case "y": + case "year": + case "years": + result += TimeUnit.DAYS.toMillis(duration * 365); + break; + case "never": + case "inf": + case "infinite": + case "perm": + case "perma": + case "forever": + // 1000 years counts as perma + result += TimeUnit.DAYS.toMillis(365 * 1000); + default: + // just ignore it + } + } + return result; + } + + @Nonnull + private static String getSuffix(@Nonnull final String arg, + @Nonnull final Predicate selector) { + StringBuilder number = new StringBuilder(); + for (int i = arg.length() - 1; i >= 0; i--) { + if (selector.test(arg.charAt(i))) { + number.insert(0, arg.substring(i, i + 1)); + } else { + break; + } + } + return number.toString(); + } + + /** + * Parses a potion effect + * + * @param configurationSection ConfigurationSection to parse the effect from + * @return The potion effect parsed + */ + @Nonnull + public static List parsePotionEffects(@Nullable final ConfigurationSection configurationSection) { + List potionEffects = Lists.newArrayList(); + if (configurationSection != null) { + for (String name : configurationSection.getKeys(false)) { + ConfigurationSection configEffect = configurationSection.getConfigurationSection(name); + String type = configEffect.getString("type"); + if (type == null) { + LOGGER.severe("Expected potion type to be specified, but found no \"type\" option at " + + configEffect.getCurrentPath()); + continue; + } + PotionEffectType effect = PotionEffectType.getByName(type); + if (effect == null) { + LOGGER.severe("Expected potion type to be specified at " + configEffect.getCurrentPath() + + " but found " + type + " which is no valid type"); + } + int duration = configEffect.getInt("duration", 200); + int amplifier = configEffect.getInt("amplifier", 0); + potionEffects.add(new PotionEffect(effect, duration, amplifier)); + } + } + return potionEffects; + } + + @Nullable + public static IArea parseArea(@Nullable final ConfigurationSection config) { + if (config == null) { + LOGGER.warning("Tried to parse area on null section"); + return null; + } + String type = config.getString("type"); + if (type == null) { + LOGGER.warning("Found no area type at " + config.getCurrentPath()); + return null; + } + int lowerYBound = config.getInt("lowerYBound", 0); + int upperYBound = config.getInt("upperYBound", 255); + String worldName = config.getString("world"); + if (worldName == null) { + LOGGER.warning("Found no world specified for area at " + config.getCurrentPath()); + return null; + } + World world = Bukkit.getWorld(worldName); + if (world == null) { + LOGGER.warning("Found no world with name " + worldName + " as specified at " + config.getCurrentPath()); + return null; + } + Location center = null; + if (config.isConfigurationSection("center")) { + ConfigurationSection centerSection = config.getConfigurationSection("center"); + int x = centerSection.getInt("x", 0); + int y = centerSection.getInt("y", 0); + int z = centerSection.getInt("z", 0); + if (world != null) { + center = new Location(world, x, y, z); + } + } + int xSize = config.getInt("xSize", -1); + int zSize = config.getInt("zSize", -1); + IArea area = null; + switch (type) { + case "GLOBAL": + area = new GlobalYLimitedArea(lowerYBound, upperYBound, world); + break; + case "ELLIPSE": + if (center == null) { + LOGGER.warning("Found no center for area at " + config.getCurrentPath()); + return null; + } + if (xSize == -1) { + LOGGER.warning("Found no xSize for area at " + config.getCurrentPath()); + return null; + } + if (zSize == -1) { + LOGGER.warning("Found no zSize for area at " + config.getCurrentPath()); + return null; + } + area = new EllipseArea(lowerYBound, upperYBound, center, xSize, zSize); + break; + case "RECTANGLE": + if (center == null) { + LOGGER.warning("Found no center for area at " + config.getCurrentPath()); + return null; + } + if (xSize == -1) { + LOGGER.warning("Found no xSize for area at " + config.getCurrentPath()); + return null; + } + if (zSize == -1) { + LOGGER.warning("Found no zSize for area at " + config.getCurrentPath()); + return null; + } + area = new RectangleArea(lowerYBound, upperYBound, center, xSize, zSize); + break; + default: + LOGGER.warning("Invalid area type " + type + " at " + config.getCurrentPath()); + } + return area; + } + + /** + * Parses a section which contains key-value mappings of a type to another type + * + * @param Key type + * @param Value type + * @param parent Configuration section containing the section with the values + * @param identifier Config identifier of the section containing the entries + * @param logger The logger to write in progress work to + * @param keyConverter Converts strings to type K + * @param valueConverter Converts strings to type V + * @param mapToUse The map to place parsed keys and values. + */ + public static void parseKeyValueMap(@Nonnull final ConfigurationSection parent, + @Nonnull final String identifier, + @Nonnull final Logger logger, + @Nonnull final Function keyConverter, + @Nonnull final Function valueConverter, + @Nonnull final Map mapToUse) { + if (!parent.isConfigurationSection(identifier)) { + return; + } + ConfigurationSection section = parent.getConfigurationSection(identifier); + for (String keyString : section.getKeys(false)) { + if (section.isConfigurationSection(keyString)) { + logger.warning( + "Ignoring invalid " + identifier + " entry " + keyString + " at " + section.getCurrentPath()); + continue; + } + K keyinstance; + try { + keyinstance = keyConverter.apply(keyString); + } catch (IllegalArgumentException e) { + logger.warning("Failed to parse " + identifier + " " + keyString + " at " + section.getCurrentPath() + + ": " + e.toString()); + continue; + } + V value = valueConverter.apply(section.getString(keyString)); + mapToUse.put(keyinstance, value); + } + } + + /** + * @deprecated Use {@link EnchantUtils#getEnchantment(String)} instead. + */ + @Nullable + @Deprecated(forRemoval = true) + public static Enchantment parseEnchantment(@Nonnull final ConfigurationSection config, + @Nonnull final String key) { + return EnchantUtils.getEnchantment(config.getString(key)); + } + } diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/config/ConfigParser.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/config/ConfigParser.java new file mode 100644 index 000000000..901f7f07a --- /dev/null +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/config/ConfigParser.java @@ -0,0 +1,82 @@ +package vg.civcraft.mc.civmodcore.config; + +import java.util.logging.Logger; +import org.bukkit.ChatColor; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.plugin.Plugin; + +/** + * This is a config parsing class intended to make handling configs a little easier, and automatically parse commonly + * seen things within civ configs. + */ +public abstract class ConfigParser { + + protected final Plugin plugin; + protected final Logger logger; + + private boolean debug; + private boolean logReplies; + + public ConfigParser(final Plugin plugin) { + this.plugin = plugin; + this.logger = plugin.getLogger(); + } + + /** + * Parses this civ plugin's config. It will also save a default config (if one is not already present) and reload + * the config, so there's no need to do so yourself beforehand. + * + * @return Returns true if the config was successfully parsed. + */ + public final boolean parse() { + this.logger.info(ChatColor.BLUE + "Parsing config."); + this.plugin.saveDefaultConfig(); + FileConfiguration config = this.plugin.getConfig(); + // Parse debug value + this.debug = config.getBoolean("debug", false); + this.logger.info("Debug mode: " + (this.debug ? "enabled" : "disabled")); + // Parse reply logging value + this.logReplies = config.getBoolean("logReplies", false); + this.logger.info("Logging replies: " + (this.logReplies ? "enabled" : "disabled")); + // Allow child class parsing + final boolean worked = parseInternal(config); + if (worked) { + this.logger.info(ChatColor.BLUE + "Config parsed."); + } + else { + this.logger.warning("Failed to parse config!"); + } + return worked; + } + + /** + * An internal parser method intended to be overridden by child classes. + * + * @param config The root config section. + * @return Return true if the + */ + protected abstract boolean parseInternal(final ConfigurationSection config); + + /** + * This should reset all config values back to their defaults. Child classes should override this if they parse + * additional values that should be reset. + */ + public void reset() { + this.debug = false; + this.logReplies = false; + } + + // ------------------------------------------------------------ // + // Getters + // ------------------------------------------------------------ // + + public final boolean isDebugEnabled() { + return this.debug; + } + + public final boolean logReplies() { + return this.logReplies; + } + +} diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/config/ConfigSection.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/config/ConfigSection.java new file mode 100644 index 000000000..6e0053c34 --- /dev/null +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/config/ConfigSection.java @@ -0,0 +1,24 @@ +package vg.civcraft.mc.civmodcore.config; + +import java.util.Map; +import javax.annotation.Nonnull; +import org.bukkit.configuration.MemoryConfiguration; +import org.bukkit.configuration.serialization.ConfigurationSerializable; + +/** + * Class is intended to be used by implementations of {@link ConfigurationSerializable} during the deserialization + * process. + */ +public class ConfigSection extends MemoryConfiguration { + + /** + * @param data The data to create a new ConfigSection from. + * @return Returns a new ConfigSection. + */ + public static ConfigSection fromData(@Nonnull final Map data) { + final var section = new ConfigSection(); + section.map.putAll(data); + return section; + } + +} diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/utilities/ConfigParsing.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/utilities/ConfigParsing.java deleted file mode 100644 index fa4337e41..000000000 --- a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/utilities/ConfigParsing.java +++ /dev/null @@ -1,472 +0,0 @@ -package vg.civcraft.mc.civmodcore.utilities; - -import com.google.common.collect.Lists; -import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeUnit; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.logging.Logger; -import java.util.regex.Pattern; -import org.bukkit.Bukkit; -import org.bukkit.Color; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.NamespacedKey; -import org.bukkit.World; -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.enchantments.Enchantment; -import org.bukkit.inventory.ItemFlag; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.EnchantmentStorageMeta; -import org.bukkit.inventory.meta.ItemMeta; -import org.bukkit.inventory.meta.LeatherArmorMeta; -import org.bukkit.inventory.meta.PotionMeta; -import org.bukkit.potion.PotionData; -import org.bukkit.potion.PotionEffect; -import org.bukkit.potion.PotionEffectType; -import org.bukkit.potion.PotionType; -import vg.civcraft.mc.civmodcore.areas.EllipseArea; -import vg.civcraft.mc.civmodcore.areas.GlobalYLimitedArea; -import vg.civcraft.mc.civmodcore.areas.IArea; -import vg.civcraft.mc.civmodcore.areas.RectangleArea; -import vg.civcraft.mc.civmodcore.itemHandling.ItemMap; - -public class ConfigParsing { - - private static final Logger log = Bukkit.getLogger(); - - /** - * Creates an itemmap containing all the items listed in the given config - * section - * - * @param config ConfigurationSection to parse the items from - * @return The item map created - */ - public static ItemMap parseItemMap(ConfigurationSection config) { - ItemMap result = new ItemMap(); - if (config == null) { - return result; - } - for (String key : config.getKeys(false)) { - ConfigurationSection current = config.getConfigurationSection(key); - ItemMap partMap = parseItemMapDirectly(current); - result.merge(partMap); - } - return result; - } - - public static ItemMap parseItemMapDirectly(ConfigurationSection current) { - ItemMap im = new ItemMap(); - if (current == null) { - return im; - } - Material m = null; - try { - m = Material.valueOf(current.getString("material")); - } catch (IllegalArgumentException iae) { - m = null; - } finally { - if (m == null) { - log.severe("Failed to find material " + current.getString("material") + " in section " + current.getCurrentPath()); - return im; - } - } - ItemStack toAdd = new ItemStack(m); - if (current.isInt("durability")) { - log.warning("Item durability as specified at " + current.getCurrentPath() + " is no longer supported"); - } - ItemMeta meta = toAdd.getItemMeta(); - if (meta == null) { - log.severe("No item meta found for" + current.getCurrentPath()); - } else { - String name = current.getString("name"); - if (name != null) { - meta.setDisplayName(name); - } - List lore = current.getStringList("lore"); - if (lore != null) { - meta.setLore(lore); - } - if (current.isBoolean("unbreakable")) { - meta.setUnbreakable(current.getBoolean("unbreakable")); - } - if (current.isBoolean("hideFlags") && current.getBoolean("hideFlags")) { - for (ItemFlag flag : ItemFlag.values()) { - meta.addItemFlags(flag); - } - } - if (current.contains("enchants")) { - for (String enchantKey : current.getConfigurationSection("enchants").getKeys(false)) { - ConfigurationSection enchantConfig = current.getConfigurationSection("enchants") - .getConfigurationSection(enchantKey); - if (!enchantConfig.isString("enchant")) { - log.warning("No enchant specified for enchantment entry at " + enchantConfig.getCurrentPath() - + ". Entry was ignored"); - continue; - } - Enchantment enchant; - enchant = Enchantment - .getByKey(NamespacedKey.minecraft((enchantConfig.getString("enchant").toLowerCase()))); - if (enchant == null) { - log.severe("Failed to parse enchantment " + enchantConfig.getString("enchant") - + ", the entry was ignored"); - continue; - } - int level = enchantConfig.getInt("level", 1); - meta.addEnchant(enchant, level, true); - } - } - if (m == Material.LEATHER_BOOTS || m == Material.LEATHER_CHESTPLATE || m == Material.LEATHER_HELMET - || m == Material.LEATHER_LEGGINGS) { - ConfigurationSection color = current.getConfigurationSection("color"); - Color leatherColor = null; - if (color != null) { - int red = color.getInt("red"); - int blue = color.getInt("blue"); - int green = color.getInt("green"); - leatherColor = Color.fromRGB(red, green, blue); - } else { - String hexColorCode = current.getString("color"); - if (hexColorCode != null) { - Integer hexColor = Integer.parseInt(hexColorCode, 16); - if (hexColor != null) { - leatherColor = Color.fromRGB(hexColor); - } - } - } - if (leatherColor != null) { - ((LeatherArmorMeta) meta).setColor(leatherColor); - } - } - if (m == Material.ENCHANTED_BOOK) { - ConfigurationSection storedEnchantSection = current.getConfigurationSection("stored_enchants"); - if (storedEnchantSection != null) { - EnchantmentStorageMeta enchantMeta = (EnchantmentStorageMeta) meta; - for (String sEKey : storedEnchantSection.getKeys(false)) { - ConfigurationSection currentStoredEnchantSection = storedEnchantSection - .getConfigurationSection(sEKey); - if (currentStoredEnchantSection != null) { - Enchantment enchant = parseEnchantment(currentStoredEnchantSection, "enchant"); - int level = currentStoredEnchantSection.getInt("level", 1); - if (enchant != null) { - enchantMeta.addStoredEnchant(enchant, level, true); - } - else { - log.severe("Failed to parse enchantment at " + currentStoredEnchantSection.getCurrentPath() - + ", it was not applied"); - } - } - } - } - } - if (m == Material.POTION || m == Material.SPLASH_POTION || m == Material.LINGERING_POTION - || m == Material.TIPPED_ARROW) { - ConfigurationSection potion = current.getConfigurationSection("potion_effects"); - if (potion != null) { - PotionType potType; - try { - potType = PotionType.valueOf(potion.getString("type", "AWKWARD")); - } catch (IllegalArgumentException e) { - log.warning("Expected potion type at " + potion.getCurrentPath() + ", but " - + potion.getString("type") + " is not a valid potion type"); - potType = PotionType.AWKWARD; - } - boolean upgraded = potion.getBoolean("upgraded", false); - boolean extended = potion.getBoolean("extended", false); - PotionMeta potMeta = (PotionMeta) meta; - potMeta.setBasePotionData(new PotionData(potType, extended, upgraded)); - ConfigurationSection customEffects = potion.getConfigurationSection("custom_effects"); - if (customEffects != null) { - List pots = parsePotionEffects(potion); - for (PotionEffect pe : pots) { - potMeta.addCustomEffect(pe, true); - } - } - } - - } - toAdd.setItemMeta(meta); - if (current.contains("nbt")) { - toAdd = ItemMap.enrichWithNBT(toAdd, 1, current.getConfigurationSection("nbt").getValues(true)); - } - } - // Setting amount must be last just in cast enrichWithNBT is called, - // which - // resets the amount to 1. - int amount = current.getInt("amount", 1); - toAdd.setAmount(amount); - im.addItemStack(toAdd); - return im; - } - - public static int parseTimeAsTicks(String arg) { - return (int) (parseTime(arg, TimeUnit.MILLISECONDS) / 50L); - } - - public static long parseTime(String arg, TimeUnit unit) { - long millis = parseTime(arg); - return unit.convert(millis, TimeUnit.MILLISECONDS); - } - - /** - * Parses a time value specified in a config. This allows to specify human - * readable time values easily, instead of having to specify every amount in - * ticks or seconds. The unit of a number specifed by the letter added after it, - * for example 5h means 5 hours or 34s means 34 seconds. Possible modifiers are: - * t (ticks), s (seconds), m (minutes), h (hours) and d (days) - *

- * Additionally you can combine those amounts in any way you want, for example - * you can specify 3h5m43s as 3 hours, 5 minutes and 43 seconds. This doesn't - * have to be sorted and may even list the same unit multiple times for - * different values, but the values are not allowed to be separated by anything - * - * @param input Parsed string containing the time format - * @return How many milliseconds the given time value is - */ - public static long parseTime(String input) { - input = input.replace(" ", "").replace(",", "").toLowerCase(); - long result = 0; - try { - result += Long.parseLong(input); - return result; - } catch (NumberFormatException e) { - } - while (!input.equals("")) { - String typeSuffix = getSuffix(input, Character::isLetter); - input = input.substring(0, input.length() - typeSuffix.length()); - String numberSuffix = getSuffix(input, Character::isDigit); - input = input.substring(0, input.length() - numberSuffix.length()); - long duration; - if (numberSuffix.length() == 0) { - duration = 1; - } else { - duration = Long.parseLong(numberSuffix); - } - switch (typeSuffix) { - case "ms": - case "milli": - case "millis": - result += duration; - break; - case "s": // seconds - case "sec": - case "second": - case "seconds": - result += TimeUnit.SECONDS.toMillis(duration); - break; - case "m": // minutes - case "min": - case "minute": - case "minutes": - result += TimeUnit.MINUTES.toMillis(duration); - break; - case "h": // hours - case "hour": - case "hours": - result += TimeUnit.HOURS.toMillis(duration); - break; - case "d": // days - case "day": - case "days": - result += TimeUnit.DAYS.toMillis(duration); - break; - case "w": // weeks - case "week": - case "weeks": - result += TimeUnit.DAYS.toMillis(duration * 7); - break; - case "month": // weeks - case "months": - result += TimeUnit.DAYS.toMillis(duration * 30); - break; - case "y": - case "year": - case "years": - result += TimeUnit.DAYS.toMillis(duration * 365); - break; - case "never": - case "inf": - case "infinite": - case "perm": - case "perma": - case "forever": - // 1000 years counts as perma - result += TimeUnit.DAYS.toMillis(365 * 1000); - default: - // just ignore it - } - } - return result; - } - - private static String getSuffix(String arg, Predicate selector) { - StringBuilder number = new StringBuilder(); - for (int i = arg.length() - 1; i >= 0; i--) { - if (selector.test(arg.charAt(i))) { - number.insert(0, arg.substring(i, i + 1)); - } else { - break; - } - } - return number.toString(); - } - - /** - * Parses a potion effect - * - * @param configurationSection ConfigurationSection to parse the effect from - * @return The potion effect parsed - */ - public static List parsePotionEffects(ConfigurationSection configurationSection) { - List potionEffects = Lists.newArrayList(); - if (configurationSection != null) { - for (String name : configurationSection.getKeys(false)) { - ConfigurationSection configEffect = configurationSection.getConfigurationSection(name); - String type = configEffect.getString("type"); - if (type == null) { - log.severe("Expected potion type to be specified, but found no \"type\" option at " - + configEffect.getCurrentPath()); - continue; - } - PotionEffectType effect = PotionEffectType.getByName(type); - if (effect == null) { - log.severe("Expected potion type to be specified at " + configEffect.getCurrentPath() - + " but found " + type + " which is no valid type"); - } - int duration = configEffect.getInt("duration", 200); - int amplifier = configEffect.getInt("amplifier", 0); - potionEffects.add(new PotionEffect(effect, duration, amplifier)); - } - } - return potionEffects; - } - - public static IArea parseArea(ConfigurationSection config) { - if (config == null) { - log.warning("Tried to parse area on null section"); - return null; - } - String type = config.getString("type"); - if (type == null) { - log.warning("Found no area type at " + config.getCurrentPath()); - return null; - } - int lowerYBound = config.getInt("lowerYBound", 0); - int upperYBound = config.getInt("upperYBound", 255); - String worldName = config.getString("world"); - if (worldName == null) { - log.warning("Found no world specified for area at " + config.getCurrentPath()); - return null; - } - World world = Bukkit.getWorld(worldName); - if (world == null) { - log.warning("Found no world with name " + worldName + " as specified at " + config.getCurrentPath()); - return null; - } - Location center = null; - if (config.isConfigurationSection("center")) { - ConfigurationSection centerSection = config.getConfigurationSection("center"); - int x = centerSection.getInt("x", 0); - int y = centerSection.getInt("y", 0); - int z = centerSection.getInt("z", 0); - if (world != null) { - center = new Location(world, x, y, z); - } - } - int xSize = config.getInt("xSize", -1); - int zSize = config.getInt("zSize", -1); - IArea area = null; - switch (type) { - case "GLOBAL": - area = new GlobalYLimitedArea(lowerYBound, upperYBound, world); - break; - case "ELLIPSE": - if (center == null) { - log.warning("Found no center for area at " + config.getCurrentPath()); - return null; - } - if (xSize == -1) { - log.warning("Found no xSize for area at " + config.getCurrentPath()); - return null; - } - if (zSize == -1) { - log.warning("Found no zSize for area at " + config.getCurrentPath()); - return null; - } - area = new EllipseArea(lowerYBound, upperYBound, center, xSize, zSize); - break; - case "RECTANGLE": - if (center == null) { - log.warning("Found no center for area at " + config.getCurrentPath()); - return null; - } - if (xSize == -1) { - log.warning("Found no xSize for area at " + config.getCurrentPath()); - return null; - } - if (zSize == -1) { - log.warning("Found no zSize for area at " + config.getCurrentPath()); - return null; - } - area = new RectangleArea(lowerYBound, upperYBound, center, xSize, zSize); - break; - default: - log.warning("Invalid area type " + type + " at " + config.getCurrentPath()); - } - return area; - } - - /** - * Parses a section which contains key-value mappings of a type to another type - * - * @param Key type - * @param Value type - * @param parent Configuration section containing the section with the values - * @param identifier Config identifier of the section containing the entries - * @param logger The logger to write in progress work to - * @param keyConverter Converts strings to type K - * @param valueConverter Converts strings to type V - * @param mapToUse The map to place parsed keys and values. - */ - public static void parseKeyValueMap(ConfigurationSection parent, String identifier, Logger logger, - Function keyConverter, Function valueConverter, Map mapToUse) { - if (!parent.isConfigurationSection(identifier)) { - return; - } - ConfigurationSection section = parent.getConfigurationSection(identifier); - for (String keyString : section.getKeys(false)) { - if (section.isConfigurationSection(keyString)) { - logger.warning( - "Ignoring invalid " + identifier + " entry " + keyString + " at " + section.getCurrentPath()); - continue; - } - K keyinstance; - try { - keyinstance = keyConverter.apply(keyString); - } catch (IllegalArgumentException e) { - logger.warning("Failed to parse " + identifier + " " + keyString + " at " + section.getCurrentPath() - + ": " + e.toString()); - continue; - } - V value = valueConverter.apply(section.getString(keyString)); - mapToUse.put(keyinstance, value); - } - } - - public static Enchantment parseEnchantment(ConfigurationSection config, String key) { - if (!config.isString(key)) { - return null; - } - String val = config.getString(key); - if (!Pattern.matches("[a-z0-9/._-]+", val)) { - return null; - } - NamespacedKey nsKey = NamespacedKey.minecraft(val); - if (nsKey == null) { - return null; - } - return Enchantment.getByKey(nsKey); - } - -} diff --git a/plugins/civmodcore-paper/src/main/resources/config.yml b/plugins/civmodcore-paper/src/main/resources/config.yml index 5191b05f4..e62e9b938 100644 --- a/plugins/civmodcore-paper/src/main/resources/config.yml +++ b/plugins/civmodcore-paper/src/main/resources/config.yml @@ -1,15 +1,13 @@ # Database connection details, should you want to have a database connection. -# KEEP THIS COMMENTED OUT IN THIS DEFAULT CONFIG OTHERWISE BAD THINGS! # Do NOT change the ==: value. -#database: -# ==: vg.civcraft.mc.civmodcore.dao.ManagedDatasource -# plugin: CivModCore -# user: username -# password: password -# host: localhost -# port: 3306 -# database: minecraft -# poolsize: 5 -# connection_timeout: 10000 -# idle_timeout: 600000 -# max_lifetime: 7200000 +database: + ==: vg.civcraft.mc.civmodcore.dao.DatabaseCredentials + username: username + password: password + host: localhost + port: 3306 + database: minecraft + poolsize: 5 + connection_timeout: 10000 + idle_timeout: 600000 + max_lifetime: 7200000 diff --git a/plugins/civmodcore-paper/src/test/java/vg/civcraft/mc/civmodcore/config/ConfigTests.java b/plugins/civmodcore-paper/src/test/java/vg/civcraft/mc/civmodcore/config/ConfigTests.java new file mode 100644 index 000000000..b8b49f047 --- /dev/null +++ b/plugins/civmodcore-paper/src/test/java/vg/civcraft/mc/civmodcore/config/ConfigTests.java @@ -0,0 +1,26 @@ +package vg.civcraft.mc.civmodcore.config; + +import java.util.HashMap; +import org.bukkit.Material; +import org.junit.Assert; +import org.junit.Test; + +public class ConfigTests { + + @Test + public void pseudoConfigTest() { + // Setup + final var data = new HashMap() {{ + put("key", "value"); + put("material", Material.NAME_TAG.name()); + put("amount", 100); + }}; + // Process + final var config = ConfigSection.fromData(data); + // Check + Assert.assertEquals("value", config.getString("key")); + Assert.assertEquals(Material.NAME_TAG.name(), config.getString("material")); + Assert.assertEquals(100, config.getInt("amount")); + } + +}