diff --git a/src/main/java/net/knarcraft/blacksmith/BlacksmithPlugin.java b/src/main/java/net/knarcraft/blacksmith/BlacksmithPlugin.java index 15fb73c..29dea58 100644 --- a/src/main/java/net/knarcraft/blacksmith/BlacksmithPlugin.java +++ b/src/main/java/net/knarcraft/blacksmith/BlacksmithPlugin.java @@ -13,6 +13,7 @@ import net.knarcraft.blacksmith.command.scrapper.ScrapperConfigCommand; import net.knarcraft.blacksmith.command.scrapper.ScrapperConfigTabCompleter; import net.knarcraft.blacksmith.command.scrapper.ScrapperEditCommand; import net.knarcraft.blacksmith.command.scrapper.ScrapperEditTabCompleter; +import net.knarcraft.blacksmith.config.StargateYamlConfiguration; import net.knarcraft.blacksmith.config.blacksmith.GlobalBlacksmithSettings; import net.knarcraft.blacksmith.config.scrapper.GlobalScrapperSettings; import net.knarcraft.blacksmith.formatting.BlacksmithTranslatableMessage; @@ -21,15 +22,15 @@ import net.knarcraft.blacksmith.listener.PlayerListener; import net.knarcraft.blacksmith.manager.EconomyManager; import net.knarcraft.blacksmith.trait.BlacksmithTrait; import net.knarcraft.blacksmith.trait.ScrapperTrait; +import net.knarcraft.blacksmith.util.ConfigHelper; import net.knarcraft.knarlib.formatting.StringFormatter; import net.knarcraft.knarlib.formatting.TranslatableTimeUnit; import net.knarcraft.knarlib.formatting.Translator; -import net.knarcraft.knarlib.property.ColorConversion; -import net.knarcraft.knarlib.util.FileHelper; import net.knarcraft.knarlib.util.UpdateChecker; import org.bukkit.command.CommandExecutor; import org.bukkit.command.PluginCommand; import org.bukkit.command.TabCompleter; +import org.bukkit.configuration.InvalidConfigurationException; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.plugin.PluginDescriptionFile; import org.bukkit.plugin.PluginManager; @@ -40,9 +41,6 @@ import org.jetbrains.annotations.Nullable; import java.io.File; import java.io.IOException; -import java.io.InputStream; -import java.util.Arrays; -import java.util.Map; import java.util.logging.Level; /** @@ -50,11 +48,13 @@ import java.util.logging.Level; */ public class BlacksmithPlugin extends JavaPlugin { + private static final String CONFIG_FILE_NAME = "config.yml"; private static BlacksmithPlugin instance; private GlobalBlacksmithSettings blacksmithConfig; private GlobalScrapperSettings scrapperConfig; private static Translator translator; private static StringFormatter stringFormatter; + private FileConfiguration configuration; /** * Constructor required for MockBukkit @@ -107,8 +107,18 @@ public class BlacksmithPlugin extends JavaPlugin { this.reloadConfig(); blacksmithConfig.load(); scrapperConfig.load(); - translator.loadLanguages(this.getDataFolder(), "en", - this.getConfig().getString("language", "en")); + translator.loadLanguages(this.getDataFolder(), "en", this.getConfiguration().getString( + "language", "en")); + } + + /** + * Gets the raw configuration + * + * @return

The raw configuration

+ */ + @NotNull + public FileConfiguration getConfiguration() { + return this.configuration; } /** @@ -139,18 +149,23 @@ public class BlacksmithPlugin extends JavaPlugin { instance = this; //Copy default config to disk - FileConfiguration fileConfiguration = this.getConfig(); this.saveDefaultConfig(); - if (fileConfiguration.getString("defaults.dropItem") != null) { - migrateConfig(fileConfiguration); - this.saveConfig(); + this.getConfig(); + this.configuration = new StargateYamlConfiguration(); + try { + this.configuration.load(new File(getDataFolder(), CONFIG_FILE_NAME)); + } catch (IOException | InvalidConfigurationException exception) { + getLogger().log(Level.SEVERE, exception.getMessage()); } - fileConfiguration.options().copyDefaults(true); - this.reloadConfig(); - this.saveConfig(); + this.configuration.options().copyDefaults(true); // Initialize custom configuration files - initializeConfigurations(fileConfiguration); + this.reloadConfig(); + if (this.configuration.getString("scrapper.defaults.dropItem") == null) { + ConfigHelper.migrateConfig(this.getDataFolder().getPath().replaceAll("\\\\", "/"), this.configuration); + this.reloadConfig(); + } + initializeConfigurations(this.configuration); //Set up Vault integration if (!setUpVault()) { @@ -174,6 +189,28 @@ public class BlacksmithPlugin extends JavaPlugin { () -> this.getDescription().getVersion(), null); } + @Override + public void reloadConfig() { + super.reloadConfig(); + this.configuration = new StargateYamlConfiguration(); + this.configuration.options().copyDefaults(true); + try { + this.configuration.load(new File(getDataFolder(), CONFIG_FILE_NAME)); + } catch (IOException | InvalidConfigurationException exception) { + getLogger().log(Level.SEVERE, "Unable to load the configuration! Message: " + exception.getMessage()); + } + } + + @Override + public void saveConfig() { + super.saveConfig(); + try { + this.configuration.save(new File(getDataFolder(), CONFIG_FILE_NAME)); + } catch (IOException exception) { + getLogger().log(Level.SEVERE, "Unable to save the configuration! Message: " + exception.getMessage()); + } + } + /** * Initializes custom configuration and translation * @@ -256,48 +293,4 @@ public class BlacksmithPlugin extends JavaPlugin { } } - /** - * Changes all configuration values from the old name to the new name - * - * @param fileConfiguration

The config to read from and write to

- */ - private void migrateConfig(@NotNull FileConfiguration fileConfiguration) { - //Save the old config just in case something goes wrong - try { - fileConfiguration.save(getDataFolder() + "/config.yml.old"); - } catch (IOException exception) { - BlacksmithPlugin.getInstance().getLogger().log(Level.SEVERE, Arrays.toString(exception.getStackTrace())); - return; - } - - //Read all available config migrations - Map migrationFields = null; - try { - InputStream inputStream = FileHelper.getInputStreamForInternalFile("/config-migrations.txt"); - if (inputStream != null) { - migrationFields = FileHelper.readKeyValuePairs(FileHelper.getBufferedReaderFromInputStream(inputStream), - "=", ColorConversion.NORMAL); - } - } catch (IOException exception) { - BlacksmithPlugin.getInstance().getLogger().log(Level.SEVERE, Arrays.toString(exception.getStackTrace())); - return; - } - - if (migrationFields == null) { - return; - } - - //Replace old config names with the new ones - for (String key : migrationFields.keySet()) { - if (fileConfiguration.contains(key)) { - String newPath = migrationFields.get(key); - Object oldValue = fileConfiguration.get(key); - if (!newPath.trim().isEmpty()) { - fileConfiguration.set(newPath, oldValue); - } - fileConfiguration.set(key, null); - } - } - } - } diff --git a/src/main/java/net/knarcraft/blacksmith/config/StargateYamlConfiguration.java b/src/main/java/net/knarcraft/blacksmith/config/StargateYamlConfiguration.java new file mode 100644 index 0000000..05aa5b0 --- /dev/null +++ b/src/main/java/net/knarcraft/blacksmith/config/StargateYamlConfiguration.java @@ -0,0 +1,246 @@ +package net.knarcraft.blacksmith.config; + +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * A YAML configuration which retains all comments + * + *

This configuration converts all comments to YAML values when loaded, which all start with comment_. When saved, + * those YAML values are converted to normal text comments. This ensures that the comments aren't removed by the + * YamlConfiguration during its parsing.

+ * + *

Note: When retrieving a configuration section, that will include comments that start with "comment_". You should + * filter those before parsing input.

+ * + * @author Kristian Knarvik + * @author Thorin + */ +public class StargateYamlConfiguration extends YamlConfiguration { + + private static final String START_OF_COMMENT_LINE = "[HASHTAG]"; + private static final String END_OF_COMMENT = "_endOfComment_"; + private static final String START_OF_COMMENT = "comment_"; + + @Override + @NotNull + @SuppressWarnings("deprecation") + protected String buildHeader() { + return ""; + } + + @Override + @NotNull + public String saveToString() { + // Convert YAML comments to normal comments + return this.convertYAMLMappingsToComments(super.saveToString()); + } + + @Override + public void loadFromString(@NotNull String contents) throws InvalidConfigurationException { + // Convert normal comments to YAML comments to prevent them from disappearing + super.loadFromString(this.convertCommentsToYAMLMappings(contents)); + } + + /** + * Gets a configuration section's keys, without any comment entries + * + * @param configurationSection

The configuration section to get keys for

+ * @param deep

Whether to get keys for child elements as well

+ * @return

The configuration section's keys, with comment entries removed

+ */ + public static Set getKeysWithoutComments(@NotNull ConfigurationSection configurationSection, boolean deep) { + Set keys = new HashSet<>(configurationSection.getKeys(deep)); + keys.removeIf(key -> key.matches(START_OF_COMMENT + "[0-9]+")); + return keys; + } + + /** + * Reads a file with comments, and recreates them into yaml mappings + * + *

A mapping follows this format: comment_{CommentNumber}: "The comment" + * This needs to be done as comments otherwise get removed using + * the {@link FileConfiguration#save(File)} method. The config + * needs to be saved if a config value has changed.

+ */ + @NotNull + private String convertCommentsToYAMLMappings(@NotNull String configString) { + StringBuilder yamlBuilder = new StringBuilder(); + List currentComment = new ArrayList<>(); + int commentId = 0; + int previousIndentation = 0; + + for (String line : configString.split("\n")) { + String trimmed = line.trim(); + if (trimmed.startsWith("#")) { + // Store the indentation of the block + if (currentComment.isEmpty()) { + previousIndentation = getIndentation(line); + } + //Temporarily store the comment line + addComment(currentComment, trimmed); + } else { + addYamlString(yamlBuilder, currentComment, line, previousIndentation, commentId); + commentId++; + previousIndentation = 0; + } + } + return yamlBuilder.toString(); + } + + /** + * Adds a YAML string to the given string builder + * + * @param yamlBuilder

The string builder used for building YAML

+ * @param currentComment

The comment to add as a YAML string

+ * @param line

The current line

+ * @param previousIndentation

The indentation of the current block comment

+ * @param commentId

The id of the comment

+ */ + private void addYamlString(@NotNull StringBuilder yamlBuilder, @NotNull List currentComment, + @NotNull String line, int previousIndentation, int commentId) { + String trimmed = line.trim(); + //Write the full formatted comment to the StringBuilder + if (!currentComment.isEmpty()) { + int indentation = trimmed.isEmpty() ? previousIndentation : getIndentation(line); + generateCommentYAML(yamlBuilder, currentComment, commentId, indentation); + currentComment.clear(); + } + //Add the non-comment line assuming it isn't empty + if (!trimmed.isEmpty()) { + yamlBuilder.append(line).append("\n"); + } + } + + /** + * Adds the given comment to the given list + * + * @param commentParts

The list to add to

+ * @param comment

The comment to add

+ */ + private void addComment(@NotNull List commentParts, @NotNull String comment) { + if (comment.startsWith("# ")) { + commentParts.add(comment.replaceFirst("# ", START_OF_COMMENT_LINE)); + } else { + commentParts.add(comment.replaceFirst("#", START_OF_COMMENT_LINE)); + } + } + + /** + * Generates a YAML-compatible string for one comment block + * + * @param yamlBuilder

The string builder to add the generated YAML to

+ * @param commentLines

The lines of the comment to convert into YAML

+ * @param commentId

The unique id of the comment

+ * @param indentation

The indentation to add to every line

+ */ + private void generateCommentYAML(@NotNull StringBuilder yamlBuilder, @NotNull List commentLines, + int commentId, int indentation) { + String subIndentation = this.addIndentation(indentation + 2); + //Add the comment start marker + yamlBuilder.append(this.addIndentation(indentation)).append(START_OF_COMMENT).append(commentId).append(": |\n"); + for (String commentLine : commentLines) { + //Add each comment line with the proper indentation + yamlBuilder.append(subIndentation).append(commentLine).append("\n"); + } + //Add the comment end marker + yamlBuilder.append(subIndentation).append(subIndentation).append(END_OF_COMMENT).append("\n"); + } + + /** + * Converts the internal YAML mapping format to a readable config file + * + *

The internal YAML structure is converted to a string with the same format as a standard configuration file. + * The internal structure has comments in the format: START_OF_COMMENT + id + multi-line YAML string + + * END_OF_COMMENT.

+ * + * @param yamlString

A string using the YAML format

+ * @return

The corresponding comment string

+ */ + @NotNull + private String convertYAMLMappingsToComments(@NotNull String yamlString) { + StringBuilder finalText = new StringBuilder(); + + String[] lines = yamlString.split("\n"); + for (int currentIndex = 0; currentIndex < lines.length; currentIndex++) { + String line = lines[currentIndex]; + String possibleComment = line.trim(); + + if (possibleComment.startsWith(START_OF_COMMENT)) { + //Add an empty line before every comment block + finalText.append("\n"); + currentIndex = readComment(finalText, lines, currentIndex + 1, getIndentation(line)); + } else { + //Output the configuration key + finalText.append(line).append("\n"); + } + } + + return finalText.toString().trim(); + } + + /** + * Fully reads a comment + * + * @param builder

The string builder to write to

+ * @param lines

The lines to read from

+ * @param startIndex

The index to start reading from

+ * @param commentIndentation

The indentation of the read comment

+ * @return

The index containing the next non-comment line

+ */ + private int readComment(@NotNull StringBuilder builder, @NotNull String[] lines, int startIndex, + int commentIndentation) { + for (int currentIndex = startIndex; currentIndex < lines.length; currentIndex++) { + String line = lines[currentIndex]; + String possibleComment = line.trim(); + if (!line.contains(END_OF_COMMENT)) { + possibleComment = possibleComment.replace(START_OF_COMMENT_LINE, ""); + builder.append(addIndentation(commentIndentation)).append("# ").append(possibleComment).append("\n"); + } else { + return currentIndex; + } + } + + return startIndex; + } + + /** + * Gets a string containing the given indentation + * + * @param indentationSpaces

The number spaces to use for indentation

+ * @return

A string containing the number of spaces specified

+ */ + @NotNull + private String addIndentation(int indentationSpaces) { + return " ".repeat(Math.max(0, indentationSpaces)); + } + + + /** + * Gets the indentation (number of spaces) of the given line + * + * @param line

The line to get indentation of

+ * @return

The number of spaces in the line's indentation

+ */ + private int getIndentation(@NotNull String line) { + int spacesFound = 0; + for (char aCharacter : line.toCharArray()) { + if (aCharacter == ' ') { + spacesFound++; + } else { + break; + } + } + return spacesFound; + } + +} \ No newline at end of file diff --git a/src/main/java/net/knarcraft/blacksmith/config/blacksmith/GlobalBlacksmithSettings.java b/src/main/java/net/knarcraft/blacksmith/config/blacksmith/GlobalBlacksmithSettings.java index c3cb514..0173001 100644 --- a/src/main/java/net/knarcraft/blacksmith/config/blacksmith/GlobalBlacksmithSettings.java +++ b/src/main/java/net/knarcraft/blacksmith/config/blacksmith/GlobalBlacksmithSettings.java @@ -1,20 +1,21 @@ package net.knarcraft.blacksmith.config.blacksmith; -import net.citizensnpcs.api.util.DataKey; -import net.citizensnpcs.api.util.YamlStorage; import net.knarcraft.blacksmith.BlacksmithPlugin; import net.knarcraft.blacksmith.config.SettingValueType; import net.knarcraft.blacksmith.config.Settings; +import net.knarcraft.blacksmith.config.StargateYamlConfiguration; import net.knarcraft.blacksmith.util.ConfigHelper; import net.knarcraft.blacksmith.util.InputParsingHelper; import net.knarcraft.blacksmith.util.ItemHelper; import org.bukkit.Material; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.enchantments.Enchantment; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.io.File; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -31,28 +32,21 @@ public class GlobalBlacksmithSettings implements Settings { private final Map settings = new HashMap<>(); private final List defaultReforgeAbleMaterials = new ArrayList<>(); private final List defaultEnchantmentBlockList = new ArrayList<>(); - - private final YamlStorage defaultConfig; + private final BlacksmithPlugin instance; /** - * Instantiates a new "Settings" + * Instantiates a new "Global Blacksmith Settings" * - * @param plugin

A reference to the blacksmith plugin

+ * @param instance

The blacksmith plugin to load and save configurations for

*/ - public GlobalBlacksmithSettings(BlacksmithPlugin plugin) { - this.defaultConfig = new YamlStorage(new File(plugin.getDataFolder() + File.separator + "config.yml"), - "Blacksmith Configuration\nWarning: The values under defaults are the values set for a " + - "blacksmith upon creation. To change any values for existing NPCs, edit the citizens NPC file."); + public GlobalBlacksmithSettings(BlacksmithPlugin instance) { + this.instance = instance; } /** * Loads all configuration values from the config file */ public void load() { - // Load the config from disk - this.defaultConfig.load(); - DataKey root = this.defaultConfig.getKey(""); - // Just in case, clear existing values this.settings.clear(); this.materialBasePrices.clear(); @@ -60,10 +54,10 @@ public class GlobalBlacksmithSettings implements Settings { this.enchantmentCosts.clear(); // Load/Save settings - loadSettings(root); + loadSettings(); // Save any modified values to disk - this.defaultConfig.save(); + instance.saveConfig(); } /** @@ -314,34 +308,51 @@ public class GlobalBlacksmithSettings implements Settings { /** * Loads all global settings - * - * @param root

The root node of all global settings

*/ - private void loadSettings(DataKey root) { + private void loadSettings() { + instance.reloadConfig(); + FileConfiguration configuration = instance.getConfiguration(); + for (BlacksmithSetting blacksmithSetting : BlacksmithSetting.values()) { - if (!root.keyExists(blacksmithSetting.getPath())) { + if (!configuration.contains(blacksmithSetting.getPath())) { //If the setting does not exist in the config file, add it - root.setRaw(blacksmithSetting.getPath(), blacksmithSetting.getDefaultValue()); + configuration.set(blacksmithSetting.getPath(), blacksmithSetting.getDefaultValue()); } else { //Set the setting to the value found in the path - settings.put(blacksmithSetting, root.getRaw(blacksmithSetting.getPath())); + settings.put(blacksmithSetting, configuration.get(blacksmithSetting.getPath())); } } loadReforgeAbleItems(); loadEnchantmentBlockList(); //Load all base prices - loadBasePrices(root); + loadBasePrices(configuration); //Load all per-durability-point prices - loadPricesPerDurabilityPoint(root); + loadPricesPerDurabilityPoint(configuration); //Load all enchantment prices - DataKey enchantmentCostNode = root.getRelative(BlacksmithSetting.ENCHANTMENT_COST.getPath()); + loadEnchantmentPrices(configuration); + } + + /** + * Loads all prices for enchantments + * + * @param fileConfiguration

The configuration to read

+ */ + private void loadEnchantmentPrices(@NotNull FileConfiguration fileConfiguration) { + ConfigurationSection enchantmentCostNode = fileConfiguration.getConfigurationSection( + getBase(BlacksmithSetting.ENCHANTMENT_COST.getPath())); + if (enchantmentCostNode == null) { + instance.getLogger().log(Level.WARNING, "Could not load enchantment prices. because the " + + "configuration section doesn't exist"); + return; + } Map relevantKeys = getRelevantKeys(enchantmentCostNode); for (String key : relevantKeys.keySet()) { String enchantmentName = relevantKeys.get(key); Enchantment enchantment = InputParsingHelper.matchEnchantment(enchantmentName); + instance.getLogger().log(Level.WARNING, "loadEnchantmentPrices " + enchantmentName); setItemPrice(this.enchantmentCosts, enchantmentName, enchantment, enchantmentCostNode.getDouble(key)); } } @@ -349,10 +360,16 @@ public class GlobalBlacksmithSettings implements Settings { /** * Loads all prices per durability point for all materials * - * @param root

The configuration root node to search from

+ * @param fileConfiguration

The configuration to read

*/ - private void loadPricesPerDurabilityPoint(DataKey root) { - DataKey basePerDurabilityPriceNode = root.getRelative(BlacksmithSetting.PRICE_PER_DURABILITY_POINT.getPath()); + private void loadPricesPerDurabilityPoint(@NotNull FileConfiguration fileConfiguration) { + ConfigurationSection basePerDurabilityPriceNode = fileConfiguration.getConfigurationSection( + getBase(BlacksmithSetting.PRICE_PER_DURABILITY_POINT.getPath())); + if (basePerDurabilityPriceNode == null) { + instance.getLogger().log(Level.WARNING, "Could not load per durability prices. because the " + + "configuration section doesn't exist"); + return; + } Map relevantKeys = getRelevantKeys(basePerDurabilityPriceNode); for (String key : relevantKeys.keySet()) { @@ -371,10 +388,16 @@ public class GlobalBlacksmithSettings implements Settings { /** * Loads base prices for all materials * - * @param root

The configuration root node to search from

+ * @param fileConfiguration

The configuration to read

*/ - private void loadBasePrices(DataKey root) { - DataKey basePriceNode = root.getRelative(BlacksmithSetting.BASE_PRICE.getPath()); + private void loadBasePrices(@NotNull FileConfiguration fileConfiguration) { + ConfigurationSection basePriceNode = fileConfiguration.getConfigurationSection( + getBase(BlacksmithSetting.BASE_PRICE.getPath())); + if (basePriceNode == null) { + instance.getLogger().log(Level.WARNING, "Could not load base prices, because the configuration " + + "section doesn't exist"); + return; + } Map relevantKeys = getRelevantKeys(basePriceNode); for (String key : relevantKeys.keySet()) { @@ -397,7 +420,7 @@ public class GlobalBlacksmithSettings implements Settings { * @param materialName

The material name to match

* @param price

The price to set for the matched materials

*/ - private void setMatchedMaterialPrices(Map prices, String materialName, double price) { + private void setMatchedMaterialPrices(@NotNull Map prices, @NotNull String materialName, double price) { String search = InputParsingHelper.regExIfy(materialName); for (Material material : ItemHelper.getAllReforgeAbleMaterials()) { if (material.name().matches(search)) { @@ -414,31 +437,31 @@ public class GlobalBlacksmithSettings implements Settings { * @param item

The material parsed from the name

* @param price

The price to set

*/ - private void setItemPrice(Map prices, String itemName, K item, double price) { + private void setItemPrice(@NotNull Map prices, @NotNull String itemName, @Nullable K item, + double price) { if (item != null) { prices.put(item, price); } else { - BlacksmithPlugin.getInstance().getLogger().log(Level.WARNING, - "Unable to find a material/enchantment matching " + itemName); + instance.getLogger().log(Level.WARNING, "Unable to find a material/enchantment matching " + itemName); } } /** * Gets a map between relevant keys and their normalized name * - * @param rootKey

The root data key containing sub-keys

+ * @param configurationSection

The configuration section to search

* @return

Any sub-keys found that aren't the default

*/ - private Map getRelevantKeys(DataKey rootKey) { + @NotNull + private Map getRelevantKeys(@NotNull ConfigurationSection configurationSection) { Map relevant = new HashMap<>(); - for (DataKey dataKey : rootKey.getSubKeys()) { - String keyName = dataKey.name(); + for (String dataKey : StargateYamlConfiguration.getKeysWithoutComments(configurationSection, false)) { //Skip the default value - if (keyName.equals("default")) { + if (dataKey.equals("default")) { continue; } - String normalizedName = keyName.toUpperCase().replace("-", "_"); - relevant.put(keyName, normalizedName); + String normalizedName = dataKey.toUpperCase().replace("-", "_"); + relevant.put(dataKey, normalizedName); } return relevant; } @@ -449,7 +472,8 @@ public class GlobalBlacksmithSettings implements Settings { * @param normalizedName

The normalized name to un-normalize

* @return

The un-normalized name

*/ - private String unNormalizeName(String normalizedName) { + @NotNull + private String unNormalizeName(@NotNull String normalizedName) { return normalizedName.toLowerCase().replace("_", "-"); } @@ -481,32 +505,66 @@ public class GlobalBlacksmithSettings implements Settings { * Saves all current settings to the config file */ private void save() { - DataKey root = this.defaultConfig.getKey(""); + FileConfiguration fileConfiguration = instance.getConfiguration(); + //Save all default settings for (BlacksmithSetting setting : BlacksmithSetting.values()) { - root.setRaw(setting.getPath(), this.settings.get(setting)); + fileConfiguration.set(setting.getPath(), this.settings.get(setting)); } //Save all base prices - DataKey basePriceNode = root.getRelative(BlacksmithSetting.BASE_PRICE.getPath()); + ConfigurationSection basePriceNode = getAndCreateSection(fileConfiguration, getBase(BlacksmithSetting.BASE_PRICE.getPath())); for (Material material : this.materialBasePrices.keySet()) { - basePriceNode.setRaw(unNormalizeName(material.name()), this.materialBasePrices.get(material)); + basePriceNode.set(unNormalizeName(material.name()), this.materialBasePrices.get(material)); } //Save all per-durability-point prices - DataKey basePerDurabilityPriceNode = root.getRelative(BlacksmithSetting.PRICE_PER_DURABILITY_POINT.getPath()); + ConfigurationSection basePerDurabilityPriceNode = getAndCreateSection(fileConfiguration, getBase(BlacksmithSetting.PRICE_PER_DURABILITY_POINT.getPath())); for (Material material : this.materialPricePerDurabilityPoints.keySet()) { - basePerDurabilityPriceNode.setRaw(unNormalizeName(material.name()), this.materialPricePerDurabilityPoints.get(material)); + basePerDurabilityPriceNode.set(unNormalizeName(material.name()), this.materialPricePerDurabilityPoints.get(material)); } //Load all enchantment prices - DataKey enchantmentCostNode = root.getRelative(BlacksmithSetting.ENCHANTMENT_COST.getPath()); + ConfigurationSection enchantmentCostNode = getAndCreateSection(fileConfiguration, getBase(BlacksmithSetting.ENCHANTMENT_COST.getPath())); for (Enchantment enchantment : this.enchantmentCosts.keySet()) { - enchantmentCostNode.setRaw(unNormalizeName(enchantment.getKey().getKey()), this.enchantmentCosts.get(enchantment)); + enchantmentCostNode.set(unNormalizeName(enchantment.getKey().getKey()), this.enchantmentCosts.get(enchantment)); } //Perform the actual save to disk - this.defaultConfig.save(); + instance.saveConfig(); + } + + /** + * Gets a configuration section, creating it if necessary + * + * @param fileConfiguration

The file configuration to read

+ * @param sectionPath

The path to the configuration section to get

+ * @return

The configuration section

+ */ + @NotNull + private ConfigurationSection getAndCreateSection(@NotNull FileConfiguration fileConfiguration, @NotNull String sectionPath) { + ConfigurationSection configurationSection = fileConfiguration.getConfigurationSection(sectionPath); + if (configurationSection == null) { + fileConfiguration.createSection(sectionPath); + configurationSection = fileConfiguration.getConfigurationSection(sectionPath); + } + if (configurationSection == null) { + throw new RuntimeException("Unable to create configuration section!"); + } + return configurationSection; + } + + /** + * Gets the base path of a configuration path pointing to a specific value + * + * @param configurationPath

The configuration path to get the base of

+ * @return

The base configuration path

+ */ + @NotNull + private String getBase(@NotNull String configurationPath) { + String[] parts = configurationPath.split("\\."); + String[] base = Arrays.copyOfRange(parts, 0, parts.length - 1); + return String.join(".", base); } } diff --git a/src/main/java/net/knarcraft/blacksmith/config/scrapper/GlobalScrapperSettings.java b/src/main/java/net/knarcraft/blacksmith/config/scrapper/GlobalScrapperSettings.java index 3b25055..80dcce9 100644 --- a/src/main/java/net/knarcraft/blacksmith/config/scrapper/GlobalScrapperSettings.java +++ b/src/main/java/net/knarcraft/blacksmith/config/scrapper/GlobalScrapperSettings.java @@ -1,17 +1,15 @@ package net.knarcraft.blacksmith.config.scrapper; -import net.citizensnpcs.api.util.DataKey; -import net.citizensnpcs.api.util.YamlStorage; import net.knarcraft.blacksmith.BlacksmithPlugin; import net.knarcraft.blacksmith.config.SettingValueType; import net.knarcraft.blacksmith.config.Settings; import net.knarcraft.blacksmith.util.ConfigHelper; import net.knarcraft.blacksmith.util.ItemHelper; import org.bukkit.Material; +import org.bukkit.configuration.file.FileConfiguration; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.io.File; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -29,35 +27,29 @@ public class GlobalScrapperSettings implements Settings { private final List defaultSalvageableMaterials = new ArrayList<>(); private final Map> ignoredSalvage = new HashMap<>(); - private final YamlStorage defaultConfig; + private final BlacksmithPlugin instance; /** * Instantiates a new "Settings" * - * @param plugin

A reference to the blacksmith plugin

+ * @param instance

A reference to the blacksmith plugin

*/ - public GlobalScrapperSettings(BlacksmithPlugin plugin) { - this.defaultConfig = new YamlStorage(new File(plugin.getDataFolder() + File.separator + "config.yml"), - "Scrapper Configuration\nWarning: The values under defaults are the values set for a " + - "scrapper upon creation. To change any values for existing NPCs, edit the citizens NPC file."); + public GlobalScrapperSettings(BlacksmithPlugin instance) { + this.instance = instance; } /** * Loads all configuration values from the config file */ public void load() { - //Load the config from disk - this.defaultConfig.load(); - DataKey root = this.defaultConfig.getKey(""); - //Just in case, clear existing values this.settings.clear(); //Load/Save global settings - loadSettings(root); + loadSettings(); //Save any modified values to disk - this.defaultConfig.save(); + instance.saveConfig(); } /** @@ -137,17 +129,18 @@ public class GlobalScrapperSettings implements Settings { /** * Loads all global settings - * - * @param root

The root node of all global settings

*/ - private void loadSettings(DataKey root) { + private void loadSettings() { + instance.reloadConfig(); + FileConfiguration configuration = instance.getConfiguration(); + for (ScrapperSetting setting : ScrapperSetting.values()) { - if (!root.keyExists(setting.getPath())) { + if (!configuration.contains(setting.getPath())) { //If the setting does not exist in the config file, add it - root.setRaw(setting.getPath(), setting.getDefaultValue()); + configuration.set(setting.getPath(), setting.getDefaultValue()); } else { //Set the setting to the value found in the path - this.settings.put(setting, root.getRaw(setting.getPath())); + this.settings.put(setting, configuration.get(setting.getPath())); } } loadSalvageAbleItems(); @@ -158,14 +151,15 @@ public class GlobalScrapperSettings implements Settings { * Saves all current settings to the config file */ private void save() { - DataKey root = this.defaultConfig.getKey(""); + FileConfiguration fileConfiguration = instance.getConfiguration(); + //Save all default settings for (ScrapperSetting setting : ScrapperSetting.values()) { - root.setRaw(setting.getPath(), this.settings.get(setting)); + fileConfiguration.set(setting.getPath(), this.settings.get(setting)); } //Perform the actual save to disk - this.defaultConfig.save(); + instance.saveConfig(); } /** diff --git a/src/main/java/net/knarcraft/blacksmith/config/scrapper/ScrapperSetting.java b/src/main/java/net/knarcraft/blacksmith/config/scrapper/ScrapperSetting.java index 1d5b035..e05cbc7 100644 --- a/src/main/java/net/knarcraft/blacksmith/config/scrapper/ScrapperSetting.java +++ b/src/main/java/net/knarcraft/blacksmith/config/scrapper/ScrapperSetting.java @@ -131,7 +131,7 @@ public enum ScrapperSetting implements Setting { * The message displayed if a player presents a different item after seeing the price to reforge an item */ ITEM_UNEXPECTEDLY_CHANGED_MESSAGE("itemChangedMessage", SettingValueType.STRING, "&cThat's not the item" + - " you wanted to reforge before!", "The message to display when presenting a different item than" + + " you wanted to salvage before!", "The message to display when presenting a different item than" + " the one just evaluated", true, true), /** diff --git a/src/main/java/net/knarcraft/blacksmith/util/ConfigHelper.java b/src/main/java/net/knarcraft/blacksmith/util/ConfigHelper.java index 8ae38e7..395d418 100644 --- a/src/main/java/net/knarcraft/blacksmith/util/ConfigHelper.java +++ b/src/main/java/net/knarcraft/blacksmith/util/ConfigHelper.java @@ -1,10 +1,23 @@ package net.knarcraft.blacksmith.util; +import net.knarcraft.blacksmith.BlacksmithPlugin; +import net.knarcraft.blacksmith.config.StargateYamlConfiguration; +import net.knarcraft.knarlib.property.ColorConversion; +import net.knarcraft.knarlib.util.FileHelper; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.MemorySection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.logging.Level; /** * A helper class for getting an object value as the correct type @@ -94,4 +107,103 @@ public final class ConfigHelper { } } + /** + * Changes all configuration values from the old name to the new name + * + * @param currentConfiguration

The current config to back up

+ */ + public static void migrateConfig(@NotNull String dataFolderPath, @NotNull FileConfiguration currentConfiguration) { + BlacksmithPlugin instance = BlacksmithPlugin.getInstance(); + + //Save the old config just in case something goes wrong + try { + currentConfiguration.save(new File(dataFolderPath, "config.yml.old")); + } catch (IOException exception) { + instance.getLogger().log(Level.WARNING, "Unable to save old backup and do migration"); + return; + } + + //Load old and new configuration + instance.reloadConfig(); + FileConfiguration oldConfiguration = instance.getConfig(); + InputStream configStream = FileHelper.getInputStreamForInternalFile("/config.yml"); + if (configStream == null) { + instance.getLogger().log(Level.SEVERE, "Could not migrate the configuration, as the internal " + + "configuration could not be read!"); + return; + } + YamlConfiguration newConfiguration = StargateYamlConfiguration.loadConfiguration( + FileHelper.getBufferedReaderFromInputStream(configStream)); + + //Read all available config migrations + Map migrationFields; + try { + InputStream migrationStream = FileHelper.getInputStreamForInternalFile("/config-migrations.txt"); + if (migrationStream == null) { + instance.getLogger().log(Level.SEVERE, "Could not migrate the configuration, as the internal migration paths could not be read!"); + return; + } + migrationFields = FileHelper.readKeyValuePairs(FileHelper.getBufferedReaderFromInputStream(migrationStream), + "=", ColorConversion.NORMAL); + } catch (IOException exception) { + instance.getLogger().log(Level.WARNING, "Unable to load config migration file"); + return; + } + + //Replace old config names with the new ones + for (String key : migrationFields.keySet()) { + if (oldConfiguration.contains(key)) { + migrateProperty(migrationFields, key, oldConfiguration); + } + } + + // Copy all keys to the new config + for (String key : StargateYamlConfiguration.getKeysWithoutComments(oldConfiguration, true)) { + if (oldConfiguration.get(key) instanceof MemorySection) { + continue; + } + newConfiguration.set(key, oldConfiguration.get(key)); + } + + try { + newConfiguration.save(new File(dataFolderPath, "config.yml")); + } catch (IOException exception) { + instance.getLogger().log(Level.WARNING, "Unable to save migrated config"); + } + + instance.reloadConfig(); + } + + /** + * Migrates one configuration property + * + * @param migrationFields

The configuration fields to be migrated

+ * @param key

The key/path of the property to migrate

+ * @param oldConfiguration

The original pre-migration configuration

+ */ + private static void migrateProperty(@NotNull Map migrationFields, @NotNull String key, + @NotNull FileConfiguration oldConfiguration) { + String newPath = migrationFields.get(key); + Object oldValue = oldConfiguration.get(key); + if (!newPath.trim().isEmpty()) { + if (oldConfiguration.isConfigurationSection(key)) { + // Copy each value of a configuration section + ConfigurationSection sourceSection = oldConfiguration.getConfigurationSection(key); + ConfigurationSection destinationSection = oldConfiguration.createSection(newPath); + if (sourceSection == null) { + return; + } + for (String path : StargateYamlConfiguration.getKeysWithoutComments(sourceSection, true)) { + destinationSection.set(path, sourceSection.get(path)); + } + } else { + // Copy the value to the new path + oldConfiguration.set(newPath, oldValue); + } + } + + // Remove the old path's value + oldConfiguration.set(key, null); + } + } diff --git a/src/main/java/net/knarcraft/blacksmith/util/ItemHelper.java b/src/main/java/net/knarcraft/blacksmith/util/ItemHelper.java index 232bd39..26d295b 100644 --- a/src/main/java/net/knarcraft/blacksmith/util/ItemHelper.java +++ b/src/main/java/net/knarcraft/blacksmith/util/ItemHelper.java @@ -244,7 +244,7 @@ public final class ItemHelper { //Parse every material, and add to reforgeAble items for (String item : itemList) { //Ignore ",," - if (InputParsingHelper.isEmpty(item)) { + if (InputParsingHelper.isEmpty(item) || item.matches("\\[]")) { continue; } diff --git a/src/main/resources/config-migrations.txt b/src/main/resources/config-migrations.txt index 8b9f634..177ca80 100644 --- a/src/main/resources/config-migrations.txt +++ b/src/main/resources/config-migrations.txt @@ -1,9 +1,38 @@ -defaults=blacksmith.defaults -global=blacksmith.global -scrapper.defaults.delaysInSeconds.minimum=scrapper.defaults.minSalvageWaitTimeSeconds -scrapper.defaults.delaysInSeconds.maximum=scrapper.defaults.maxSalvageWaitTimeSeconds -scrapper.defaults.delaysInSeconds.salvageCoolDown=scrapper.defaults.salvageCoolDownSeconds -blacksmith.defaults.delaysInSeconds.minimum=blacksmith.defaults.minReforgeWaitTimeSeconds -blacksmith.defaults.delaysInSeconds.maximum=blacksmith.defaults.maxReforgeWaitTimeSeconds -blacksmith.defaults.delaysInSeconds.reforgeCoolDown=blacksmith.defaults.reforgeCoolDownSeconds -blacksmith.defaults.enchantmentBlocklist=blacksmith.defaults.enchantmentBlockList \ No newline at end of file +global.basePrice=blacksmith.global.basePrice +global.pricePerDurabilityPoint=blacksmith.global.pricePerDurabilityPoint +global.enchantmentCost=blacksmith.global.enchantmentCost +global.useNaturalCost=blacksmith.global.useNaturalCost +global.showExactTime=blacksmith.global.showExactTime +global.chippedAnvilRepairCost=blacksmith.global.chippedAnvilReforgingCost +global.damagedAnvilRepairCost=blacksmith.global.damagedAnvilReforgingCost +global.chippedAnvilReforgingCost=blacksmith.global.chippedAnvilReforgingCost +global.damagedAnvilReforgingCost=blacksmith.global.damagedAnvilReforgingCost +global.disableMaterialLimitation= +defaults.dropItem=blacksmith.defaults.dropItem +defaults.reforgeAbleItems=blacksmith.defaults.reforgeAbleItems +defaults.disableCoolDown= +defaults.disableDelay= +defaults.failReforgeChance=blacksmith.defaults.failReforgeChance +defaults.extraEnchantmentChance=blacksmith.defaults.extraEnchantmentChance +defaults.maxEnchantments=blacksmith.defaults.maxEnchantments +defaults.delaysInSeconds.maximum=blacksmith.defaults.maxReforgeWaitTimeSeconds +defaults.delaysInSeconds.minimum=blacksmith.defaults.minReforgeWaitTimeSeconds +defaults.delaysInSeconds.reforgeCoolDown=blacksmith.defaults.reforgeCoolDownSeconds +defaults.delaysInSeconds.blacksmithTitle= +defaults.messages.busyPlayerMessage=blacksmith.defaults.messages.busyPlayerMessage +defaults.messages.busyReforgeMessage=blacksmith.defaults.messages.busyReforgeMessage +defaults.messages.coolDownUnexpiredMessage=blacksmith.defaults.messages.coolDownUnexpiredMessage +defaults.messages.costMessage=blacksmith.defaults.messages.costMessage +defaults.messages.failReforgeMessage=blacksmith.defaults.messages.failReforgeMessage +defaults.messages.insufficientFundsMessage=blacksmith.defaults.messages.insufficientFundsMessage +defaults.messages.invalidItemMessage=blacksmith.defaults.messages.invalidItemMessage +defaults.messages.itemChangedMessage=blacksmith.defaults.messages.itemChangedMessage +defaults.messages.startReforgeMessage=blacksmith.defaults.messages.startReforgeMessage +defaults.messages.successMessage=blacksmith.defaults.messages.successMessage +defaults.messages.notDamagedMessage=blacksmith.defaults.messages.notDamagedMessage +defaults.blacksmithTitle=blacksmith.defaults.blacksmithTitle +defaults.enchantmentBlocklist=blacksmith.defaults.enchantmentBlockList +defaults.repairAnvils=blacksmith.defaults.reforgeAnvils +defaults.repairAnvils= +defaults.reforgeAnvils=blacksmith.defaults.reforgeAnvils +defaults.failReforgeRemovesEnchantments=blacksmith.defaults.failReforgeRemovesEnchantments \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 744a129..533a637 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -139,7 +139,7 @@ scrapper: defaults: # Whether the item will drop materials resulting from scrapping on the ground, instead of putting them into the user's inventory - dropItems: true + dropItem: true # The chance to fail a salvage, thus destroying the item (0-100) failSalvageChance: 0 @@ -175,8 +175,8 @@ scrapper: # The message to display when the scrapper is still on a cool-down from the previous salvaging coolDownUnexpiredMessage: "&cYou've already had your chance! Give me a break! I'll be ready {time}!" - # The message to display if the player tries to salvage an item the scrapper cannot salvage - cannotSalvageMessage: "&cI'm unable to salvage that item" + # The message to display when holding an item the blacksmith is unable to reforge + invalidItemMessage: "&cI'm sorry, but I'm a/an {title}, I don't know how to salvage that!" # The message to display if salvaging the player's item would result in no salvage tooDamagedForSalvageMessage: "&cThat item is too damaged to be salvaged into anything useful" @@ -193,5 +193,8 @@ scrapper: # The message to display once the scrapper starts salvaging startSalvageMessage: "&eOk, let's see what I can do..." - # The message to display when holding an item the blacksmith is unable to reforge - invalidItemMessage: "&cI'm sorry, but I'm a/an {title}, I don't know how to salvage that!" \ No newline at end of file + # The message to display if the player is unable to pay the scrapper's fee + insufficientFundsMessage: "&cYou don't have enough money to salvage an item!" + + # The message to display when explaining the shown item's salvage cost + costMessage: "&eIt will cost &a{cost}&e to salvage that item! Click again to salvage!" \ No newline at end of file