diff --git a/Changelog.txt b/Changelog.txt index 71dc0bfc9..22763243d 100644 --- a/Changelog.txt +++ b/Changelog.txt @@ -1,8 +1,8 @@ Version 2.2.052 - Fixed Minecraft version detection failing on Spigot servers — the server version string format used by Spigot (git-Spigot-...) was not matched by the version regex, causing version to resolve as 0.0.0 and Tricky Trials potions to be incorrectly skipped - Fixed Woodcutting bonus drops (Harvest Lumber, Clean Cuts) and Excavation treasure drops bypassing BlockDropItemEvent, making them invisible to Telekinesis-style enchant plugins + Fixed Minecraft version detection failing on Spigot servers, causing Tricky Trials potions to be skipped + Fixed Woodcutting bonus drops (Harvest Lumber, Clean Cuts) and Excavation treasure drops bypassing BlockDropItemEvent, making them invisible to Telekinesis-style enchant plugins (See notes) Skill info tooltip colors are now fully customizable via locale strings — all colors in JSON hover components are driven by locale keys and support & codes, hex colors (&#RRGGBB), and [[COLOR_NAME]] tokens - Fixed incorrect effect durations for splash and lingering variants of Infestation, Weaving, Wind Charging, and Oozing potions in potions.yml (Thanks Warriorrrr) + Fixed incorrect effect durations for splash and lingering variants of Infestation, Weaving, Wind Charging, and Oozing potions in potions.yml (Thanks Warriorrrr) — existing servers are auto-migrated on first startup Fixed an exploit where arrows from multishot crossbows could be recovered in regions protected by WorldGuard Fixed an exploit where renamed seeds could trigger Green Thumb crop replanting without being consumed Fixed an exploit where renamed mushrooms could trigger Shroom Thumb without being consumed @@ -40,6 +40,7 @@ Version 2.2.052 (Codebase) Bumped api-version in plugin.yml from 1.13 to 1.20.5, eliminating legacy bytecode field-rename shims applied by the server at load time NOTES: + Regarding the telekenisis plugin compatibility fix, Tree Feller is not yet fixed, I will try to address it in the near future. In this update I've added two new advanced.yml settings for more granular control of the enchant level cap for both Repair and Salvage, this is an alternative to flipping on ExploitFix.UnsafeEnchantments in experience.yml which simply uncaps both skills. If ExploitFix.UnsafeEnchantments happens to be enabled in experience.yml, it will override both of these new settings and therefor allow basically uncapped enchant levels for Arcane Forging and Arcane Salvage. Allowing the old setting to override is mostly for backwards compatibility so we don't disrupt any existing server behavior. diff --git a/src/main/java/com/gmail/nossr50/config/skills/alchemy/PotionConfig.java b/src/main/java/com/gmail/nossr50/config/skills/alchemy/PotionConfig.java index 3a3083f1e..eb6ef0141 100644 --- a/src/main/java/com/gmail/nossr50/config/skills/alchemy/PotionConfig.java +++ b/src/main/java/com/gmail/nossr50/config/skills/alchemy/PotionConfig.java @@ -5,14 +5,18 @@ import static com.gmail.nossr50.util.PotionUtil.matchPotionType; import static com.gmail.nossr50.util.PotionUtil.setBasePotionType; import com.gmail.nossr50.config.LegacyConfigLoader; +import com.gmail.nossr50.datatypes.database.UpgradeType; import com.gmail.nossr50.datatypes.skills.alchemy.AlchemyPotion; import com.gmail.nossr50.mcMMO; import com.gmail.nossr50.util.ItemUtils; import java.io.File; +import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.format.TextDecoration; @@ -20,6 +24,7 @@ import org.bukkit.ChatColor; import org.bukkit.Color; import org.bukkit.Material; import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.inventory.meta.PotionMeta; @@ -39,6 +44,32 @@ public class PotionConfig extends LegacyConfigLoader { private static final String COBWEB_STR = "COBWEB"; private static final String STONE_STR = "STONE"; + // The four Tricky Trials splash potion YAML keys shipped with incorrect duration 2500 ticks; + // the correct value is 3600 (matching the base potion duration). + @VisibleForTesting + static final List TRICKY_TRIALS_SPLASH_POTION_KEYS = List.of( + "SPLASH_POTION_OF_INFESTATION", + "SPLASH_POTION_OF_WEAVING", + "SPLASH_POTION_OF_WIND_CHARGING", + "SPLASH_POTION_OF_OOZING"); + @VisibleForTesting + static final int TRICKY_TRIALS_SPLASH_INCORRECT_DURATION = 2500; + @VisibleForTesting + static final int TRICKY_TRIALS_SPLASH_CORRECT_DURATION = 3600; + + // The four Tricky Trials lingering potion YAML keys shipped with incorrect duration 3000 ticks; + // the correct value is 900 (1/4 of the base 3600 tick duration per vanilla rules). + @VisibleForTesting + static final List TRICKY_TRIALS_LINGERING_POTION_KEYS = List.of( + "LINGERING_POTION_OF_INFESTATION", + "LINGERING_POTION_OF_WEAVING", + "LINGERING_POTION_WIND_CHARGING", + "LINGERING_POTION_OF_OOZING"); + @VisibleForTesting + static final int TRICKY_TRIALS_LINGERING_INCORRECT_DURATION = 3000; + @VisibleForTesting + static final int TRICKY_TRIALS_LINGERING_CORRECT_DURATION = 900; + private final List concoctionsIngredientsTierOne = new ArrayList<>(); private final List concoctionsIngredientsTierTwo = new ArrayList<>(); private final List concoctionsIngredientsTierThree = new ArrayList<>(); @@ -80,10 +111,129 @@ public class PotionConfig extends LegacyConfigLoader { } public void loadPotions() { + if (mcMMO.getUpgradeManager().shouldUpgrade( + UpgradeType.FIX_TRICKY_TRIALS_SPLASH_POTION_DURATIONS)) { + mcMMO.p.getLogger().log(Level.INFO, + "Fixing incorrect potion durations for Tricky Trials potions," + + " this will only run once..."); + final boolean patched = fixTrickyTrialsPotionDurations(config, mcMMO.p.getLogger()); + if (patched) { + try { + config.save(getFile()); + mcMMO.getUpgradeManager().setUpgradeCompleted( + UpgradeType.FIX_TRICKY_TRIALS_SPLASH_POTION_DURATIONS); + } catch (IOException e) { + mcMMO.p.getLogger().log(Level.SEVERE, + "Failed to save potions.yml after patching Tricky Trials potion" + + " durations. You may manually fix the Effects durations:" + + " splash potions should be 3600, lingering potions should" + + " be 900, for INFESTATION, WEAVING, WIND_CHARGING, and" + + " OOZING.", e); + } + } else { + // Nothing to patch (keys absent or already correct) — mark complete so we + // don't check again on the next server startup. + mcMMO.getUpgradeManager().setUpgradeCompleted( + UpgradeType.FIX_TRICKY_TRIALS_SPLASH_POTION_DURATIONS); + } + } loadConcoctions(); loadPotionMap(); } + /** + * Patches the Tricky Trials potion Effects durations in the supplied config: + *
    + *
  • Splash potions: incorrect 2500 → correct 3600 ticks
  • + *
  • Lingering potions: incorrect 3000 → correct 900 ticks
  • + *
+ * Only modifies entries that exactly match the known-bad values — user-customized durations + * are preserved unchanged. Silently skips potion keys that are absent in the config; + * a customized potions.yml may legitimately omit any of them. + * + * @param configuration the loaded YAML configuration to patch in-memory + * @param logger logger used to report which potions were patched + * @return true if at least one entry was patched, false otherwise + */ + @VisibleForTesting + static boolean fixTrickyTrialsPotionDurations( + final YamlConfiguration configuration, + final Logger logger) { + final ConfigurationSection potionsSection = + configuration.getConfigurationSection("Potions"); + if (potionsSection == null) { + return false; + } + + boolean anyPatched = false; + anyPatched |= patchTrickyTrialsDurations( + potionsSection, + TRICKY_TRIALS_SPLASH_POTION_KEYS, + TRICKY_TRIALS_SPLASH_INCORRECT_DURATION, + TRICKY_TRIALS_SPLASH_CORRECT_DURATION, + logger); + anyPatched |= patchTrickyTrialsDurations( + potionsSection, + TRICKY_TRIALS_LINGERING_POTION_KEYS, + TRICKY_TRIALS_LINGERING_INCORRECT_DURATION, + TRICKY_TRIALS_LINGERING_CORRECT_DURATION, + logger); + return anyPatched; + } + + private static boolean patchTrickyTrialsDurations( + final ConfigurationSection potionsSection, + final List potionKeys, + final int incorrectDuration, + final int correctDuration, + final Logger logger) { + boolean anyPatched = false; + + for (final String potionKey : potionKeys) { + final ConfigurationSection potionSection = + potionsSection.getConfigurationSection(potionKey); + if (potionSection == null) { + // Not present — the user may have customized potions.yml to omit this entry. + continue; + } + + final List originalEffects = potionSection.getStringList("Effects"); + if (originalEffects.isEmpty()) { + continue; + } + + final List patchedEffects = new ArrayList<>(originalEffects.size()); + boolean potionPatched = false; + + for (final String effectEntry : originalEffects) { + final String[] parts = effectEntry.trim().split("\\s+"); + if (parts.length == 3 && isTrickyTrialsPotionEffect(parts[0])) { + try { + final int duration = Integer.parseInt(parts[2]); + if (duration == incorrectDuration) { + patchedEffects.add( + parts[0] + " " + parts[1] + " " + correctDuration); + potionPatched = true; + continue; + } + } catch (NumberFormatException ignored) { + // Malformed duration field — leave the entry unchanged. + } + } + patchedEffects.add(effectEntry); + } + + if (potionPatched) { + potionSection.set("Effects", patchedEffects); + anyPatched = true; + logger.info("Patched Tricky Trials potion duration for " + potionKey + + " from " + incorrectDuration + " to " + correctDuration + " ticks."); + } + } + + return anyPatched; + } + @VisibleForTesting void loadConcoctions() { final ConfigurationSection concoctionSection = config.getConfigurationSection( diff --git a/src/main/java/com/gmail/nossr50/datatypes/database/UpgradeType.java b/src/main/java/com/gmail/nossr50/datatypes/database/UpgradeType.java index 971c85018..057142052 100644 --- a/src/main/java/com/gmail/nossr50/datatypes/database/UpgradeType.java +++ b/src/main/java/com/gmail/nossr50/datatypes/database/UpgradeType.java @@ -16,5 +16,6 @@ public enum UpgradeType { FIX_SPELLING_NETHERITE_SALVAGE, FIX_SPELLING_NETHERITE_REPAIR, FIX_NETHERITE_SALVAGE_QUANTITIES, - SQL_CHARSET_UTF8MB4 + SQL_CHARSET_UTF8MB4, + FIX_TRICKY_TRIALS_SPLASH_POTION_DURATIONS } diff --git a/src/test/java/com/gmail/nossr50/config/skills/alchemy/PotionConfigTest.java b/src/test/java/com/gmail/nossr50/config/skills/alchemy/PotionConfigTest.java index b9b648046..a2e747ee8 100644 --- a/src/test/java/com/gmail/nossr50/config/skills/alchemy/PotionConfigTest.java +++ b/src/test/java/com/gmail/nossr50/config/skills/alchemy/PotionConfigTest.java @@ -1,69 +1,263 @@ -//package com.gmail.nossr50.config.skills.alchemy; -// -//import com.gmail.nossr50.MMOTestEnvironment; -//import org.bukkit.inventory.meta.ItemMeta; -//import org.bukkit.inventory.meta.PotionMeta; -//import org.junit.jupiter.api.AfterEach; -//import org.junit.jupiter.api.BeforeEach; -//import org.junit.jupiter.api.Test; -// -//import java.io.File; -//import java.net.URL; -//import java.util.logging.Logger; -// -//import static org.junit.jupiter.api.Assertions.assertNotNull; -//import static org.mockito.ArgumentMatchers.any; -//import static org.mockito.Mockito.mock; -//import static org.mockito.Mockito.when; -// -//class PotionConfigTest extends MMOTestEnvironment { -// -// public static final String POTION_LEGACY_POTION_YML = "potion/legacy_potion.yml"; -// public static final String POTION_MODERN_YML = "potion/modern_potion.yml"; -// public static final Logger logger = Logger.getLogger(PotionConfigTest.class.getName()); -// -// @BeforeEach -// void setUp() { -// mockBaseEnvironment(logger); -// final PotionMeta potionMeta = mock(PotionMeta.class); -// when(itemFactory.getItemMeta(any())).thenReturn(potionMeta); -// } -// -// @AfterEach -// void tearDown() { -// cleanupBaseEnvironment(); -// } -// -// @Test -// void testLoadLegacyConfig() { -// final PotionConfig potionConfig = getPotionConfig(POTION_LEGACY_POTION_YML); -// assertNotNull(potionConfig); -// -// potionConfig.loadConcoctions(); -// int loaded = potionConfig.loadPotionMap(); -// System.out.println("Loaded " + loaded + " potions"); -// } -// -// @Test -// void testModernConfig() { -// final PotionConfig potionConfig = getPotionConfig(POTION_MODERN_YML); -// assertNotNull(potionConfig); -// -// potionConfig.loadConcoctions(); -// int loaded = potionConfig.loadPotionMap(); -// System.out.println("Loaded " + loaded + " potions"); -// } -// -// private PotionConfig getPotionConfig(String path) { -// // Get the file URL using the class loader -// final URL resource = getClass().getClassLoader().getResource(path); -// if (resource == null) { -// throw new IllegalArgumentException("file not found!"); -// } else { -// // Convert URL to a File object -// final File potionFile = new File(resource.getFile()); -// System.out.println("File path: " + potionFile.getAbsolutePath()); -// return new PotionConfig(potionFile); -// } -// } -//} \ No newline at end of file +package com.gmail.nossr50.config.skills.alchemy; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.StringReader; +import java.util.List; +import java.util.logging.Logger; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PotionConfigTest { + + private static final Logger LOGGER = Logger.getLogger(PotionConfigTest.class.getName()); + + // --------------------------------------------------------------------------- + // YAML helpers + // --------------------------------------------------------------------------- + + /** + * Builds a minimal potions YAML with the four Tricky Trials splash potions at the given + * duration, and the four lingering potions at a separate given duration. + */ + private static String allEightTrickyTrialsPotionsYaml( + final int splashDuration, final int lingeringDuration) { + return "Potions:\n" + + " SPLASH_POTION_OF_INFESTATION:\n" + + " Effects:\n" + + " - \"INFESTED 0 " + splashDuration + "\"\n" + + " SPLASH_POTION_OF_WEAVING:\n" + + " Effects:\n" + + " - \"WEAVING 0 " + splashDuration + "\"\n" + + " SPLASH_POTION_OF_WIND_CHARGING:\n" + + " Effects:\n" + + " - \"WIND_CHARGED 0 " + splashDuration + "\"\n" + + " SPLASH_POTION_OF_OOZING:\n" + + " Effects:\n" + + " - \"OOZING 0 " + splashDuration + "\"\n" + + " LINGERING_POTION_OF_INFESTATION:\n" + + " Effects:\n" + + " - \"INFESTED 0 " + lingeringDuration + "\"\n" + + " LINGERING_POTION_OF_WEAVING:\n" + + " Effects:\n" + + " - \"WEAVING 0 " + lingeringDuration + "\"\n" + + " LINGERING_POTION_WIND_CHARGING:\n" + + " Effects:\n" + + " - \"WIND_CHARGED 0 " + lingeringDuration + "\"\n" + + " LINGERING_POTION_OF_OOZING:\n" + + " Effects:\n" + + " - \"OOZING 0 " + lingeringDuration + "\"\n"; + } + + /** Parses a YAML string into a {@link YamlConfiguration} without touching the file system. */ + private static YamlConfiguration loadYaml(final String yaml) { + return YamlConfiguration.loadConfiguration(new StringReader(yaml)); + } + + /** + * Extracts the integer duration from the first Effects entry of the named potion, + * splitting the {@code "EFFECT AMPLIFIER DURATION"} string by whitespace. + */ + private static int parseDurationFromFirstEffect( + final YamlConfiguration config, final String potionKey) { + final ConfigurationSection potionSection = + config.getConfigurationSection("Potions." + potionKey); + assertThat(potionSection) + .as("Config section for potion key '%s' should exist", potionKey) + .isNotNull(); + final List effects = potionSection.getStringList("Effects"); + assertThat(effects) + .as("Effects list for '%s' should not be empty", potionKey) + .isNotEmpty(); + final String[] parts = effects.get(0).trim().split("\\s+"); + return Integer.parseInt(parts[2]); + } + + // --------------------------------------------------------------------------- + // Migration tests + // --------------------------------------------------------------------------- + + @Nested + class TrickyTrialsPotionDurationMigration { + + @Test + void patchesAllEightPotionsWhenDurationsAreIncorrect() { + /* + * Intent: verifies end-to-end that fixTrickyTrialsPotionDurations correctly patches + * all four splash potions (2500 -> 3600) and all four lingering potions (3000 -> 900). + * The original mcMMO defaults shipped wrong values before the Warriorrrr fix; this + * migration corrects existing user installs that still have the old values. + */ + + // Given: config with all eight potions at their respective incorrect durations + final YamlConfiguration configWithBadDurations = loadYaml( + allEightTrickyTrialsPotionsYaml( + PotionConfig.TRICKY_TRIALS_SPLASH_INCORRECT_DURATION, + PotionConfig.TRICKY_TRIALS_LINGERING_INCORRECT_DURATION)); + + // When: migration runs + final boolean patched = PotionConfig.fixTrickyTrialsPotionDurations( + configWithBadDurations, LOGGER); + + // Then: method reports that patches were applied + assertThat(patched).isTrue(); + + // Then: all four splash potions have the correct 3600-tick duration + for (final String splashKey : PotionConfig.TRICKY_TRIALS_SPLASH_POTION_KEYS) { + assertThat(parseDurationFromFirstEffect(configWithBadDurations, splashKey)) + .as("Splash potion '%s' should be patched to %d ticks", + splashKey, PotionConfig.TRICKY_TRIALS_SPLASH_CORRECT_DURATION) + .isEqualTo(PotionConfig.TRICKY_TRIALS_SPLASH_CORRECT_DURATION); + } + + // Then: all four lingering potions have the correct 900-tick duration + for (final String lingeringKey : PotionConfig.TRICKY_TRIALS_LINGERING_POTION_KEYS) { + assertThat(parseDurationFromFirstEffect(configWithBadDurations, lingeringKey)) + .as("Lingering potion '%s' should be patched to %d ticks", + lingeringKey, PotionConfig.TRICKY_TRIALS_LINGERING_CORRECT_DURATION) + .isEqualTo(PotionConfig.TRICKY_TRIALS_LINGERING_CORRECT_DURATION); + } + } + + @Test + void returnsFalseAndLeavesValuesUnchangedWhenAllDurationsAreAlreadyCorrect() { + // Given: config where all eight potions already have the correct durations + final YamlConfiguration configWithCorrectDurations = loadYaml( + allEightTrickyTrialsPotionsYaml( + PotionConfig.TRICKY_TRIALS_SPLASH_CORRECT_DURATION, + PotionConfig.TRICKY_TRIALS_LINGERING_CORRECT_DURATION)); + + // When: migration runs + final boolean patched = PotionConfig.fixTrickyTrialsPotionDurations( + configWithCorrectDurations, LOGGER); + + // Then: no patches were applied + assertThat(patched).isFalse(); + + // Then: values are unchanged + for (final String splashKey : PotionConfig.TRICKY_TRIALS_SPLASH_POTION_KEYS) { + assertThat(parseDurationFromFirstEffect(configWithCorrectDurations, splashKey)) + .isEqualTo(PotionConfig.TRICKY_TRIALS_SPLASH_CORRECT_DURATION); + } + for (final String lingeringKey : PotionConfig.TRICKY_TRIALS_LINGERING_POTION_KEYS) { + assertThat(parseDurationFromFirstEffect(configWithCorrectDurations, lingeringKey)) + .isEqualTo(PotionConfig.TRICKY_TRIALS_LINGERING_CORRECT_DURATION); + } + } + + @ParameterizedTest(name = "custom splash duration {0} is preserved unchanged") + @ValueSource(ints = {1200, 1800, 4000, 7200}) + void doesNotModifyUserCustomizedSplashDurations(final int customDuration) { + // Given: a user has set a custom splash duration (not the bad default 2500) + final YamlConfiguration config = loadYaml( + allEightTrickyTrialsPotionsYaml( + customDuration, + PotionConfig.TRICKY_TRIALS_LINGERING_CORRECT_DURATION)); + + // When + final boolean patched = PotionConfig.fixTrickyTrialsPotionDurations(config, LOGGER); + + // Then: custom splash values are untouched + assertThat(patched).isFalse(); + for (final String splashKey : PotionConfig.TRICKY_TRIALS_SPLASH_POTION_KEYS) { + assertThat(parseDurationFromFirstEffect(config, splashKey)) + .as("Custom splash duration %d for '%s' should not be modified", + customDuration, splashKey) + .isEqualTo(customDuration); + } + } + + @ParameterizedTest(name = "custom lingering duration {0} is preserved unchanged") + @ValueSource(ints = {300, 600, 1200, 2000}) + void doesNotModifyUserCustomizedLingeringDurations(final int customDuration) { + // Given: a user has set a custom lingering duration (not the bad default 3000) + final YamlConfiguration config = loadYaml( + allEightTrickyTrialsPotionsYaml( + PotionConfig.TRICKY_TRIALS_SPLASH_CORRECT_DURATION, + customDuration)); + + // When + final boolean patched = PotionConfig.fixTrickyTrialsPotionDurations(config, LOGGER); + + // Then: custom lingering values are untouched + assertThat(patched).isFalse(); + for (final String lingeringKey : PotionConfig.TRICKY_TRIALS_LINGERING_POTION_KEYS) { + assertThat(parseDurationFromFirstEffect(config, lingeringKey)) + .as("Custom lingering duration %d for '%s' should not be modified", + customDuration, lingeringKey) + .isEqualTo(customDuration); + } + } + + @Test + void silentlySkipsMissingPotionKeysAndPatchesPresentOnes() { + /* + * Intent: users who have customized potions.yml may have removed some or all of the + * Tricky Trials potions. The migration must not log warnings or errors for absent + * keys — it should silently skip them and only patch the keys that are present. + */ + + // Given: only two of the eight Tricky Trials potions are defined, both at bad values + final String yamlWithTwoPotions = "Potions:\n" + + " SPLASH_POTION_OF_INFESTATION:\n" + + " Effects:\n" + + " - \"INFESTED 0 " + PotionConfig.TRICKY_TRIALS_SPLASH_INCORRECT_DURATION + "\"\n" + + " LINGERING_POTION_OF_OOZING:\n" + + " Effects:\n" + + " - \"OOZING 0 " + PotionConfig.TRICKY_TRIALS_LINGERING_INCORRECT_DURATION + "\"\n"; + final YamlConfiguration partialConfig = loadYaml(yamlWithTwoPotions); + + // When + final boolean patched = PotionConfig.fixTrickyTrialsPotionDurations( + partialConfig, LOGGER); + + // Then: the two present potions are patched + assertThat(patched).isTrue(); + assertThat(parseDurationFromFirstEffect(partialConfig, "SPLASH_POTION_OF_INFESTATION")) + .isEqualTo(PotionConfig.TRICKY_TRIALS_SPLASH_CORRECT_DURATION); + assertThat(parseDurationFromFirstEffect(partialConfig, "LINGERING_POTION_OF_OOZING")) + .isEqualTo(PotionConfig.TRICKY_TRIALS_LINGERING_CORRECT_DURATION); + + // Then: the absent potions were silently skipped (still null) + assertThat(partialConfig.getConfigurationSection( + "Potions.SPLASH_POTION_OF_OOZING")).isNull(); + assertThat(partialConfig.getConfigurationSection( + "Potions.LINGERING_POTION_OF_INFESTATION")).isNull(); + } + + @Test + void returnsFalseWhenNoPotionsSectionExistsInConfig() { + // Given: a completely empty config with no Potions section + final YamlConfiguration emptyConfig = loadYaml("SomeOtherSection:\n key: value\n"); + + // When + final boolean patched = + PotionConfig.fixTrickyTrialsPotionDurations(emptyConfig, LOGGER); + + // Then: nothing is patched, method returns false without throwing + assertThat(patched).isFalse(); + } + + @Test + void returnsFalseWhenAllTrickyTrialsKeysAreAbsent() { + // Given: a config with unrelated potions only + final String yamlWithUnrelatedPotions = "Potions:\n" + + " POTION_OF_WATER_BREATHING:\n" + + " Effects:\n" + + " - \"WATER_BREATHING 0 3600\"\n"; + final YamlConfiguration configWithOnlyUnrelatedPotions = + loadYaml(yamlWithUnrelatedPotions); + + // When + final boolean patched = PotionConfig.fixTrickyTrialsPotionDurations( + configWithOnlyUnrelatedPotions, LOGGER); + + // Then + assertThat(patched).isFalse(); + } + } +}