Improves configuration migration, and fixes some issues
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good

Fixes incorrect dropItems instead of dropItem key in config.yml
Adds some missing dropper messages to config.yml
Adds proper migration keys for all known configuration options
Adds a modified version of Stargate's StargateYamlConfiguration to allow retaining comments during configuration migration.
Fixes the annoying "[]" is not a valid repairable item
Removes the use of data keys for the global configuration
This commit is contained in:
Kristian Knarvik 2024-05-04 17:40:21 +02:00
parent 8b8890c408
commit 757fcdf139
9 changed files with 587 additions and 152 deletions

View File

@ -13,6 +13,7 @@ import net.knarcraft.blacksmith.command.scrapper.ScrapperConfigCommand;
import net.knarcraft.blacksmith.command.scrapper.ScrapperConfigTabCompleter; import net.knarcraft.blacksmith.command.scrapper.ScrapperConfigTabCompleter;
import net.knarcraft.blacksmith.command.scrapper.ScrapperEditCommand; import net.knarcraft.blacksmith.command.scrapper.ScrapperEditCommand;
import net.knarcraft.blacksmith.command.scrapper.ScrapperEditTabCompleter; 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.blacksmith.GlobalBlacksmithSettings;
import net.knarcraft.blacksmith.config.scrapper.GlobalScrapperSettings; import net.knarcraft.blacksmith.config.scrapper.GlobalScrapperSettings;
import net.knarcraft.blacksmith.formatting.BlacksmithTranslatableMessage; 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.manager.EconomyManager;
import net.knarcraft.blacksmith.trait.BlacksmithTrait; import net.knarcraft.blacksmith.trait.BlacksmithTrait;
import net.knarcraft.blacksmith.trait.ScrapperTrait; import net.knarcraft.blacksmith.trait.ScrapperTrait;
import net.knarcraft.blacksmith.util.ConfigHelper;
import net.knarcraft.knarlib.formatting.StringFormatter; import net.knarcraft.knarlib.formatting.StringFormatter;
import net.knarcraft.knarlib.formatting.TranslatableTimeUnit; import net.knarcraft.knarlib.formatting.TranslatableTimeUnit;
import net.knarcraft.knarlib.formatting.Translator; 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 net.knarcraft.knarlib.util.UpdateChecker;
import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandExecutor;
import org.bukkit.command.PluginCommand; import org.bukkit.command.PluginCommand;
import org.bukkit.command.TabCompleter; import org.bukkit.command.TabCompleter;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.plugin.PluginDescriptionFile; import org.bukkit.plugin.PluginDescriptionFile;
import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.PluginManager;
@ -40,9 +41,6 @@ import org.jetbrains.annotations.Nullable;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Map;
import java.util.logging.Level; import java.util.logging.Level;
/** /**
@ -50,11 +48,13 @@ import java.util.logging.Level;
*/ */
public class BlacksmithPlugin extends JavaPlugin { public class BlacksmithPlugin extends JavaPlugin {
private static final String CONFIG_FILE_NAME = "config.yml";
private static BlacksmithPlugin instance; private static BlacksmithPlugin instance;
private GlobalBlacksmithSettings blacksmithConfig; private GlobalBlacksmithSettings blacksmithConfig;
private GlobalScrapperSettings scrapperConfig; private GlobalScrapperSettings scrapperConfig;
private static Translator translator; private static Translator translator;
private static StringFormatter stringFormatter; private static StringFormatter stringFormatter;
private FileConfiguration configuration;
/** /**
* Constructor required for MockBukkit * Constructor required for MockBukkit
@ -107,8 +107,18 @@ public class BlacksmithPlugin extends JavaPlugin {
this.reloadConfig(); this.reloadConfig();
blacksmithConfig.load(); blacksmithConfig.load();
scrapperConfig.load(); scrapperConfig.load();
translator.loadLanguages(this.getDataFolder(), "en", translator.loadLanguages(this.getDataFolder(), "en", this.getConfiguration().getString(
this.getConfig().getString("language", "en")); "language", "en"));
}
/**
* Gets the raw configuration
*
* @return <p>The raw configuration</p>
*/
@NotNull
public FileConfiguration getConfiguration() {
return this.configuration;
} }
/** /**
@ -139,18 +149,23 @@ public class BlacksmithPlugin extends JavaPlugin {
instance = this; instance = this;
//Copy default config to disk //Copy default config to disk
FileConfiguration fileConfiguration = this.getConfig();
this.saveDefaultConfig(); this.saveDefaultConfig();
if (fileConfiguration.getString("defaults.dropItem") != null) { this.getConfig();
migrateConfig(fileConfiguration); this.configuration = new StargateYamlConfiguration();
this.saveConfig(); 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.configuration.options().copyDefaults(true);
this.reloadConfig();
this.saveConfig();
// Initialize custom configuration files // 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 //Set up Vault integration
if (!setUpVault()) { if (!setUpVault()) {
@ -174,6 +189,28 @@ public class BlacksmithPlugin extends JavaPlugin {
() -> this.getDescription().getVersion(), null); () -> 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 * 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 <p>The config to read from and write to</p>
*/
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<String, String> 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);
}
}
}
} }

