diff --git a/.classpath b/.classpath new file mode 100644 index 0000000..bf02bb5 --- /dev/null +++ b/.classpath @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.project b/.project new file mode 100644 index 0000000..43e0905 --- /dev/null +++ b/.project @@ -0,0 +1,15 @@ + + + DivinityEconomy + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.jdt.core.javanature + + diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e0f15db --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.configuration.updateBuildConfiguration": "automatic" +} \ No newline at end of file diff --git a/DivinityEconomy.eml b/DivinityEconomy.eml new file mode 100644 index 0000000..54cded0 --- /dev/null +++ b/DivinityEconomy.eml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DivinityEconomy.userlibraries b/DivinityEconomy.userlibraries new file mode 100644 index 0000000..7ac4270 --- /dev/null +++ b/DivinityEconomy.userlibraries @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml index c1fb736..3517698 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.divinitycraft DivinityEconomy - 3.5.7 + 3.6.Kodak DivinityEconomy - The Global Market https://divinitycraft.org @@ -37,11 +37,6 @@ - - com.tchristofferson - ConfigUpdater - 2.1-SNAPSHOT - org.mariuszgromada.math MathParser.org-mXparser diff --git a/src/main/java/org/divinitycraft/divinityeconomy/DEPlugin.java b/src/main/java/org/divinitycraft/divinityeconomy/DEPlugin.java index fa8a028..0aaeffd 100644 --- a/src/main/java/org/divinitycraft/divinityeconomy/DEPlugin.java +++ b/src/main/java/org/divinitycraft/divinityeconomy/DEPlugin.java @@ -174,6 +174,7 @@ public void onEnable() { new ESetValue(this); new ESetValueTC(this); new Reload(this); + new Modded(this); new ReloadTC(this); new Save(this); new SaveTC(this); diff --git a/src/main/java/org/divinitycraft/divinityeconomy/commands/admin/Modded.java b/src/main/java/org/divinitycraft/divinityeconomy/commands/admin/Modded.java new file mode 100644 index 0000000..1dc3484 --- /dev/null +++ b/src/main/java/org/divinitycraft/divinityeconomy/commands/admin/Modded.java @@ -0,0 +1,36 @@ +package org.divinitycraft.divinityeconomy.commands.admin; + +import org.divinitycraft.divinityeconomy.DEPlugin; +import org.divinitycraft.divinityeconomy.commands.DivinityCommand; +import org.divinitycraft.divinityeconomy.config.Setting; +import org.divinitycraft.divinityeconomy.lang.LangEntry; +import org.bukkit.entity.Player; + +public class Modded extends DivinityCommand { + + public Modded(DEPlugin main) { + super(main, "modded", true, Setting.COMMAND_RELOAD_ENABLE_BOOLEAN); + } + + @Override + public boolean onPlayerCommand(Player sender, String[] args) { + int reloaded = getMain().getMatMan().reloadModdedItems(); + if (reloaded > 0) { + getMain().getConsole().info(sender, "Reloaded %d modded materials", reloaded); + } else { + getMain().getConsole().info(sender, "No modded materials were reloaded"); + } + return true; + } + + @Override + public boolean onConsoleCommand(String[] args) { + int reloaded = getMain().getMatMan().reloadModdedItems(); + if (reloaded > 0) { + getMain().getConsole().info("Reloaded %d modded materials", reloaded); + } else { + getMain().getConsole().info("No modded materials were reloaded"); + } + return true; + } +} diff --git a/src/main/java/org/divinitycraft/divinityeconomy/config/ConfigManager.java b/src/main/java/org/divinitycraft/divinityeconomy/config/ConfigManager.java index a95fe56..dfa59ca 100644 --- a/src/main/java/org/divinitycraft/divinityeconomy/config/ConfigManager.java +++ b/src/main/java/org/divinitycraft/divinityeconomy/config/ConfigManager.java @@ -1,13 +1,12 @@ package org.divinitycraft.divinityeconomy.config; -import com.tchristofferson.configupdater.ConfigUpdater; import org.divinitycraft.divinityeconomy.DEPlugin; import org.divinitycraft.divinityeconomy.DivinityModule; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; import java.io.File; -import java.io.IOException; + import java.io.InputStreamReader; import java.util.Arrays; import java.util.Collections; @@ -44,8 +43,13 @@ public void init() { this.saveFile(config, configFile); try { - ConfigUpdater.update(getMain(), configFile, this.getFile(configFile), Collections.emptyList()); - } catch (IOException e) { + try { + Class cfgUpd = Class.forName("com.tchristofferson.configupdater.ConfigUpdater"); + java.lang.reflect.Method m = cfgUpd.getMethod("update", org.bukkit.plugin.Plugin.class, String.class, java.io.File.class, java.util.List.class); + m.invoke(null, getMain(), configFile, this.getFile(configFile), Collections.emptyList()); + } catch (ClassNotFoundException ignored) { + } + } catch (Exception e) { e.printStackTrace(); } getMain().reloadConfig(); diff --git a/src/main/java/org/divinitycraft/divinityeconomy/market/TokenManager.java b/src/main/java/org/divinitycraft/divinityeconomy/market/TokenManager.java index f1d1faa..d179702 100644 --- a/src/main/java/org/divinitycraft/divinityeconomy/market/TokenManager.java +++ b/src/main/java/org/divinitycraft/divinityeconomy/market/TokenManager.java @@ -1,6 +1,5 @@ package org.divinitycraft.divinityeconomy.market; -import com.tchristofferson.configupdater.ConfigUpdater; import org.divinitycraft.divinityeconomy.Constants; import org.divinitycraft.divinityeconomy.DEPlugin; import org.divinitycraft.divinityeconomy.DivinityModule; @@ -13,6 +12,8 @@ import java.io.File; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; import java.util.Collections; import java.util.HashSet; import java.util.Map; @@ -484,54 +485,83 @@ public void loadAliases() { aliasFile.createNewFile(); } - // Run Update - ConfigUpdater.update(getMain(), this.aliasFile, aliasFile, Collections.emptyList()); + // Run Update via reflection if ConfigUpdater is available; otherwise skip + try { + Class cfgUpd = Class.forName("com.tchristofferson.configupdater.ConfigUpdater"); + java.lang.reflect.Method m = cfgUpd.getMethod("update", org.bukkit.plugin.Plugin.class, String.class, java.io.File.class, java.util.List.class); + m.invoke(null, getMain(), this.aliasFile, aliasFile, Collections.emptyList()); + } catch (ClassNotFoundException ignored) { + } catch (Exception ignored) { + } } catch (IOException e) { e.printStackTrace(); } - // Load config - FileConfiguration config = this.getConfMan().loadFile(this.aliasFile); + // Load config with error handling for corrupted YAML + FileConfiguration config = null; + try { + config = this.getConfMan().loadFile(this.aliasFile); + } catch (Exception e) { + this.getConsole().warn("Failed to load %s (YAML may be corrupted): %s", this.aliasFile, e.getMessage()); + // Create backup and start fresh + try { + File backup = new File(this.getConfMan().getFile(this.aliasFile).getAbsolutePath() + ".backup"); + Files.copy( + this.getConfMan().getFile(this.aliasFile).toPath(), + backup.toPath(), + StandardCopyOption.REPLACE_EXISTING + ); + this.getConsole().warn("Created backup at %s", backup.getAbsolutePath()); + } catch (Exception ignored) { + } + config = this.getConfMan().readResource(this.aliasFile); + } // Store the alias -> ItemID pairs Map values = new ConcurrentHashMap<>(); // Store ItemID -> arraylist to migrate to ItemID -> String[] pairs Map> revBuildAliasValues = new ConcurrentHashMap<>(); Map> revResultAliasValues = new ConcurrentHashMap<>(); - // Loop through keys in config - for (String key : config.getKeys(false)) { - // Get string item name - // Format key/value - key = key.toLowerCase().replace(" ", ""); - String value = config.getString(key); - - // If value is null, skip - if (value == null) { - if (!this.getConfMan().getBoolean(Setting.IGNORE_ALIAS_ERRORS_BOOLEAN)) - this.getConsole().warn("Bad config value in %s: '%s' - Corresponding value is null", this.aliasFile, key); - continue; - } - - value = value.toLowerCase().replace(" ", ""); - - // If the value is not stored in the items map, skip - if (this.getItem(value) == null) { - if (!this.getConfMan().getBoolean(Setting.IGNORE_ALIAS_ERRORS_BOOLEAN)) - this.getConsole().warn("Bad config value in %s: '%s' - Corresponding value '%s' does not exist.", this.aliasFile, key, value); - continue; - } - - // Store the value under the key - values.put(key, value); - if (!values.containsKey(key)) values.put(key, key); - - // Store the value under the key (Value = materialID, key = alias) - if (!revBuildAliasValues.containsKey(value)) { - revBuildAliasValues.put(value, new HashSet<>()); - revBuildAliasValues.get(value).add(value); + + // Loop through keys in config safely + try { + for (String key : config.getKeys(false)) { + // Get string item name + // Format key/value + key = key.toLowerCase().replace(" ", ""); + String value = config.getString(key); + + // If value is null, skip + if (value == null) { + if (!this.getConfMan().getBoolean(Setting.IGNORE_ALIAS_ERRORS_BOOLEAN)) + this.getConsole().warn("Bad config value in %s: '%s' - Corresponding value is null", this.aliasFile, key); + continue; + } + + value = value.toLowerCase().replace(" ", ""); + + // If the value is not stored in the items map, skip + if (this.getItem(value) == null) { + if (!this.getConfMan().getBoolean(Setting.IGNORE_ALIAS_ERRORS_BOOLEAN)) + this.getConsole().warn("Bad config value in %s: '%s' - Corresponding value '%s' does not exist.", this.aliasFile, key, value); + continue; + } + + // Store the value under the key + values.put(key, value); + if (!values.containsKey(key)) values.put(key, key); + + // Store the value under the key (Value = materialID, key = alias) + if (!revBuildAliasValues.containsKey(value)) { + revBuildAliasValues.put(value, new HashSet<>()); + revBuildAliasValues.get(value).add(value); + } + revBuildAliasValues.get(value).add(key); } - revBuildAliasValues.get(value).add(key); + } catch (Exception e) { + this.getConsole().warn("Error loading aliases from %s: %s", this.aliasFile, e.getMessage()); } + this.aliasMap = values; // Migrate all keys-arraylist pairs to key-array pairs @@ -541,6 +571,67 @@ public void loadAliases() { this.getConsole().info(LangEntry.MARKET_ItemAliasesLoaded.get(getMain()), values.size(), this.aliasFile); } + /** + * Adds an alias for an item to the runtime alias maps. + * The alias will be persisted to file at the next saveAliases() call. + * @param alias - The alias name (will be lowercased) + * @param itemId - The item ID to point to + */ + public void addAlias(String alias, String itemId) { + if (alias == null || itemId == null) return; + + try { + alias = alias.toLowerCase().replace(" ", ""); + itemId = itemId.toLowerCase().replace(" ", ""); + + // Check if item exists + if (this.getItem(itemId) == null) { + if (this.getConsole() != null) { + this.getConsole().warn("Cannot add alias '%s': item '%s' does not exist", alias, itemId); + } + return; + } + + // Add to aliasMap + this.aliasMap.put(alias, itemId); + + // Add to revAliasMap + if (!this.revAliasMap.containsKey(itemId)) { + this.revAliasMap.put(itemId, new HashSet<>()); + this.revAliasMap.get(itemId).add(itemId); + } + this.revAliasMap.get(itemId).add(alias); + } catch (Exception e) { + if (this.getConsole() != null) { + this.getConsole().warn("Failed to add alias '%s': %s", alias, e.getMessage()); + } + } + } + + /** + * Saves all aliases to the alias config file asynchronously. + * This prevents blocking the main server thread. + */ + public void saveAliases() { + // Run save in a separate thread to prevent blocking server + new Thread(() -> { + try { + FileConfiguration config = this.getConfMan().loadFile(this.aliasFile); + + // Write all aliases from the current aliasMap + for (String alias : this.aliasMap.keySet()) { + config.set(alias, this.aliasMap.get(alias)); + } + + this.getConfMan().saveFile(config, this.aliasFile); + } catch (Exception e) { + if (this.getConsole() != null) { + this.getConsole().warn("Failed to save aliases: %s", e.getMessage()); + } + } + }).start(); + } + /** * Loads the items from the items file into the items variable */ @@ -553,8 +644,14 @@ public void loadItems() { itemFile.createNewFile(); } - // Run Update - ConfigUpdater.update(getMain(), this.itemFile, this.getConfMan().getFile(this.itemFile), Collections.emptyList()); + // Run Update via reflection if ConfigUpdater is available; otherwise skip + try { + Class cfgUpd = Class.forName("com.tchristofferson.configupdater.ConfigUpdater"); + java.lang.reflect.Method m = cfgUpd.getMethod("update", org.bukkit.plugin.Plugin.class, String.class, java.io.File.class, java.util.List.class); + m.invoke(null, getMain(), this.itemFile, this.getConfMan().getFile(this.itemFile), Collections.emptyList()); + } catch (ClassNotFoundException ignored) { + } catch (Exception ignored) { + } } catch (IOException e) { e.printStackTrace(); } @@ -570,6 +667,7 @@ public void loadItems() { Map values = new ConcurrentHashMap<>(); // Loop through keys and get data // Add data to a MaterialData and put in HashMap under key + // First load vanilla items from defaultConf for (String key : defaultConf.getKeys(false)) { ConfigurationSection data = this.config.getConfigurationSection(key); @@ -595,6 +693,33 @@ public void loadItems() { this.totalItems += itemData.getQuantity(); values.put(key, itemData); } + + // Also load modded items that exist in config but not in defaultConf + // These are items that were dynamically imported and saved to materials.yml + for (String key : this.config.getKeys(false)) { + if (defaultConf.contains(key)) continue; // Skip vanilla items already loaded + + ConfigurationSection data = this.config.getConfigurationSection(key); + if (data == null) { + if (!this.getConfMan().getBoolean(Setting.IGNORE_ITEM_ERRORS_BOOLEAN)) + this.getConsole().warn("Bad config value in %s: '%s' - Data is null, skipping modded item.", this.itemFile, key); + continue; + } + + // For modded items, use the item config as default since they don't have a vanilla default + MarketableToken itemData = this.loadItem(key, data, data); + if (!itemData.check()) { + if (!this.getConfMan().getBoolean(Setting.IGNORE_ITEM_ERRORS_BOOLEAN)) + this.getConsole().warn("Bad config value in %s for '%s': %s (modded item)", this.itemFile, key, itemData.getError()); + continue; + } + + String formattedKey = key.toLowerCase().replace(" ", ""); + this.defaultTotalItems += itemData.getDefaultQuantity(); + this.totalItems += itemData.getQuantity(); + values.put(formattedKey, itemData); + } + // Copy values into items this.itemMap = values; this.getConsole().info(LangEntry.MARKET_ItemsLoaded.get(getMain()), values.size(), this.totalItems, this.defaultTotalItems, this.itemFile); @@ -650,12 +775,25 @@ public void saveItems() { private void saveFile() { // load back all info FileConfiguration config = this.getConfMan().loadFile(this.itemFile); + + // Save all existing keys that are in the itemMap for (String key : config.getKeys(false)) { String internalKey = key.toLowerCase().replace(" ", ""); if (!this.itemMap.containsKey(internalKey)) continue; config.set(key, this.config.get(key)); } + + // Also save any new items that exist in the itemMap but weren't in the original config + for (String mapKey : this.itemMap.keySet()) { + MarketableToken token = this.itemMap.get(mapKey); + String originalKey = token.getID(); // Use original ID as the key in the file + + // Only add if not already processed above + if (!config.contains(originalKey)) { + config.set(originalKey, this.config.getConfigurationSection(mapKey)); + } + } this.getConfMan().saveFile(config, this.itemFile); } diff --git a/src/main/java/org/divinitycraft/divinityeconomy/market/items/materials/MarketableMaterial.java b/src/main/java/org/divinitycraft/divinityeconomy/market/items/materials/MarketableMaterial.java index 09dd468..8b8d609 100644 --- a/src/main/java/org/divinitycraft/divinityeconomy/market/items/materials/MarketableMaterial.java +++ b/src/main/java/org/divinitycraft/divinityeconomy/market/items/materials/MarketableMaterial.java @@ -6,6 +6,7 @@ import org.divinitycraft.divinityeconomy.market.items.MarketableItem; import org.divinitycraft.divinityeconomy.player.PlayerManager; import org.bukkit.Material; +import org.bukkit.NamespacedKey; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; @@ -18,15 +19,69 @@ public abstract class MarketableMaterial extends MarketableItem { public MarketableMaterial(DEPlugin main, MaterialManager itemManager, String ID, ConfigurationSection config, ConfigurationSection defaultConfig) { super(main, itemManager, ID, config, defaultConfig); - Material material; - try { - material = Material.valueOf( - config.getString(MapKeys.MATERIAL_ID.key, ID) - ); - } catch (IllegalArgumentException | NullPointerException exception) { - material = null; + String materialId = config.getString(MapKeys.MATERIAL_ID.key, ID); + Material mat = null; + if (materialId != null) { + try { + mat = Material.valueOf(materialId); + } catch (IllegalArgumentException ignored) { + } + + if (mat == null) { + try { + mat = Material.matchMaterial(materialId); + } catch (Throwable ignored) { + } + } + + if (mat == null) { + try { + NamespacedKey key = NamespacedKey.fromString(materialId); + if (key != null) { + try { + mat = Material.matchMaterial(key.toString()); + } catch (Throwable ignored) { + } + } + } catch (Throwable ignored) { + } + } + + // Final fallback: iterate registered Materials and match by NamespacedKey or key name (for modded materials) + if (mat == null && materialId != null) { + for (Material m : Material.values()) { + try { + NamespacedKey mk = m.getKey(); + if (mk != null) { + if (mk.toString().equalsIgnoreCase(materialId) || mk.getKey().equalsIgnoreCase(materialId)) { + mat = m; + break; + } + } + if (m.name().equalsIgnoreCase(materialId)) { + mat = m; + break; + } + } catch (Throwable ignored) { + } + } + } + } + + this.material = mat; + + // If this material couldn't be resolved to a vanilla Bukkit Material + // treat it as a modded/non-vanilla item by defaulting quantity to 0 + // and disallowing it unless the server admin explicitly sets values + // in the materials.yml config. + if (this.material == null) { + if (!this.itemConfig.contains(MapKeys.QUANTITY.key)) { + this.itemConfig.set(MapKeys.QUANTITY.key, 0); + } + if (!this.itemConfig.contains(MapKeys.ALLOWED.key)) { + this.itemConfig.set(MapKeys.ALLOWED.key, false); + } } - this.material = material; } @@ -75,6 +130,9 @@ public Material getMaterial() { * @return */ public ItemStack[] getItemStacks(int amount) { + // If material couldn't be resolved, return empty array + if (this.getMaterial() == null) return new ItemStack[0]; + // Get the max stack size and the number of stacks int maxStackSize = this.getMaterial().getMaxStackSize(); int stacks = (int) Math.ceil((double) amount / maxStackSize); @@ -99,6 +157,7 @@ public ItemStack[] getItemStacks(int amount) { * @return ItemStack[] - An array of the ItemStack's in the player of material */ public ItemStack[] getMaterialSlots(Player player) { + if (this.material == null) return new ItemStack[0]; Map inventory = player.getInventory().all(this.material); ArrayList itemStacks = new ArrayList<>(); for (ItemStack itemStack : inventory.values()) { diff --git a/src/main/java/org/divinitycraft/divinityeconomy/market/items/materials/MaterialManager.java b/src/main/java/org/divinitycraft/divinityeconomy/market/items/materials/MaterialManager.java index 97d155b..421f0c6 100644 --- a/src/main/java/org/divinitycraft/divinityeconomy/market/items/materials/MaterialManager.java +++ b/src/main/java/org/divinitycraft/divinityeconomy/market/items/materials/MaterialManager.java @@ -3,6 +3,7 @@ import org.divinitycraft.divinityeconomy.DEPlugin; import org.divinitycraft.divinityeconomy.config.Setting; import org.divinitycraft.divinityeconomy.lang.LangEntry; +import org.divinitycraft.divinityeconomy.market.MapKeys; import org.divinitycraft.divinityeconomy.market.MarketableToken; import org.divinitycraft.divinityeconomy.market.items.ItemManager; import org.divinitycraft.divinityeconomy.utils.Converter; @@ -13,6 +14,9 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.NamespacedKey; +import org.bukkit.Material; public abstract class MaterialManager extends ItemManager { @@ -56,6 +60,16 @@ public void run() { this.saveTimer.runTaskTimerAsynchronously(getMain(), timer, timer); this.loadItems(); this.loadAliases(); + // Schedule a delayed attempt to resolve modded (non-vanilla) materials + // Some hybrid servers (NeoForge/Arclight) may register modded materials + // after plugin enable; this will re-attempt resolution shortly after. + new BukkitRunnable() { + @Override + public void run() { + int reloaded = reloadModdedItems(); + if (reloaded > 0) getConsole().info("Reloaded %d modded materials", reloaded); + } + }.runTaskLater(getMain(), 200L); // ~10 seconds // this.checkLoadedItems(); - This is for internal debugging only. Hi! :) this.getMarkMan().addManager(this); } @@ -188,4 +202,147 @@ public void checkLoadedItems() { public Set getLocalKeys() { return new HashSet<>(); } + + /** + * Re-attempt to resolve modded / non-vanilla materials which previously + * failed to resolve at load time. This creates fresh MarketableMaterial + * instances via {@link #loadItem} and replaces entries that now resolve. + * + * @return number of items successfully reloaded with a resolved Material + */ + public int reloadModdedItems() { + int reloaded = 0; + try { + // First, attempt to resolve any existing unresolved entries (previous behaviour) + // Collect candidates whose Material is currently unresolved + Set candidates = new HashSet<>(); + for (String key : this.itemMap.keySet()) { + MarketableToken token = this.itemMap.get(key); + if (!(token instanceof MarketableMaterial)) continue; + MarketableMaterial mat = (MarketableMaterial) token; + if (mat.getMaterial() == null) candidates.add(key); + } + + if (candidates.isEmpty()) { + this.getConsole().debug("No unresolved modded materials found to reload."); + // Continue - even if there were no unresolved entries, we still want to scan for new modded materials + } else { + this.getConsole().info("Found %d unresolved modded material entries to attempt reload.", candidates.size()); + + for (String key : candidates) { + MarketableMaterial token = (MarketableMaterial) this.itemMap.get(key); + String materialId = token.getItemConfig().getString(MapKeys.MATERIAL_ID.key, token.getID()); + this.getConsole().info("Attempting to reload '%s' (configured MATERIAL_ID='%s')", key, materialId); + + MarketableMaterial newMat = (MarketableMaterial) this.loadItem(token.getID(), token.getItemConfig(), token.getDefaultItemConfig()); + if (newMat != null && newMat.check() && newMat.getMaterial() != null) { + // adjust totals + this.defaultTotalItems -= token.getDefaultQuantity(); + this.totalItems -= token.getQuantity(); + this.defaultTotalItems += newMat.getDefaultQuantity(); + this.totalItems += newMat.getQuantity(); + + ((Map) this.itemMap).put(key, newMat); + reloaded++; + this.getConsole().info("Successfully reloaded '%s' -> resolved as %s", key, newMat.getMaterial().name()); + } else { + this.getConsole().warn("Failed to resolve material for '%s' (MATERIAL_ID='%s')", key, materialId); + } + } + } + + // Second, scan the current server Material registry for non-vanilla (modded) materials + // and import any that are not yet present in the market config. + int imported = 0; + Set discovered = new HashSet<>(); + for (Material m : Material.values()) { + try { + NamespacedKey k = m.getKey(); + if (k == null) continue; + String namespace = k.getNamespace(); + if (namespace == null) continue; + if ("minecraft".equals(namespace)) continue; // skip vanilla + + String nsKey = k.toString(); // modid:item + // avoid duplicates + if (discovered.contains(nsKey)) continue; + discovered.add(nsKey); + + // Check if already represented in itemMap (by MATERIAL_ID or ID) + // OR if it already exists in the config file (user may have edited it manually) + boolean exists = false; + + // Check itemMap first + for (MarketableToken token : this.itemMap.values()) { + String mid = token.getItemConfig().getString(MapKeys.MATERIAL_ID.key, token.getID()); + if (mid == null) continue; + if (mid.equalsIgnoreCase(nsKey) || mid.equalsIgnoreCase(m.name()) || mid.equalsIgnoreCase(k.getKey())) { + exists = true; + break; + } + } + + // Also check the config file itself to avoid overwriting user edits + if (!exists && this.config.contains(nsKey)) { + exists = true; + } + + if (exists) continue; + + // Create config section for this modded material with sensible defaults + this.getConsole().info("Importing modded material: %s", nsKey); + this.config.set(nsKey + "." + MapKeys.MATERIAL_ID.key, nsKey); + this.config.set(nsKey + "." + MapKeys.QUANTITY.key, 0); + this.config.set(nsKey + "." + MapKeys.ALLOWED.key, true); + + // Create a minimal default section for loadItem + YamlConfiguration defaultSec = new YamlConfiguration(); + defaultSec.set(MapKeys.QUANTITY.key, 0); + defaultSec.set(MapKeys.ALLOWED.key, true); + + MarketableMaterial newMat = (MarketableMaterial) this.loadItem(nsKey, this.config.getConfigurationSection(nsKey), defaultSec); + if (newMat != null && newMat.check() && newMat.getMaterial() != null) { + // add into map and adjust totals + this.defaultTotalItems += newMat.getDefaultQuantity(); + this.totalItems += newMat.getQuantity(); + ((Map) this.itemMap).put(nsKey.toLowerCase().replace(" ", ""), newMat); + imported++; + this.getConsole().info("Imported modded material '%s' -> %s", nsKey, newMat.getMaterial().name()); + + // Automatically create an alias using just the key part (after the colon) + // e.g., "cobblemon:mago_berry" -> alias "mago_berry" + // Alias will be saved later in batch + try { + int colonIdx = nsKey.indexOf(':'); + if (colonIdx > 0 && colonIdx < nsKey.length() - 1) { + String keyPart = nsKey.substring(colonIdx + 1); + this.addAlias(keyPart, nsKey); + } + } catch (Exception e) { + this.getConsole().warn("Failed to create alias for '%s': %s", nsKey, e.getMessage()); + } + } else { + // Rollback config if load failed + this.config.set(nsKey, null); + this.getConsole().warn("Failed to import modded material '%s'", nsKey); + } + } catch (Throwable ignored) { + } + } + + if (imported > 0) { + // Persist new entries + this.saveItems(); + this.getConsole().info("Imported %d new modded materials into %s", imported, this.itemFile); + reloaded += imported; + + // Save all created aliases at once + this.saveAliases(); + this.getConsole().info("Saved aliases for imported modded materials"); + } + } catch (Exception e) { + this.getConsole().warn("Failed to reload modded materials: %s", e.getMessage()); + } + return reloaded; + } } diff --git a/src/main/java/org/divinitycraft/divinityeconomy/market/items/materials/block/BlockManager.java b/src/main/java/org/divinitycraft/divinityeconomy/market/items/materials/block/BlockManager.java index 5984ad8..b035143 100644 --- a/src/main/java/org/divinitycraft/divinityeconomy/market/items/materials/block/BlockManager.java +++ b/src/main/java/org/divinitycraft/divinityeconomy/market/items/materials/block/BlockManager.java @@ -9,7 +9,6 @@ import org.divinitycraft.divinityeconomy.market.items.materials.MaterialValueResponse; import net.milkbowl.vault.economy.EconomyResponse.ResponseType; import org.bukkit.Material; -import org.bukkit.NamespacedKey; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.Damageable; @@ -90,11 +89,9 @@ public void init() { @Override public Set getLocalKeys() { return Arrays.stream(Material.values()) - .map(Material::getKey) - .map(NamespacedKey::getKey) - .map(Object::toString) - .map(String::toUpperCase) - .collect(Collectors.toSet()); + .map(Material::name) + .map(String::toUpperCase) + .collect(Collectors.toSet()); } diff --git a/src/main/java/org/divinitycraft/divinityeconomy/market/items/materials/block/MarketableBlock.java b/src/main/java/org/divinitycraft/divinityeconomy/market/items/materials/block/MarketableBlock.java index 3cd4353..2b9bad7 100644 --- a/src/main/java/org/divinitycraft/divinityeconomy/market/items/materials/block/MarketableBlock.java +++ b/src/main/java/org/divinitycraft/divinityeconomy/market/items/materials/block/MarketableBlock.java @@ -32,12 +32,20 @@ public MarketableBlock(DEPlugin main, MaterialManager itemManager, String ID, Co */ @Override public boolean check() { - return this.material != null; + // Allow market entries for modded/non-vanilla items (material may be null) + return true; } @Override public ItemStack getItemStack(int amount) { - return new ItemStack(this.getMaterial(), amount); + if (this.getMaterial() != null) { + return new ItemStack(this.getMaterial(), amount); + } + + // Material couldn't be resolved (modded item). Return a placeholder stack. + // Admins can override this in materials.yml to point to a valid material + // if they want buy/sell functionality. + return new ItemStack(org.bukkit.Material.STONE, amount); } /** @@ -67,6 +75,7 @@ public boolean equals(ItemStack itemStack) { if (itemMeta instanceof PotionMeta) { return false; } else { + if (this.getMaterial() == null) return false; return itemStack.getType().equals(this.getMaterial()); } } diff --git a/src/resources/config.yml b/src/resources/config.yml index 30d3fcf..a16ccd8 100644 --- a/src/resources/config.yml +++ b/src/resources/config.yml @@ -98,7 +98,7 @@ # Main Settings Main: # This is the plugin config version - Version: 3.5.7 + Version: 3.6.Kodak # Should placeholder api support be enabled? ( use "true" / "false" ) Enable PAPI: true # What language file should be used? ( use any string ) | default = en_GB.yml diff --git a/src/resources/plugin.yml b/src/resources/plugin.yml index 20e9ceb..b753398 100644 --- a/src/resources/plugin.yml +++ b/src/resources/plugin.yml @@ -1,7 +1,7 @@ name: DivinityEconomy prefix: DivinityEconomy main: org.divinitycraft.divinityeconomy.DEPlugin -version: 3.5.7 +version: 3.6.Kodak author: EDGRRRR description: Divinity Economy - The dynamic global economy plugin. api-version: 1.20 @@ -208,6 +208,13 @@ commands: permission-message: "You do not have the permission to use this command" usage: "/reload materials, /reload enchants, /reload potions, /reload entities, /reload experience, /reload config" + modded: + description: "Attempts to (re)load modded materials registered after startup." + aliases: [] + permission: de.admin.modded + permission-message: "You do not have the permission to use this command" + usage: "/modded" + save: description: "Saves the respective market file." aliases: []