From b6c6b48a572321ff3aefcd763a13fcd45943f8e1 Mon Sep 17 00:00:00 2001 From: XxFran10xX <318299142+XxFran10xX@users.noreply.github.com> Date: Sat, 17 Oct 2026 12:44:59 +0200 Subject: [PATCH 1/3] chore: compact gem stat config with shared bands Each gem now sets its stat once and its range per rarity as one "min-max" line under bands:, either naming a shared stat-bands set or listing its own. The 40 bundled gems use five shared sets (tier-1 to tier-5) and eight inline lists; config.yml drops from 998 to 462 lines with every stat unchanged. The old rarities: blocks still load, so existing server configs need no rewrite. A gem with neither bands nor rarities now logs a warning instead of breaking the whole config load. Co-Authored-By: Claude Opus 5.5 --- .../tfminecraft/geminfusion/ConfigLoader.java | 11 +- .../net/tfminecraft/geminfusion/GemStat.java | 64 +- src/main/resources/config.yml | 824 +++--------------- .../geminfusion/GemStatBandsTest.java | 168 ++++ 4 files changed, 378 insertions(+), 689 deletions(-) create mode 100644 src/test/java/net/tfminecraft/geminfusion/GemStatBandsTest.java diff --git a/src/main/java/net/tfminecraft/geminfusion/ConfigLoader.java b/src/main/java/net/tfminecraft/geminfusion/ConfigLoader.java index d6f8c52..5b54d95 100644 --- a/src/main/java/net/tfminecraft/geminfusion/ConfigLoader.java +++ b/src/main/java/net/tfminecraft/geminfusion/ConfigLoader.java @@ -65,14 +65,9 @@ public Gemstone getGemFromConfig(FileConfiguration config, String key) { gem.setSocketColour(config.getConfigurationSection("gems."+key).getString("socket_colour")); gem.setSocketNameColour(ChatColor.valueOf(config.getConfigurationSection("gems."+key).getString("socket_name_colour").toUpperCase())); gem.setMMOItem(config.getConfigurationSection("gems."+key).getString("gem")); - Set set = config.getConfigurationSection("gems."+key+".rarities").getKeys(false); - - List list = new ArrayList(set); - for(String id : list) { - GemStat stat = GemStat.parse(key, id, config.getConfigurationSection("gems."+key+".rarities."+id)); - if (stat != null) { - gem.addStat(stat); - } + for (GemStat stat : GemStat.parseGem(key, config.getConfigurationSection("gems."+key), + config.getConfigurationSection("stat-bands"))) { + gem.addStat(stat); } return gem; } diff --git a/src/main/java/net/tfminecraft/geminfusion/GemStat.java b/src/main/java/net/tfminecraft/geminfusion/GemStat.java index 8c8eedc..a22ebc9 100644 --- a/src/main/java/net/tfminecraft/geminfusion/GemStat.java +++ b/src/main/java/net/tfminecraft/geminfusion/GemStat.java @@ -1,11 +1,17 @@ package net.tfminecraft.geminfusion; +import java.util.ArrayList; import java.util.List; import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.bukkit.configuration.ConfigurationSection; public class GemStat { + /** {@code min-max}, or one number for a fixed stat. */ + private static final Pattern RANGE = Pattern.compile("(\\d+(?:\\.\\d+)?)(?:\\s*-\\s*(\\d+(?:\\.\\d+)?))?"); + private final String id; private final String statId; private final double min; @@ -35,7 +41,63 @@ public double getMax() { } /** - * New shape: {@code stat} / {@code min} / {@code max}. + * Reads a gem's stat for each rarity. Compact shape: one {@code stat} for the gem and {@code bands}, either the + * name of a set under the top-level {@code stat-bands} or a section of {@code : min-max}. Older configs + * list {@code rarities.} blocks instead (see {@link #parse}), which still load. + */ + public static List parseGem(String gemId, ConfigurationSection gem, ConfigurationSection statBands) { + List stats = new ArrayList<>(); + if (gem.contains("bands")) { + if (gem.contains("rarities")) { + warn("Gem '" + gemId + "' has both bands and rarities; using bands."); + } + String statId = gem.getString("stat"); + if (statId == null || statId.isBlank()) { + warn("Gem '" + gemId + "' has bands but no stat, skipping its stats."); + return stats; + } + ConfigurationSection bands = gem.isConfigurationSection("bands") ? gem.getConfigurationSection("bands") + : statBands == null ? null : statBands.getConfigurationSection(gem.getString("bands")); + if (bands == null) { + warn("Gem '" + gemId + "' uses unknown stat-bands set '" + gem.getString("bands") + + "', skipping its stats."); + return stats; + } + for (String rarityId : bands.getKeys(false)) { + GemStat stat = parseRange(gemId, rarityId, statId, bands.getString(rarityId)); + if (stat != null) { + stats.add(stat); + } + } + return stats; + } + ConfigurationSection rarities = gem.getConfigurationSection("rarities"); + if (rarities == null) { + warn("Gem '" + gemId + "' has neither bands nor rarities, skipping its stats."); + return stats; + } + for (String rarityId : rarities.getKeys(false)) { + GemStat stat = parse(gemId, rarityId, rarities.getConfigurationSection(rarityId)); + if (stat != null) { + stats.add(stat); + } + } + return stats; + } + + private static GemStat parseRange(String gemId, String rarityId, String statId, String text) { + Matcher range = RANGE.matcher(String.valueOf(text).trim()); + if (!range.matches()) { + warn("Gem '" + gemId + "' " + rarityId + " band '" + text + "' is not min-max, skipping."); + return null; + } + double min = Double.parseDouble(range.group(1)); + double max = range.group(2) == null ? min : Double.parseDouble(range.group(2)); + return new GemStat(rarityId, statId, min, max); + } + + /** + * Old per-rarity shape: {@code stat} / {@code min} / {@code max}. * Legacy: first entry of {@code stats: - type(min-max)}. * Returns null when the block cannot be used. */ diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index ff85a28..b0a3939 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -72,6 +72,38 @@ rarities: name: "ยง6Legendary" chance: 3 announce: true + +# Stat ranges for each rarity, shared by gems that roll the same numbers. Each range is min-max, +# before the d20 spread. +stat-bands: + tier-1: + common: 1.00-1.20 + rare: 1.30-1.50 + epic: 1.60-1.80 + legendary: 1.90-2.00 + tier-2: + common: 2.00-2.20 + rare: 2.30-2.50 + epic: 2.60-2.80 + legendary: 2.90-3.00 + tier-3: + common: 3.00-3.20 + rare: 3.30-3.50 + epic: 3.60-3.80 + legendary: 3.90-4.00 + tier-4: + common: 4.00-4.20 + rare: 4.30-4.50 + epic: 4.60-4.80 + legendary: 4.90-5.00 + tier-5: + common: 5.00-5.20 + rare: 5.30-5.50 + epic: 5.60-5.80 + legendary: 5.90-6.00 + +# Each gem gives one stat. bands names a stat-bands set, or lists the gem's own min-max for each rarity. +# Older configs that list stat, min and max under rarities: still load. gems: agate: gem: gemstones.agate @@ -81,917 +113,349 @@ gems: colour: white socket_colour: Basic Gemstone socket_name_colour: white - rarities: - common: - stat: max_health - min: 0.20 - max: 0.30 - rare: - stat: max_health - min: 0.30 - max: 0.40 - epic: - stat: max_health - min: 0.40 - max: 0.50 - legendary: - stat: max_health - min: 0.50 - max: 0.60 + stat: max_health + bands: + common: 0.20-0.30 + rare: 0.30-0.40 + epic: 0.40-0.50 + legendary: 0.50-0.60 jasper: gem: gemstones.jasper name: "Jasper" colour: white socket_colour: Basic Gemstone socket_name_colour: white - rarities: - common: - stat: armor - min: 0.05 - max: 0.10 - rare: - stat: armor - min: 0.10 - max: 0.15 - epic: - stat: armor - min: 0.15 - max: 0.20 - legendary: - stat: armor - min: 0.20 - max: 0.25 + stat: armor + bands: + common: 0.05-0.10 + rare: 0.10-0.15 + epic: 0.15-0.20 + legendary: 0.20-0.25 onyx: gem: gemstones.onyx name: "Onyx" colour: white socket_colour: Basic Gemstone socket_name_colour: white - rarities: - common: - stat: physical_damage_reduction - min: 1.00 - max: 1.20 - rare: - stat: physical_damage_reduction - min: 1.30 - max: 1.50 - epic: - stat: physical_damage_reduction - min: 1.60 - max: 1.80 - legendary: - stat: physical_damage_reduction - min: 1.90 - max: 2.00 + stat: physical_damage_reduction + bands: tier-1 tourmaline: gem: gemstones.tourmaline name: "Tourmaline" colour: white socket_colour: Basic Gemstone socket_name_colour: white - rarities: - common: - stat: projectile_damage_reduction - min: 1.00 - max: 1.20 - rare: - stat: projectile_damage_reduction - min: 1.30 - max: 1.50 - epic: - stat: projectile_damage_reduction - min: 1.60 - max: 1.80 - legendary: - stat: projectile_damage_reduction - min: 1.90 - max: 2.00 + stat: projectile_damage_reduction + bands: tier-1 pearl: gem: gemstones.pearl name: "Pearl" colour: white socket_colour: Basic Gemstone socket_name_colour: white - rarities: - common: - stat: magic_damage_reduction - min: 1.00 - max: 1.20 - rare: - stat: magic_damage_reduction - min: 1.30 - max: 1.50 - epic: - stat: magic_damage_reduction - min: 1.60 - max: 1.80 - legendary: - stat: magic_damage_reduction - min: 1.90 - max: 2.00 + stat: magic_damage_reduction + bands: tier-1 coral: gem: gemstones.coral name: "Coral" colour: white socket_colour: Basic Gemstone socket_name_colour: white - rarities: - common: - stat: physical_damage - min: 2.00 - max: 2.20 - rare: - stat: physical_damage - min: 2.30 - max: 2.50 - epic: - stat: physical_damage - min: 2.60 - max: 2.80 - legendary: - stat: physical_damage - min: 2.90 - max: 3.00 + stat: physical_damage + bands: tier-2 chrysoprase: gem: gemstones.chrysoprase name: "Chrysoprase" colour: white socket_colour: Basic Gemstone socket_name_colour: white - rarities: - common: - stat: projectile_damage - min: 2.00 - max: 2.20 - rare: - stat: projectile_damage - min: 2.30 - max: 2.50 - epic: - stat: projectile_damage - min: 2.60 - max: 2.80 - legendary: - stat: projectile_damage - min: 2.90 - max: 3.00 + stat: projectile_damage + bands: tier-2 larimar: gem: gemstones.larimar name: "Larimar" colour: white socket_colour: Basic Gemstone socket_name_colour: white - rarities: - common: - stat: magic_damage - min: 2.00 - max: 2.20 - rare: - stat: magic_damage - min: 2.30 - max: 2.50 - epic: - stat: magic_damage - min: 2.60 - max: 2.80 - legendary: - stat: magic_damage - min: 2.90 - max: 3.00 + stat: magic_damage + bands: tier-2 rhodonite: gem: gemstones.rhodonite name: "Rhodonite" colour: white socket_colour: Basic Gemstone socket_name_colour: white - rarities: - common: - stat: spell_vampirism - min: 2.00 - max: 2.20 - rare: - stat: spell_vampirism - min: 2.30 - max: 2.50 - epic: - stat: spell_vampirism - min: 2.60 - max: 2.80 - legendary: - stat: spell_vampirism - min: 2.90 - max: 3.00 + stat: spell_vampirism + bands: tier-2 vesuvianite: gem: gemstones.vesuvianite name: "Vesuvianite" colour: white socket_colour: Basic Gemstone socket_name_colour: white - rarities: - common: - stat: lifesteal - min: 2.00 - max: 2.20 - rare: - stat: lifesteal - min: 2.30 - max: 2.50 - epic: - stat: lifesteal - min: 2.60 - max: 2.80 - legendary: - stat: lifesteal - min: 2.90 - max: 3.00 + stat: lifesteal + bands: tier-2 turquoise: gem: gemstones.turquoise name: "Turquoise" colour: white socket_colour: Polished Gemstone socket_name_colour: white - rarities: - common: - stat: max_health - min: 0.60 - max: 0.70 - rare: - stat: max_health - min: 0.70 - max: 0.80 - epic: - stat: max_health - min: 0.80 - max: 0.90 - legendary: - stat: max_health - min: 0.90 - max: 1.00 + stat: max_health + bands: + common: 0.60-0.70 + rare: 0.70-0.80 + epic: 0.80-0.90 + legendary: 0.90-1.00 peridot: gem: gemstones.peridot name: "Peridot" colour: white socket_colour: Polished Gemstone socket_name_colour: white - rarities: - common: - stat: armor - min: 0.25 - max: 0.30 - rare: - stat: armor - min: 0.30 - max: 0.35 - epic: - stat: armor - min: 0.35 - max: 0.40 - legendary: - stat: armor - min: 0.40 - max: 0.45 + stat: armor + bands: + common: 0.25-0.30 + rare: 0.30-0.35 + epic: 0.35-0.40 + legendary: 0.40-0.45 malachite: gem: gemstones.malachite name: "Malachite" colour: white socket_colour: Polished Gemstone socket_name_colour: white - rarities: - common: - stat: physical_damage_reduction - min: 2.00 - max: 2.20 - rare: - stat: physical_damage_reduction - min: 2.30 - max: 2.50 - epic: - stat: physical_damage_reduction - min: 2.60 - max: 2.80 - legendary: - stat: physical_damage_reduction - min: 2.90 - max: 3.00 + stat: physical_damage_reduction + bands: tier-2 zircon: gem: gemstones.zircon name: "Zircon" colour: white socket_colour: Polished Gemstone socket_name_colour: white - rarities: - common: - stat: projectile_damage_reduction - min: 2.00 - max: 2.20 - rare: - stat: projectile_damage_reduction - min: 2.30 - max: 2.50 - epic: - stat: projectile_damage_reduction - min: 2.60 - max: 2.80 - legendary: - stat: projectile_damage_reduction - min: 2.90 - max: 3.00 + stat: projectile_damage_reduction + bands: tier-2 apatite: gem: gemstones.apatite name: "Apatite" colour: white socket_colour: Polished Gemstone socket_name_colour: white - rarities: - common: - stat: magic_damage_reduction - min: 2.00 - max: 2.20 - rare: - stat: magic_damage_reduction - min: 2.30 - max: 2.50 - epic: - stat: magic_damage_reduction - min: 2.60 - max: 2.80 - legendary: - stat: magic_damage_reduction - min: 2.90 - max: 3.00 + stat: magic_damage_reduction + bands: tier-2 carnelian: gem: gemstones.carnelian name: "Carnelian" colour: white socket_colour: Polished Gemstone socket_name_colour: white - rarities: - common: - stat: physical_damage - min: 3.00 - max: 3.20 - rare: - stat: physical_damage - min: 3.30 - max: 3.50 - epic: - stat: physical_damage - min: 3.60 - max: 3.80 - legendary: - stat: physical_damage - min: 3.90 - max: 4.00 + stat: physical_damage + bands: tier-3 labradorite: gem: gemstones.labradorite name: "Labradorite" colour: white socket_colour: Polished Gemstone socket_name_colour: white - rarities: - common: - stat: projectile_damage - min: 3.00 - max: 3.20 - rare: - stat: projectile_damage - min: 3.30 - max: 3.50 - epic: - stat: projectile_damage - min: 3.60 - max: 3.80 - legendary: - stat: projectile_damage - min: 3.90 - max: 4.00 + stat: projectile_damage + bands: tier-3 sardonyx: gem: gemstones.sardonyx name: "Sardonyx" colour: white socket_colour: Polished Gemstone socket_name_colour: white - rarities: - common: - stat: magic_damage - min: 3.00 - max: 3.20 - rare: - stat: magic_damage - min: 3.30 - max: 3.50 - epic: - stat: magic_damage - min: 3.60 - max: 3.80 - legendary: - stat: magic_damage - min: 3.90 - max: 4.00 + stat: magic_damage + bands: tier-3 variscite: gem: gemstones.variscite name: "Variscite" colour: white socket_colour: Polished Gemstone socket_name_colour: white - rarities: - common: - stat: spell_vampirism - min: 3.00 - max: 3.20 - rare: - stat: spell_vampirism - min: 3.30 - max: 3.50 - epic: - stat: spell_vampirism - min: 3.60 - max: 3.80 - legendary: - stat: spell_vampirism - min: 3.90 - max: 4.00 + stat: spell_vampirism + bands: tier-3 wulfenite: gem: gemstones.wulfenite name: "Wulfenite" colour: white socket_colour: Polished Gemstone socket_name_colour: white - rarities: - common: - stat: lifesteal - min: 3.00 - max: 3.20 - rare: - stat: lifesteal - min: 3.30 - max: 3.50 - epic: - stat: lifesteal - min: 3.60 - max: 3.80 - legendary: - stat: lifesteal - min: 3.90 - max: 4.00 + stat: lifesteal + bands: tier-3 aquamarine: gem: gemstones.aquamarine name: "Aquamarine" colour: white socket_colour: Radiant Gemstone socket_name_colour: white - rarities: - common: - stat: max_health - min: 1.00 - max: 1.10 - rare: - stat: max_health - min: 1.10 - max: 1.20 - epic: - stat: max_health - min: 1.20 - max: 1.30 - legendary: - stat: max_health - min: 1.40 - max: 1.50 + stat: max_health + bands: + common: 1.00-1.10 + rare: 1.10-1.20 + epic: 1.20-1.30 + legendary: 1.40-1.50 garnet: gem: gemstones.garnet name: "Garnet" colour: white socket_colour: Radiant Gemstone socket_name_colour: white - rarities: - common: - stat: armor - min: 0.45 - max: 0.50 - rare: - stat: armor - min: 0.50 - max: 0.55 - epic: - stat: armor - min: 0.55 - max: 0.60 - legendary: - stat: armor - min: 0.60 - max: 0.65 + stat: armor + bands: + common: 0.45-0.50 + rare: 0.50-0.55 + epic: 0.55-0.60 + legendary: 0.60-0.65 opal: gem: gemstones.opal name: "Opal" colour: white socket_colour: Radiant Gemstone socket_name_colour: white - rarities: - common: - stat: physical_damage_reduction - min: 3.00 - max: 3.20 - rare: - stat: physical_damage_reduction - min: 3.30 - max: 3.50 - epic: - stat: physical_damage_reduction - min: 3.60 - max: 3.80 - legendary: - stat: physical_damage_reduction - min: 3.90 - max: 4.00 + stat: physical_damage_reduction + bands: tier-3 tanzanite: gem: gemstones.tanzanite name: "Tanzanite" colour: white socket_colour: Radiant Gemstone socket_name_colour: white - rarities: - common: - stat: projectile_damage_reduction - min: 3.00 - max: 3.20 - rare: - stat: projectile_damage_reduction - min: 3.30 - max: 3.50 - epic: - stat: projectile_damage_reduction - min: 3.60 - max: 3.80 - legendary: - stat: projectile_damage_reduction - min: 3.90 - max: 4.00 + stat: projectile_damage_reduction + bands: tier-3 moonstone: gem: gemstones.moonstone name: "Moonstone" colour: white socket_colour: Radiant Gemstone socket_name_colour: white - rarities: - common: - stat: magic_damage_reduction - min: 3.00 - max: 3.20 - rare: - stat: magic_damage_reduction - min: 3.30 - max: 3.50 - epic: - stat: magic_damage_reduction - min: 3.60 - max: 3.80 - legendary: - stat: magic_damage_reduction - min: 3.90 - max: 4.00 + stat: magic_damage_reduction + bands: tier-3 sunstone: gem: gemstones.sunstone name: "Sunstone" colour: white socket_colour: Radiant Gemstone socket_name_colour: white - rarities: - common: - stat: physical_damage - min: 4.00 - max: 4.20 - rare: - stat: physical_damage - min: 4.30 - max: 4.50 - epic: - stat: physical_damage - min: 4.60 - max: 4.80 - legendary: - stat: physical_damage - min: 4.90 - max: 5.00 + stat: physical_damage + bands: tier-4 spinel: gem: gemstones.spinel name: "Spinel" colour: white socket_colour: Radiant Gemstone socket_name_colour: white - rarities: - common: - stat: projectile_damage - min: 4.00 - max: 4.20 - rare: - stat: projectile_damage - min: 4.30 - max: 4.50 - epic: - stat: projectile_damage - min: 4.60 - max: 4.80 - legendary: - stat: projectile_damage - min: 4.90 - max: 5.00 + stat: projectile_damage + bands: tier-4 alexandrite: gem: gemstones.alexandrite name: "Alexandrite" colour: white socket_colour: Radiant Gemstone socket_name_colour: white - rarities: - common: - stat: magic_damage - min: 4.00 - max: 4.20 - rare: - stat: magic_damage - min: 4.30 - max: 4.50 - epic: - stat: magic_damage - min: 4.60 - max: 4.80 - legendary: - stat: magic_damage - min: 4.90 - max: 5.00 + stat: magic_damage + bands: tier-4 firestone: gem: gemstones.firestone name: "Direstone" colour: white socket_colour: Radiant Gemstone socket_name_colour: white - rarities: - common: - stat: spell_vampirism - min: 4.00 - max: 4.20 - rare: - stat: spell_vampirism - min: 4.30 - max: 4.50 - epic: - stat: spell_vampirism - min: 4.60 - max: 4.80 - legendary: - stat: spell_vampirism - min: 4.90 - max: 5.00 + stat: spell_vampirism + bands: tier-4 cloudstone: gem: gemstones.cloudstone name: "Cloudstone" colour: white socket_colour: Radiant Gemstone socket_name_colour: white - rarities: - common: - stat: lifesteal - min: 4.00 - max: 4.20 - rare: - stat: lifesteal - min: 4.30 - max: 4.50 - epic: - stat: lifesteal - min: 4.60 - max: 4.80 - legendary: - stat: lifesteal - min: 4.90 - max: 5.00 + stat: lifesteal + bands: tier-4 ruby: gem: gemstones.ruby name: "Ruby" colour: white socket_colour: Mythical Gemstone socket_name_colour: white - rarities: - common: - stat: max_health - min: 1.50 - max: 1.60 - rare: - stat: max_health - min: 1.60 - max: 1.70 - epic: - stat: max_health - min: 1.70 - max: 1.80 - legendary: - stat: max_health - min: 1.80 - max: 2.00 + stat: max_health + bands: + common: 1.50-1.60 + rare: 1.60-1.70 + epic: 1.70-1.80 + legendary: 1.80-2.00 sapphire: gem: gemstones.sapphire name: "Sapphire" colour: white socket_colour: Mythical Gemstone socket_name_colour: white - rarities: - common: - stat: armor - min: 0.65 - max: 0.70 - rare: - stat: armor - min: 0.70 - max: 0.75 - epic: - stat: armor - min: 0.75 - max: 0.80 - legendary: - stat: armor - min: 0.80 - max: 1.00 + stat: armor + bands: + common: 0.65-0.70 + rare: 0.70-0.75 + epic: 0.75-0.80 + legendary: 0.80-1.00 topaz: gem: gemstones.topaz name: "Topaz" colour: white socket_colour: Mythical Gemstone socket_name_colour: white - rarities: - common: - stat: physical_damage_reduction - min: 4.00 - max: 4.20 - rare: - stat: physical_damage_reduction - min: 4.30 - max: 4.50 - epic: - stat: physical_damage_reduction - min: 4.60 - max: 4.80 - legendary: - stat: physical_damage_reduction - min: 4.90 - max: 5.00 + stat: physical_damage_reduction + bands: tier-4 citrine: gem: gemstones.citrine name: "Citrine" colour: white socket_colour: Mythical Gemstone socket_name_colour: white - rarities: - common: - stat: projectile_damage_reduction - min: 4.00 - max: 4.20 - rare: - stat: projectile_damage_reduction - min: 4.30 - max: 4.50 - epic: - stat: projectile_damage_reduction - min: 4.60 - max: 4.80 - legendary: - stat: projectile_damage_reduction - min: 4.90 - max: 5.00 + stat: projectile_damage_reduction + bands: tier-4 morganite: gem: gemstones.morganite name: "Morganite" colour: white socket_colour: Mythical Gemstone socket_name_colour: white - rarities: - common: - stat: magic_damage_reduction - min: 4.00 - max: 4.2 - rare: - stat: magic_damage_reduction - min: 4.30 - max: 4.50 - epic: - stat: magic_damage_reduction - min: 4.60 - max: 4.80 - legendary: - stat: magic_damage_reduction - min: 4.90 - max: 5.00 + stat: magic_damage_reduction + bands: tier-4 crystallite: gem: gemstones.crystallite name: "Crystallite" colour: white socket_colour: Mythical Gemstone socket_name_colour: white - rarities: - common: - stat: physical_damage - min: 5.00 - max: 5.20 - rare: - stat: physical_damage - min: 5.30 - max: 5.50 - epic: - stat: physical_damage - min: 5.60 - max: 5.80 - legendary: - stat: physical_damage - min: 5.90 - max: 6.00 + stat: physical_damage + bands: tier-5 tigers_eye: gem: gemstones.tigers_eye name: "Tiger's Eye" colour: white socket_colour: Mythical Gemstone socket_name_colour: white - rarities: - common: - stat: projectile_damage - min: 5.00 - max: 5.20 - rare: - stat: projectile_damage - min: 5.30 - max: 5.50 - epic: - stat: projectile_damage - min: 5.60 - max: 5.80 - legendary: - stat: projectile_damage - min: 5.90 - max: 6.00 + stat: projectile_damage + bands: tier-5 serpents_eye: gem: gemstones.serpents_eye name: "Serpent's Eye" colour: white socket_colour: Mythical Gemstone socket_name_colour: white - rarities: - common: - stat: magic_damage - min: 5.00 - max: 5.20 - rare: - stat: magic_damage - min: 5.30 - max: 5.50 - epic: - stat: magic_damage - min: 5.60 - max: 5.80 - legendary: - stat: magic_damage - min: 5.90 - max: 6.00 + stat: magic_damage + bands: tier-5 musgravite: gem: gemstones.musgravite name: "Musgravite" colour: white socket_colour: Mythical Gemstone socket_name_colour: white - rarities: - common: - stat: spell_vampirism - min: 5.00 - max: 5.20 - rare: - stat: spell_vampirism - min: 5.30 - max: 5.50 - epic: - stat: spell_vampirism - min: 5.60 - max: 5.80 - legendary: - stat: spell_vampirism - min: 5.90 - max: 6.00 + stat: spell_vampirism + bands: tier-5 taaffeite: gem: gemstones.taaffeite name: "Taaffeite" colour: white socket_colour: Mythical Gemstone socket_name_colour: white - rarities: - common: - stat: lifesteal - min: 5.00 - max: 5.20 - rare: - stat: lifesteal - min: 5.30 - max: 5.50 - epic: - stat: lifesteal - min: 5.60 - max: 5.80 - legendary: - stat: lifesteal - min: 5.90 - max: 6.00 + stat: lifesteal + bands: tier-5 diff --git a/src/test/java/net/tfminecraft/geminfusion/GemStatBandsTest.java b/src/test/java/net/tfminecraft/geminfusion/GemStatBandsTest.java new file mode 100644 index 0000000..d2c0d6d --- /dev/null +++ b/src/test/java/net/tfminecraft/geminfusion/GemStatBandsTest.java @@ -0,0 +1,168 @@ +package net.tfminecraft.geminfusion; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.io.File; +import java.util.*; +import java.util.logging.Logger; +import org.bukkit.configuration.file.YamlConfiguration; +import org.junit.jupiter.api.*; + +class GemStatBandsTest { + InfusionMain previous; + Logger logger; + + @BeforeEach + void setup() { + previous = InfusionMain.plugin; + InfusionMain.plugin = mock(InfusionMain.class); + logger = mock(Logger.class); + when(InfusionMain.plugin.getLogger()).thenReturn(logger); + } + + @AfterEach + void cleanup() { + InfusionMain.plugin = previous; + ConfigLoader.loadedGems.clear(); + ConfigLoader.loadedRarities.clear(); + GemNameCutoffs.load(List.of(), List.of()); + } + + static YamlConfiguration config(String text) throws Exception { + YamlConfiguration c = new YamlConfiguration(); + c.loadFromString(text); + return c; + } + + static List describe(List stats) { + return stats.stream() + .map(s -> s.getId() + " " + s.getStatId() + " " + s.getMin() + "-" + s.getMax()) + .toList(); + } + + static final String BANDS = + "stat-bands:\n" + + " tier-1:\n" + + " common: 1.0-1.2\n" + + " legendary: 1.9 - 2\n"; + + @Test + void gemUsesANamedSetOrItsOwnBands() throws Exception { + var c = + config( + BANDS + + "gems:\n" + + " onyx:\n" + + " stat: armor\n" + + " bands: tier-1\n" + + " agate:\n" + + " stat: max_health\n" + + " bands:\n" + + " common: 0.20-0.30\n" + + " rare: 0.5\n"); + assertEquals( + List.of("common armor 1.0-1.2", "legendary armor 1.9-2.0"), + describe(GemStat.parseGem("onyx", c.getConfigurationSection("gems.onyx"), c.getConfigurationSection("stat-bands")))); + // A single number is a fixed stat. + assertEquals( + List.of("common max_health 0.2-0.3", "rare max_health 0.5-0.5"), + describe(GemStat.parseGem("agate", c.getConfigurationSection("gems.agate"), null))); + verify(logger, never()).warning(anyString()); + } + + @Test + void brokenBandsAreSkippedWithAWarning() throws Exception { + var c = + config( + BANDS + + "gems:\n" + + " unknown:\n" + + " stat: armor\n" + + " bands: tier-9\n" + + " nostat:\n" + + " bands: tier-1\n" + + " blank:\n" + + " stat: ' '\n" + + " bands: tier-1\n" + + " bad:\n" + + " stat: armor\n" + + " bands:\n" + + " common: lots\n" + + " rare: {min: 1}\n" + + " epic: 2-3\n" + + " both:\n" + + " stat: armor\n" + + " bands: tier-1\n" + + " rarities:\n" + + " common: {stat: armor, min: 9}\n" + + " none:\n" + + " name: None\n"); + var sets = c.getConfigurationSection("stat-bands"); + assertTrue(GemStat.parseGem("unknown", c.getConfigurationSection("gems.unknown"), sets).isEmpty()); + assertTrue(GemStat.parseGem("unknown", c.getConfigurationSection("gems.unknown"), null).isEmpty()); + verify(logger, times(2)).warning(contains("unknown stat-bands set 'tier-9'")); + assertTrue(GemStat.parseGem("nostat", c.getConfigurationSection("gems.nostat"), sets).isEmpty()); + assertTrue(GemStat.parseGem("blank", c.getConfigurationSection("gems.blank"), sets).isEmpty()); + verify(logger, times(2)).warning(contains("has bands but no stat")); + assertEquals( + List.of("epic armor 2.0-3.0"), + describe(GemStat.parseGem("bad", c.getConfigurationSection("gems.bad"), sets))); + verify(logger).warning(contains("common band 'lots' is not min-max")); + verify(logger).warning(contains("'bad' rare band")); + assertEquals( + List.of("common armor 1.0-1.2", "legendary armor 1.9-2.0"), + describe(GemStat.parseGem("both", c.getConfigurationSection("gems.both"), sets))); + verify(logger).warning(contains("has both bands and rarities")); + assertTrue(GemStat.parseGem("none", c.getConfigurationSection("gems.none"), sets).isEmpty()); + verify(logger).warning(contains("has neither bands nor rarities")); + } + + @Test + void olderRaritiesBlocksStillLoad() throws Exception { + var c = + config( + "gems:\n" + + " ruby:\n" + + " rarities:\n" + + " common: {stat: ATTACK_DAMAGE, min: 2, max: 4}\n" + + " rare: {stat: ATTACK_DAMAGE, min: 5}\n" + + " bad: {}\n"); + assertEquals( + List.of("common ATTACK_DAMAGE 2.0-4.0", "rare ATTACK_DAMAGE 5.0-5.0"), + describe(GemStat.parseGem("ruby", c.getConfigurationSection("gems.ruby"), null))); + } + + @Test + void bundledConfigGivesEveryGemFourBandsOfOneStat() { + new ConfigLoader() + .loadConfig(YamlConfiguration.loadConfiguration(new File("src/main/resources/config.yml"))); + assertEquals(40, ConfigLoader.loadedGems.size()); + for (Gemstone gem : ConfigLoader.loadedGems) { + assertEquals( + List.of("common", "rare", "epic", "legendary"), + gem.getStats().stream().map(GemStat::getId).toList(), + gem.getId()); + assertEquals(1, gem.getStats().stream().map(GemStat::getStatId).distinct().count(), gem.getId()); + } + Map byId = new HashMap<>(); + for (Gemstone gem : ConfigLoader.loadedGems) byId.put(gem.getId(), gem); + assertEquals( + List.of( + "common max_health 0.2-0.3", + "rare max_health 0.3-0.4", + "epic max_health 0.4-0.5", + "legendary max_health 0.5-0.6"), + describe(byId.get("agate").getStats())); + assertEquals( + List.of( + "common physical_damage 2.0-2.2", + "rare physical_damage 2.3-2.5", + "epic physical_damage 2.6-2.8", + "legendary physical_damage 2.9-3.0"), + describe(byId.get("coral").getStats())); + assertEquals( + "legendary armor 0.8-1.0", describe(byId.get("sapphire").getStats()).getLast()); + verify(logger, never()).warning(anyString()); + } +} From 9e3c14f566d3539f5f9d581cac92096de747986d Mon Sep 17 00:00:00 2001 From: XxFran10xX <318299142+XxFran10xX@users.noreply.github.com> Date: Sat, 17 Oct 2026 12:58:48 +0200 Subject: [PATCH 2/3] fix: read only the server's own gem bands, not the bundled defaults Bukkit's getConfig() falls back to the bundled config.yml for missing keys. On Dev, an older server config (rarities: blocks) therefore looked like it had bands, and getConfigurationSection built empty sections from the bundled ones, so every gem loaded with no stats. parseGem now only reads keys written in the server's file. Co-Authored-By: Claude Opus 5.5 --- .../tfminecraft/geminfusion/ConfigLoader.java | 2 +- .../net/tfminecraft/geminfusion/GemStat.java | 25 ++++++++++++------- .../geminfusion/GemStatBandsTest.java | 25 +++++++++++++++++++ 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/src/main/java/net/tfminecraft/geminfusion/ConfigLoader.java b/src/main/java/net/tfminecraft/geminfusion/ConfigLoader.java index 5b54d95..33c4881 100644 --- a/src/main/java/net/tfminecraft/geminfusion/ConfigLoader.java +++ b/src/main/java/net/tfminecraft/geminfusion/ConfigLoader.java @@ -66,7 +66,7 @@ public Gemstone getGemFromConfig(FileConfiguration config, String key) { gem.setSocketNameColour(ChatColor.valueOf(config.getConfigurationSection("gems."+key).getString("socket_name_colour").toUpperCase())); gem.setMMOItem(config.getConfigurationSection("gems."+key).getString("gem")); for (GemStat stat : GemStat.parseGem(key, config.getConfigurationSection("gems."+key), - config.getConfigurationSection("stat-bands"))) { + GemStat.ownSection(config, "stat-bands"))) { gem.addStat(stat); } return gem; diff --git a/src/main/java/net/tfminecraft/geminfusion/GemStat.java b/src/main/java/net/tfminecraft/geminfusion/GemStat.java index a22ebc9..637d6d3 100644 --- a/src/main/java/net/tfminecraft/geminfusion/GemStat.java +++ b/src/main/java/net/tfminecraft/geminfusion/GemStat.java @@ -44,34 +44,36 @@ public double getMax() { * Reads a gem's stat for each rarity. Compact shape: one {@code stat} for the gem and {@code bands}, either the * name of a set under the top-level {@code stat-bands} or a section of {@code : min-max}. Older configs * list {@code rarities.} blocks instead (see {@link #parse}), which still load. + * Only keys written in this config count: Bukkit falls back to the bundled config.yml for missing keys, which + * would hand an older server config the bundled bands (or empty sections made from them). */ public static List parseGem(String gemId, ConfigurationSection gem, ConfigurationSection statBands) { List stats = new ArrayList<>(); - if (gem.contains("bands")) { - if (gem.contains("rarities")) { + Object bandsValue = gem.get("bands", null); + if (bandsValue != null) { + if (gem.get("rarities", null) != null) { warn("Gem '" + gemId + "' has both bands and rarities; using bands."); } - String statId = gem.getString("stat"); + String statId = gem.getString("stat", null); if (statId == null || statId.isBlank()) { warn("Gem '" + gemId + "' has bands but no stat, skipping its stats."); return stats; } - ConfigurationSection bands = gem.isConfigurationSection("bands") ? gem.getConfigurationSection("bands") - : statBands == null ? null : statBands.getConfigurationSection(gem.getString("bands")); + ConfigurationSection bands = bandsValue instanceof ConfigurationSection own ? own + : ownSection(statBands, String.valueOf(bandsValue)); if (bands == null) { - warn("Gem '" + gemId + "' uses unknown stat-bands set '" + gem.getString("bands") - + "', skipping its stats."); + warn("Gem '" + gemId + "' uses unknown stat-bands set '" + bandsValue + "', skipping its stats."); return stats; } for (String rarityId : bands.getKeys(false)) { - GemStat stat = parseRange(gemId, rarityId, statId, bands.getString(rarityId)); + GemStat stat = parseRange(gemId, rarityId, statId, bands.getString(rarityId, null)); if (stat != null) { stats.add(stat); } } return stats; } - ConfigurationSection rarities = gem.getConfigurationSection("rarities"); + ConfigurationSection rarities = ownSection(gem, "rarities"); if (rarities == null) { warn("Gem '" + gemId + "' has neither bands nor rarities, skipping its stats."); return stats; @@ -85,6 +87,11 @@ public static List parseGem(String gemId, ConfigurationSection gem, Con return stats; } + /** The section written at this key of this config, ignoring the bundled defaults; null when there is none. */ + static ConfigurationSection ownSection(ConfigurationSection parent, String key) { + return parent != null && parent.get(key, null) instanceof ConfigurationSection own ? own : null; + } + private static GemStat parseRange(String gemId, String rarityId, String statId, String text) { Matcher range = RANGE.matcher(String.valueOf(text).trim()); if (!range.matches()) { diff --git a/src/test/java/net/tfminecraft/geminfusion/GemStatBandsTest.java b/src/test/java/net/tfminecraft/geminfusion/GemStatBandsTest.java index d2c0d6d..e233841 100644 --- a/src/test/java/net/tfminecraft/geminfusion/GemStatBandsTest.java +++ b/src/test/java/net/tfminecraft/geminfusion/GemStatBandsTest.java @@ -133,6 +133,31 @@ void olderRaritiesBlocksStillLoad() throws Exception { describe(GemStat.parseGem("ruby", c.getConfigurationSection("gems.ruby"), null))); } + @Test + void serverConfigIgnoresTheBundledBandsItFallsBackTo() throws Exception { + // Bukkit's getConfig() uses the bundled config.yml as defaults for every key the server's file lacks. + var bundled = YamlConfiguration.loadConfiguration(new File("src/main/resources/config.yml")); + var older = + config( + "gems:\n" + + " agate:\n" + + " rarities:\n" + + " common: {stat: max_health, min: 0.21, max: 0.31}\n" + + " coral:\n" + + " stat: physical_damage\n" + + " bands: tier-2\n"); + older.setDefaults(bundled); + var sets = GemStat.ownSection(older, "stat-bands"); + assertNull(sets); + assertEquals( + List.of("common max_health 0.21-0.31"), + describe(GemStat.parseGem("agate", older.getConfigurationSection("gems.agate"), sets))); + // A set the server's file doesn't define is unknown, even though the bundled file has it. + assertTrue(GemStat.parseGem("coral", older.getConfigurationSection("gems.coral"), sets).isEmpty()); + verify(logger).warning(contains("unknown stat-bands set 'tier-2'")); + verify(logger, never()).warning(contains("both bands and rarities")); + } + @Test void bundledConfigGivesEveryGemFourBandsOfOneStat() { new ConfigLoader() From 8bdb0846e45c689787c7c74890aeda5e5c630de1 Mon Sep 17 00:00:00 2001 From: XxFran10xX <318299142+XxFran10xX@users.noreply.github.com> Date: Sat, 17 Oct 2026 13:07:41 +0200 Subject: [PATCH 3/3] test: wrap a long bands test line Co-Authored-By: Claude Opus 5.5 --- .../java/net/tfminecraft/geminfusion/GemStatBandsTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/test/java/net/tfminecraft/geminfusion/GemStatBandsTest.java b/src/test/java/net/tfminecraft/geminfusion/GemStatBandsTest.java index e233841..69ccd2f 100644 --- a/src/test/java/net/tfminecraft/geminfusion/GemStatBandsTest.java +++ b/src/test/java/net/tfminecraft/geminfusion/GemStatBandsTest.java @@ -63,7 +63,9 @@ void gemUsesANamedSetOrItsOwnBands() throws Exception { + " rare: 0.5\n"); assertEquals( List.of("common armor 1.0-1.2", "legendary armor 1.9-2.0"), - describe(GemStat.parseGem("onyx", c.getConfigurationSection("gems.onyx"), c.getConfigurationSection("stat-bands")))); + describe( + GemStat.parseGem( + "onyx", c.getConfigurationSection("gems.onyx"), c.getConfigurationSection("stat-bands")))); // A single number is a fixed stat. assertEquals( List.of("common max_health 0.2-0.3", "rare max_health 0.5-0.5"),