View File

@ -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
*
* <p>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.</p>
*
* <p>Note: When retrieving a configuration section, that will include comments that start with "comment_". You should
* filter those before parsing input.</p>
*
* @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 <p>The configuration section to get keys for</p>
* @param deep <p>Whether to get keys for child elements as well</p>
* @return <p>The configuration section's keys, with comment entries removed</p>
*/
public static Set<String> getKeysWithoutComments(@NotNull ConfigurationSection configurationSection, boolean deep) {
Set<String> 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
*
* <p>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.</p>
*/
@NotNull
private String convertCommentsToYAMLMappings(@NotNull String configString) {
StringBuilder yamlBuilder = new StringBuilder();
List<String> 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 <p>The string builder used for building YAML</p>
* @param currentComment <p>The comment to add as a YAML string</p>
* @param line <p>The current line</p>
* @param previousIndentation <p>The indentation of the current block comment</p>
* @param commentId <p>The id of the comment</p>
*/
private void addYamlString(@NotNull StringBuilder yamlBuilder, @NotNull List<String> 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 <p>The list to add to</p>
* @param comment <p>The comment to add</p>
*/
private void addComment(@NotNull List<String> 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 <p>The string builder to add the generated YAML to</p>
* @param commentLines <p>The lines of the comment to convert into YAML</p>
* @param commentId <p>The unique id of the comment</p>
* @param indentation <p>The indentation to add to every line</p>
*/
private void generateCommentYAML(@NotNull StringBuilder yamlBuilder, @NotNull List<String> 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
*
* <p>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.</p>
*
* @param yamlString <p>A string using the YAML format</p>
* @return <p>The corresponding comment string</p>
*/
@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 <p>The string builder to write to</p>
* @param lines <p>The lines to read from</p>
* @param startIndex <p>The index to start reading from</p>
* @param commentIndentation <p>The indentation of the read comment</p>
* @return <p>The index containing the next non-comment line</p>
*/
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 <p>The number spaces to use for indentation</p>
* @return <p>A string containing the number of spaces specified</p>
*/
@NotNull
private String addIndentation(int indentationSpaces) {
return " ".repeat(Math.max(0, indentationSpaces));
}
/**
* Gets the indentation (number of spaces) of the given line
*
* @param line <p>The line to get indentation of</p>
* @return <p>The number of spaces in the line's indentation</p>
*/
private int getIndentation(@NotNull String line) {
int spacesFound = 0;
for (char aCharacter : line.toCharArray()) {
if (aCharacter == ' ') {
spacesFound++;
} else {
break;
}
}
return spacesFound;
}
}

View File

@ -1,20 +1,21 @@
package net.knarcraft.blacksmith.config.blacksmith; 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.BlacksmithPlugin;
import net.knarcraft.blacksmith.config.SettingValueType; import net.knarcraft.blacksmith.config.SettingValueType;
import net.knarcraft.blacksmith.config.Settings; import net.knarcraft.blacksmith.config.Settings;
import net.knarcraft.blacksmith.config.StargateYamlConfiguration;
import net.knarcraft.blacksmith.util.ConfigHelper; import net.knarcraft.blacksmith.util.ConfigHelper;
import net.knarcraft.blacksmith.util.InputParsingHelper; import net.knarcraft.blacksmith.util.InputParsingHelper;
import net.knarcraft.blacksmith.util.ItemHelper; import net.knarcraft.blacksmith.util.ItemHelper;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.enchantments.Enchantment; import org.bukkit.enchantments.Enchantment;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -31,28 +32,21 @@ public class GlobalBlacksmithSettings implements Settings<BlacksmithSetting> {
private final Map<BlacksmithSetting, Object> settings = new HashMap<>(); private final Map<BlacksmithSetting, Object> settings = new HashMap<>();
private final List<Material> defaultReforgeAbleMaterials = new ArrayList<>(); private final List<Material> defaultReforgeAbleMaterials = new ArrayList<>();
private final List<Enchantment> defaultEnchantmentBlockList = new ArrayList<>(); private final List<Enchantment> defaultEnchantmentBlockList = new ArrayList<>();
private final BlacksmithPlugin instance;
private final YamlStorage defaultConfig;
/** /**
* Instantiates a new "Settings" * Instantiates a new "Global Blacksmith Settings"
* *
* @param plugin <p>A reference to the blacksmith plugin</p> * @param instance <p>The blacksmith plugin to load and save configurations for</p>
*/ */
public GlobalBlacksmithSettings(BlacksmithPlugin plugin) { public GlobalBlacksmithSettings(BlacksmithPlugin instance) {
this.defaultConfig = new YamlStorage(new File(plugin.getDataFolder() + File.separator + "config.yml"), this.instance = instance;
"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.");
} }
/** /**
* Loads all configuration values from the config file * Loads all configuration values from the config file
*/ */
public void load() { public void load() {
// Load the config from disk
this.defaultConfig.load();
DataKey root = this.defaultConfig.getKey("");
// Just in case, clear existing values // Just in case, clear existing values
this.settings.clear(); this.settings.clear();
this.materialBasePrices.clear(); this.materialBasePrices.clear();
@ -60,10 +54,10 @@ public class GlobalBlacksmithSettings implements Settings<BlacksmithSetting> {
this.enchantmentCosts.clear(); this.enchantmentCosts.clear();
// Load/Save settings // Load/Save settings
loadSettings(root); loadSettings();
// Save any modified values to disk // Save any modified values to disk
this.defaultConfig.save(); instance.saveConfig();
} }
/** /**
@ -314,34 +308,51 @@ public class GlobalBlacksmithSettings implements Settings<BlacksmithSetting> {
/** /**
* Loads all global settings * Loads all global settings
*
* @param root <p>The root node of all global settings</p>
*/ */
private void loadSettings(DataKey root) { private void loadSettings() {
instance.reloadConfig();
FileConfiguration configuration = instance.getConfiguration();
for (BlacksmithSetting blacksmithSetting : BlacksmithSetting.values()) { 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 //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 { } else {
//Set the setting to the value found in the path //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(); loadReforgeAbleItems();
loadEnchantmentBlockList(); loadEnchantmentBlockList();
//Load all base prices //Load all base prices
loadBasePrices(root); loadBasePrices(configuration);
//Load all per-durability-point prices //Load all per-durability-point prices
loadPricesPerDurabilityPoint(root); loadPricesPerDurabilityPoint(configuration);
//Load all enchantment prices //Load all enchantment prices
DataKey enchantmentCostNode = root.getRelative(BlacksmithSetting.ENCHANTMENT_COST.getPath()); loadEnchantmentPrices(configuration);
}
/**
* Loads all prices for enchantments
*
* @param fileConfiguration <p>The configuration to read</p>
*/
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<String, String> relevantKeys = getRelevantKeys(enchantmentCostNode); Map<String, String> relevantKeys = getRelevantKeys(enchantmentCostNode);
for (String key : relevantKeys.keySet()) { for (String key : relevantKeys.keySet()) {
String enchantmentName = relevantKeys.get(key); String enchantmentName = relevantKeys.get(key);
Enchantment enchantment = InputParsingHelper.matchEnchantment(enchantmentName); Enchantment enchantment = InputParsingHelper.matchEnchantment(enchantmentName);
instance.getLogger().log(Level.WARNING, "loadEnchantmentPrices " + enchantmentName);
setItemPrice(this.enchantmentCosts, enchantmentName, enchantment, enchantmentCostNode.getDouble(key)); setItemPrice(this.enchantmentCosts, enchantmentName, enchantment, enchantmentCostNode.getDouble(key));
} }
} }
@ -349,10 +360,16 @@ public class GlobalBlacksmithSettings implements Settings<BlacksmithSetting> {
/** /**
* Loads all prices per durability point for all materials * Loads all prices per durability point for all materials
* *
* @param root <p>The configuration root node to search from</p> * @param fileConfiguration <p>The configuration to read</p>
*/ */
private void loadPricesPerDurabilityPoint(DataKey root) { private void loadPricesPerDurabilityPoint(@NotNull FileConfiguration fileConfiguration) {
DataKey basePerDurabilityPriceNode = root.getRelative(BlacksmithSetting.PRICE_PER_DURABILITY_POINT.getPath()); 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<String, String> relevantKeys = getRelevantKeys(basePerDurabilityPriceNode); Map<String, String> relevantKeys = getRelevantKeys(basePerDurabilityPriceNode);
for (String key : relevantKeys.keySet()) { for (String key : relevantKeys.keySet()) {
@ -371,10 +388,16 @@ public class GlobalBlacksmithSettings implements Settings<BlacksmithSetting> {
/** /**
* Loads base prices for all materials * Loads base prices for all materials
* *
* @param root <p>The configuration root node to search from</p> * @param fileConfiguration <p>The configuration to read</p>
*/ */
private void loadBasePrices(DataKey root) { private void loadBasePrices(@NotNull FileConfiguration fileConfiguration) {
DataKey basePriceNode = root.getRelative(BlacksmithSetting.BASE_PRICE.getPath()); 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<String, String> relevantKeys = getRelevantKeys(basePriceNode); Map<String, String> relevantKeys = getRelevantKeys(basePriceNode);
for (String key : relevantKeys.keySet()) { for (String key : relevantKeys.keySet()) {
@ -397,7 +420,7 @@ public class GlobalBlacksmithSettings implements Settings<BlacksmithSetting> {
* @param materialName <p>The material name to match</p> * @param materialName <p>The material name to match</p>
* @param price <p>The price to set for the matched materials</p> * @param price <p>The price to set for the matched materials</p>
*/ */
private void setMatchedMaterialPrices(Map<Material, Double> prices, String materialName, double price) { private void setMatchedMaterialPrices(@NotNull Map<Material, Double> prices, @NotNull String materialName, double price) {
String search = InputParsingHelper.regExIfy(materialName); String search = InputParsingHelper.regExIfy(materialName);
for (Material material : ItemHelper.getAllReforgeAbleMaterials()) { for (Material material : ItemHelper.getAllReforgeAbleMaterials()) {
if (material.name().matches(search)) { if (material.name().matches(search)) {
@ -414,31 +437,31 @@ public class GlobalBlacksmithSettings implements Settings<BlacksmithSetting> {
* @param item <p>The material parsed from the name</p> * @param item <p>The material parsed from the name</p>
* @param price <p>The price to set</p> * @param price <p>The price to set</p>
*/ */
private <K> void setItemPrice(Map<K, Double> prices, String itemName, K item, double price) { private <K> void setItemPrice(@NotNull Map<K, Double> prices, @NotNull String itemName, @Nullable K item,
double price) {
if (item != null) { if (item != null) {
prices.put(item, price); prices.put(item, price);
} else { } else {
BlacksmithPlugin.getInstance().getLogger().log(Level.WARNING, instance.getLogger().log(Level.WARNING, "Unable to find a material/enchantment matching " + itemName);
"Unable to find a material/enchantment matching " + itemName);
} }
} }
/** /**
* Gets a map between relevant keys and their normalized name * Gets a map between relevant keys and their normalized name
* *
* @param rootKey <p>The root data key containing sub-keys</p> * @param configurationSection <p>The configuration section to search</p>
* @return <p>Any sub-keys found that aren't the default</p> * @return <p>Any sub-keys found that aren't the default</p>
*/ */
private Map<String, String> getRelevantKeys(DataKey rootKey) { @NotNull
private Map<String, String> getRelevantKeys(@NotNull ConfigurationSection configurationSection) {
Map<String, String> relevant = new HashMap<>(); Map<String, String> relevant = new HashMap<>();
for (DataKey dataKey : rootKey.getSubKeys()) { for (String dataKey : StargateYamlConfiguration.getKeysWithoutComments(configurationSection, false)) {
String keyName = dataKey.name();
//Skip the default value //Skip the default value
if (keyName.equals("default")) { if (dataKey.equals("default")) {
continue; continue;
} }
String normalizedName = keyName.toUpperCase().replace("-", "_"); String normalizedName = dataKey.toUpperCase().replace("-", "_");
relevant.put(keyName, normalizedName); relevant.put(dataKey, normalizedName);
} }
return relevant; return relevant;
} }
@ -449,7 +472,8 @@ public class GlobalBlacksmithSettings implements Settings<BlacksmithSetting> {
* @param normalizedName <p>The normalized name to un-normalize</p> * @param normalizedName <p>The normalized name to un-normalize</p>
* @return <p>The un-normalized name</p> * @return <p>The un-normalized name</p>
*/ */
private String unNormalizeName(String normalizedName) { @NotNull
private String unNormalizeName(@NotNull String normalizedName) {
return normalizedName.toLowerCase().replace("_", "-"); return normalizedName.toLowerCase().replace("_", "-");
} }
@ -481,32 +505,66 @@ public class GlobalBlacksmithSettings implements Settings<BlacksmithSetting> {
* Saves all current settings to the config file * Saves all current settings to the config file
*/ */
private void save() { private void save() {
DataKey root = this.defaultConfig.getKey(""); FileConfiguration fileConfiguration = instance.getConfiguration();
//Save all default settings //Save all default settings
for (BlacksmithSetting setting : BlacksmithSetting.values()) { 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 //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()) { 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 //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()) { 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 //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()) { 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 //Perform the actual save to disk
this.defaultConfig.save(); instance.saveConfig();
}
/**
* Gets a configuration section, creating it if necessary
*
* @param fileConfiguration <p>The file configuration to read</p>
* @param sectionPath <p>The path to the configuration section to get</p>
* @return <p>The configuration section</p>
*/
@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 <p>The configuration path to get the base of</p>
* @return <p>The base configuration path</p>
*/
@NotNull
private String getBase(@NotNull String configurationPath) {
String[] parts = configurationPath.split("\\.");
String[] base = Arrays.copyOfRange(parts, 0, parts.length - 1);
return String.join(".", base);
} }
} }

View File

@ -1,17 +1,15 @@
package net.knarcraft.blacksmith.config.scrapper; 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.BlacksmithPlugin;
import net.knarcraft.blacksmith.config.SettingValueType; import net.knarcraft.blacksmith.config.SettingValueType;
import net.knarcraft.blacksmith.config.Settings; import net.knarcraft.blacksmith.config.Settings;
import net.knarcraft.blacksmith.util.ConfigHelper; import net.knarcraft.blacksmith.util.ConfigHelper;
import net.knarcraft.blacksmith.util.ItemHelper; import net.knarcraft.blacksmith.util.ItemHelper;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.configuration.file.FileConfiguration;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
@ -29,35 +27,29 @@ public class GlobalScrapperSettings implements Settings<ScrapperSetting> {
private final List<Material> defaultSalvageableMaterials = new ArrayList<>(); private final List<Material> defaultSalvageableMaterials = new ArrayList<>();
private final Map<Material, Set<Material>> ignoredSalvage = new HashMap<>(); private final Map<Material, Set<Material>> ignoredSalvage = new HashMap<>();
private final YamlStorage defaultConfig; private final BlacksmithPlugin instance;
/** /**
* Instantiates a new "Settings" * Instantiates a new "Settings"
* *
* @param plugin <p>A reference to the blacksmith plugin</p> * @param instance <p>A reference to the blacksmith plugin</p>
*/ */
public GlobalScrapperSettings(BlacksmithPlugin plugin) { public GlobalScrapperSettings(BlacksmithPlugin instance) {
this.defaultConfig = new YamlStorage(new File(plugin.getDataFolder() + File.separator + "config.yml"), this.instance = instance;
"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.");
} }
/** /**
* Loads all configuration values from the config file * Loads all configuration values from the config file
*/ */
public void load() { public void load() {
//Load the config from disk
this.defaultConfig.load();
DataKey root = this.defaultConfig.getKey("");
//Just in case, clear existing values //Just in case, clear existing values
this.settings.clear(); this.settings.clear();
//Load/Save global settings //Load/Save global settings
loadSettings(root); loadSettings();
//Save any modified values to disk //Save any modified values to disk
this.defaultConfig.save(); instance.saveConfig();
} }
/** /**
@ -137,17 +129,18 @@ public class GlobalScrapperSettings implements Settings<ScrapperSetting> {
/** /**
* Loads all global settings * Loads all global settings
*
* @param root <p>The root node of all global settings</p>
*/ */
private void loadSettings(DataKey root) { private void loadSettings() {
instance.reloadConfig();
FileConfiguration configuration = instance.getConfiguration();
for (ScrapperSetting setting : ScrapperSetting.values()) { 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 //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 { } else {
//Set the setting to the value found in the path //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(); loadSalvageAbleItems();
@ -158,14 +151,15 @@ public class GlobalScrapperSettings implements Settings<ScrapperSetting> {
* Saves all current settings to the config file * Saves all current settings to the config file
*/ */
private void save() { private void save() {
DataKey root = this.defaultConfig.getKey(""); FileConfiguration fileConfiguration = instance.getConfiguration();
//Save all default settings //Save all default settings
for (ScrapperSetting setting : ScrapperSetting.values()) { 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 //Perform the actual save to disk
this.defaultConfig.save(); instance.saveConfig();
} }
/** /**

View File

@ -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 * 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" + 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), " the one just evaluated", true, true),
/** /**

View File

@ -1,10 +1,23 @@
package net.knarcraft.blacksmith.util; 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.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; 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 * 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 <p>The current config to back up</p>
*/
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<String, String> 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 <p>The configuration fields to be migrated</p>
* @param key <p>The key/path of the property to migrate</p>
* @param oldConfiguration <p>The original pre-migration configuration</p>
*/
private static void migrateProperty(@NotNull Map<String, String> 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);
}
} }

View File

@ -244,7 +244,7 @@ public final class ItemHelper {
//Parse every material, and add to reforgeAble items //Parse every material, and add to reforgeAble items
for (String item : itemList) { for (String item : itemList) {
//Ignore ",," //Ignore ",,"
if (InputParsingHelper.isEmpty(item)) { if (InputParsingHelper.isEmpty(item) || item.matches("\\[]")) {
continue; continue;
} }

View File

@ -1,9 +1,38 @@
defaults=blacksmith.defaults global.basePrice=blacksmith.global.basePrice
global=blacksmith.global global.pricePerDurabilityPoint=blacksmith.global.pricePerDurabilityPoint
scrapper.defaults.delaysInSeconds.minimum=scrapper.defaults.minSalvageWaitTimeSeconds global.enchantmentCost=blacksmith.global.enchantmentCost
scrapper.defaults.delaysInSeconds.maximum=scrapper.defaults.maxSalvageWaitTimeSeconds global.useNaturalCost=blacksmith.global.useNaturalCost
scrapper.defaults.delaysInSeconds.salvageCoolDown=scrapper.defaults.salvageCoolDownSeconds global.showExactTime=blacksmith.global.showExactTime
blacksmith.defaults.delaysInSeconds.minimum=blacksmith.defaults.minReforgeWaitTimeSeconds global.chippedAnvilRepairCost=blacksmith.global.chippedAnvilReforgingCost
blacksmith.defaults.delaysInSeconds.maximum=blacksmith.defaults.maxReforgeWaitTimeSeconds global.damagedAnvilRepairCost=blacksmith.global.damagedAnvilReforgingCost
blacksmith.defaults.delaysInSeconds.reforgeCoolDown=blacksmith.defaults.reforgeCoolDownSeconds global.chippedAnvilReforgingCost=blacksmith.global.chippedAnvilReforgingCost
blacksmith.defaults.enchantmentBlocklist=blacksmith.defaults.enchantmentBlockList 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

View File

@ -139,7 +139,7 @@ scrapper:
defaults: defaults:
# Whether the item will drop materials resulting from scrapping on the ground, instead of putting them into the user's inventory # 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) # The chance to fail a salvage, thus destroying the item (0-100)
failSalvageChance: 0 failSalvageChance: 0
@ -175,8 +175,8 @@ scrapper:
# The message to display when the scrapper is still on a cool-down from the previous salvaging # 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}!" 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 # The message to display when holding an item the blacksmith is unable to reforge
cannotSalvageMessage: "&cI'm unable to salvage that item" 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 # 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" 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 # The message to display once the scrapper starts salvaging
startSalvageMessage: "&eOk, let's see what I can do..." startSalvageMessage: "&eOk, let's see what I can do..."
# The message to display when holding an item the blacksmith is unable to reforge # The message to display if the player is unable to pay the scrapper's fee
invalidItemMessage: "&cI'm sorry, but I'm a/an {title}, I don't know how to salvage that!" 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!"