From bd59392a2849f147b836741d5972b889c62b41b7 Mon Sep 17 00:00:00 2001 From: tastybento Date: Mon, 10 Aug 2026 18:42:27 -0700 Subject: [PATCH] Make the generator AcidIsland-aware AcidIsland water is acid, so its LavaCheck listener turns stone, that vanilla creates when lava pours into water, back into water. It does that by checking one tick later if the block is still stone. This addon replaced the forming stone with a generator block before that check ran, so the block survived and a single lava bucket could be used to convert an entire ocean into generator blocks. The addon now skips block generation when a water block turns into stone in a world that is managed by AcidIsland and that has acid damage enabled, so AcidIsland can revert it as it normally does. Cobblestone generators form the block at the lava block, so they are not affected. AcidIsland is a soft dependency, so the game mode is recognised by its name and the acid damage value is read reflectively. Can be disabled with the new `acid-island-aware` config option. https://github.com/BentoBoxWorld/MagicCobblestoneGenerator/issues/173 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01SDLdE5LSXRwc6NQhYvzyiv --- .../config/Settings.java | 35 ++++ .../listeners/VanillaGeneratorListener.java | 14 ++ .../utils/AcidIslandHelper.java | 157 ++++++++++++++++ src/main/resources/config.yml | 9 + .../VanillaGeneratorListenerTest.java | 170 ++++++++++++++++++ 5 files changed, 385 insertions(+) create mode 100644 src/main/java/world/bentobox/magiccobblestonegenerator/utils/AcidIslandHelper.java create mode 100644 src/test/java/world/bentobox/magiccobblestonegenerator/listeners/VanillaGeneratorListenerTest.java diff --git a/src/main/java/world/bentobox/magiccobblestonegenerator/config/Settings.java b/src/main/java/world/bentobox/magiccobblestonegenerator/config/Settings.java index 34831d7..5f9ebcf 100644 --- a/src/main/java/world/bentobox/magiccobblestonegenerator/config/Settings.java +++ b/src/main/java/world/bentobox/magiccobblestonegenerator/config/Settings.java @@ -372,6 +372,30 @@ public void setUseBankAccount(boolean useBankAccount) } + /** + * Is acid island aware boolean. + * + * @return {@code true} if the addon must not replace blocks that AcidIsland reverts to water. + * @since 2.10.0 + */ + public boolean isAcidIslandAware() + { + return acidIslandAware; + } + + + /** + * Sets acid island aware. + * + * @param acidIslandAware new value for this object. + * @since 2.10.0 + */ + public void setAcidIslandAware(boolean acidIslandAware) + { + this.acidIslandAware = acidIslandAware; + } + + /** * Gets the default number of blocks a generator is allowed to generate during a single exhaustion period. * @@ -558,6 +582,17 @@ public enum GuiAction @ConfigEntry(path = "use-bank-account") private boolean useBankAccount = false; + @ConfigComment("") + @ConfigComment("This indicates if the addon should respect AcidIsland acid water.") + @ConfigComment("AcidIsland turns stone, that is created when lava pours into its acid water, back") + @ConfigComment("into water. If this option is enabled, the addon will not process such blocks, so") + @ConfigComment("a single lava bucket cannot be used to convert an entire ocean into generator") + @ConfigComment("blocks. Normal cobblestone generators are not affected by this option.") + @ConfigComment("This option does nothing in worlds that are not managed by AcidIsland, or if acid") + @ConfigComment("damage is disabled in the AcidIsland config.") + @ConfigEntry(path = "acid-island-aware") + private boolean acidIslandAware = true; + @ConfigComment("") @ConfigComment("This list stores GameModes in which the addon should not work.") @ConfigComment("To disable addon it is necessary to write its name in new line that starts with -. Example:") diff --git a/src/main/java/world/bentobox/magiccobblestonegenerator/listeners/VanillaGeneratorListener.java b/src/main/java/world/bentobox/magiccobblestonegenerator/listeners/VanillaGeneratorListener.java index 6893050..f0d1072 100644 --- a/src/main/java/world/bentobox/magiccobblestonegenerator/listeners/VanillaGeneratorListener.java +++ b/src/main/java/world/bentobox/magiccobblestonegenerator/listeners/VanillaGeneratorListener.java @@ -17,6 +17,7 @@ import world.bentobox.bentobox.database.objects.Island; import world.bentobox.magiccobblestonegenerator.StoneGeneratorAddon; +import world.bentobox.magiccobblestonegenerator.utils.AcidIslandHelper; import world.bentobox.magiccobblestonegenerator.utils.CustomBlocks; import world.bentobox.magiccobblestonegenerator.utils.Why; @@ -70,6 +71,19 @@ public void onBlockFormEvent(BlockFormEvent event) Island island = islandOptional.get(); + if (this.addon.getSettings().isAcidIslandAware() && + event.getNewState().getType() == Material.STONE && + eventSourceBlock.getType() == Material.WATER && + AcidIslandHelper.revertsStoneFormedInWater(this.addon, eventSourceBlock.getWorld())) + { + // Lava poured into acid water. AcidIsland turns this stone back into water on the next + // tick, but only if it is still stone. Replacing it would defeat that protection and + // allow whole oceans to be converted into generator blocks. + Why.report(island, eventSourceBlock.getLocation(), + "AcidIsland reverts stone that is formed in acid water!"); + return; + } + if (!island.isAllowed(StoneGeneratorAddon.MAGIC_COBBLESTONE_GENERATOR)) { // Currently addon is not working outside island protection ranges. diff --git a/src/main/java/world/bentobox/magiccobblestonegenerator/utils/AcidIslandHelper.java b/src/main/java/world/bentobox/magiccobblestonegenerator/utils/AcidIslandHelper.java new file mode 100644 index 0000000..f84def1 --- /dev/null +++ b/src/main/java/world/bentobox/magiccobblestonegenerator/utils/AcidIslandHelper.java @@ -0,0 +1,157 @@ +// +// Created by BONNe +// Copyright - 2020 +// + + +package world.bentobox.magiccobblestonegenerator.utils; + + +import java.lang.reflect.Method; +import java.util.Optional; + +import org.bukkit.World; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import world.bentobox.bentobox.api.addons.GameModeAddon; +import world.bentobox.bentobox.api.configuration.WorldSettings; +import world.bentobox.magiccobblestonegenerator.StoneGeneratorAddon; + + +/** + * Helper that detects if AcidIsland is going to undo a block that this addon is about to replace. + *

+ * AcidIsland water is acid, so its LavaCheck listener turns stone that vanilla creates when lava + * pours into water back into water again. It does that by checking, one tick later, if the block is + * stone. If this addon replaces the forming stone with a generator block, that check no longer + * matches and the block survives, which lets a single lava bucket turn an entire ocean into + * generator blocks. + *

+ * AcidIsland is only a soft dependency, so the game mode is recognised by its name and the acid + * damage value is read reflectively. That also keeps this working if AcidIsland is not installed at + * all. + * + * @since 2.10.0 + */ +public final class AcidIslandHelper +{ + /** + * Private constructor. This is a utility class. + */ + private AcidIslandHelper() + { + // Utility class. + } + + + /** + * This method returns if AcidIsland manages the given world and will revert stone that is formed + * inside its acid water back to water. + * + * @param addon Instance of this addon. + * @param world World where the block is formed. + * @return {@code true} if AcidIsland will revert the formed stone, {@code false} otherwise. + */ + public static boolean revertsStoneFormedInWater(@NotNull StoneGeneratorAddon addon, @NotNull World world) + { + Optional gameMode = addon.getPlugin().getIWM().getAddon(world); + + if (gameMode.isEmpty() || !ACID_ISLAND.equals(gameMode.get().getDescription().getName())) + { + // Not an AcidIsland world. + return false; + } + + // AcidIsland reverts the stone only if acid actually does damage. + return getAcidDamage(gameMode.get().getWorldSettings()) > 0; + } + + + /** + * This method returns the acid damage value from AcidIsland world settings. + * + * @param worldSettings World settings of the AcidIsland game mode. + * @return Acid damage value or 0 if it could not be read. + */ + private static int getAcidDamage(@Nullable WorldSettings worldSettings) + { + if (worldSettings == null) + { + return 0; + } + + Method method = getAcidDamageMethod(worldSettings.getClass()); + + if (method == null) + { + return 0; + } + + try + { + return ((Number) method.invoke(worldSettings)).intValue(); + } + catch (ReflectiveOperationException | ClassCastException | NullPointerException e) + { + return 0; + } + } + + + /** + * This method returns the cached acid damage getter for the given world settings class. + * + * @param settingsClass Class of the AcidIsland world settings. + * @return The getter method or {@code null} if the class does not have one. + */ + @Nullable + private static Method getAcidDamageMethod(@NotNull Class settingsClass) + { + if (settingsClass.equals(cachedSettingsClass)) + { + return cachedAcidDamageMethod; + } + + Method method; + + try + { + method = settingsClass.getMethod(ACID_DAMAGE_GETTER); + } + catch (NoSuchMethodException | SecurityException e) + { + method = null; + } + + cachedSettingsClass = settingsClass; + cachedAcidDamageMethod = method; + + return method; + } + + +// --------------------------------------------------------------------- +// Section: Variables +// --------------------------------------------------------------------- + + /** + * Name of the AcidIsland game mode addon. + */ + private static final String ACID_ISLAND = "AcidIsland"; + + /** + * Name of the method that returns player acid damage in AcidIsland settings. + */ + private static final String ACID_DAMAGE_GETTER = "getAcidDamage"; + + /** + * Class for which the acid damage getter is cached. + */ + private static Class cachedSettingsClass; + + /** + * Cached acid damage getter. + */ + private static Method cachedAcidDamageMethod; +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 5e91785..c0cd1bf 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -37,6 +37,15 @@ notify-on-unlock: true # Requires Bank Addon use-bank: false # +# This indicates if the addon should respect AcidIsland acid water. +# AcidIsland turns stone, that is created when lava pours into its acid water, back +# into water. If this option is enabled, the addon will not process such blocks, so +# a single lava bucket cannot be used to convert an entire ocean into generator +# blocks. Normal cobblestone generators are not affected by this option. +# This option does nothing in worlds that are not managed by AcidIsland, or if acid +# damage is disabled in the AcidIsland config. +acid-island-aware: true +# # This list stores GameModes in which the addon should not work. # To disable addon it is necessary to write its name in new line that starts with -. Example: # disabled-gamemodes: diff --git a/src/test/java/world/bentobox/magiccobblestonegenerator/listeners/VanillaGeneratorListenerTest.java b/src/test/java/world/bentobox/magiccobblestonegenerator/listeners/VanillaGeneratorListenerTest.java new file mode 100644 index 0000000..2565495 --- /dev/null +++ b/src/test/java/world/bentobox/magiccobblestonegenerator/listeners/VanillaGeneratorListenerTest.java @@ -0,0 +1,170 @@ +package world.bentobox.magiccobblestonegenerator.listeners; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.Optional; + +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.block.BlockState; +import org.bukkit.event.block.BlockFormEvent; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; + +import world.bentobox.bentobox.api.addons.AddonDescription; +import world.bentobox.bentobox.api.addons.GameModeAddon; +import world.bentobox.bentobox.api.configuration.WorldSettings; +import world.bentobox.magiccobblestonegenerator.CommonTestSetup; +import world.bentobox.magiccobblestonegenerator.StoneGeneratorAddon; +import world.bentobox.magiccobblestonegenerator.TestWorldSettings; +import world.bentobox.magiccobblestonegenerator.config.Settings; +import world.bentobox.magiccobblestonegenerator.database.objects.GeneratorTierObject; +import world.bentobox.magiccobblestonegenerator.managers.StoneGeneratorManager; +import world.bentobox.magiccobblestonegenerator.tasks.MagicGenerator; + +/** + * Tests for {@link VanillaGeneratorListener}, focused on AcidIsland awareness (#173). + */ +class VanillaGeneratorListenerTest extends CommonTestSetup { + + /** + * World settings that pretend to be AcidIsland ones, i.e. they have the acid damage getter that + * is read reflectively. + */ + public static class AcidWorldSettings extends TestWorldSettings { + + private final int acidDamage; + + public AcidWorldSettings(int acidDamage) { + this.acidDamage = acidDamage; + } + + @SuppressWarnings("unused") + public int getAcidDamage() { + return acidDamage; + } + } + + @Mock + private StoneGeneratorAddon addon; + @Mock + private StoneGeneratorManager manager; + @Mock + private MagicGenerator generator; + @Mock + private GeneratorTierObject generatorTier; + @Mock + private Block block; + @Mock + private BlockState newState; + + private Settings settings; + private VanillaGeneratorListener listener; + + @Override + @BeforeEach + public void setUp() throws Exception { + super.setUp(); + + settings = new Settings(); + // Skip the online member and range checks. + settings.setOfflineGeneration(true); + settings.setDefaultWorkingRange(0); + + when(addon.getSettings()).thenReturn(settings); + when(addon.getPlugin()).thenReturn(plugin); + when(addon.getAddonManager()).thenReturn(manager); + when(addon.getIslands()).thenReturn(im); + when(addon.getGenerator()).thenReturn(generator); + + when(manager.canOperateInWorld(any())).thenReturn(true); + when(manager.getGeneratorTier(any(), any(), any())).thenReturn(generatorTier); + when(manager.canGenerateBlock(any(), any())).thenReturn(true); + when(generator.processBlockReplacement(any(), any(), any())).thenReturn("DIAMOND_ORE"); + + when(im.getIslandAt(any())).thenReturn(Optional.of(island)); + when(island.isAllowed(any())).thenReturn(true); + + // Why reporting reads player metadata. + when(mockPlayer.getMetadata(anyString())).thenReturn(Collections.emptyList()); + + // Lava pours into water: the water block turns into stone. + when(block.isLiquid()).thenReturn(true); + when(block.getType()).thenReturn(Material.WATER); + when(block.getWorld()).thenReturn(world); + when(block.getLocation()).thenReturn(location); + when(newState.getType()).thenReturn(Material.STONE); + + listener = new VanillaGeneratorListener(addon); + } + + /** + * Makes the world belong to a game mode with the given name and world settings. + */ + private void setGameMode(String name, WorldSettings worldSettings) { + GameModeAddon gameMode = mock(GameModeAddon.class); + AddonDescription description = mock(AddonDescription.class); + when(description.getName()).thenReturn(name); + when(gameMode.getDescription()).thenReturn(description); + when(gameMode.getWorldSettings()).thenReturn(worldSettings); + when(iwm.getAddon(any())).thenReturn(Optional.of(gameMode)); + } + + @Test + void testStoneInAcidWaterIsNotReplaced() { + setGameMode("AcidIsland", new AcidWorldSettings(10)); + + listener.onBlockFormEvent(new BlockFormEvent(block, newState)); + + // AcidIsland reverts this block, so the generator must not touch it. + verify(newState, never()).setType(any()); + } + + @Test + void testStoneInAcidWaterIsReplacedIfAcidDamageIsDisabled() { + setGameMode("AcidIsland", new AcidWorldSettings(0)); + + listener.onBlockFormEvent(new BlockFormEvent(block, newState)); + + verify(newState).setType(Material.DIAMOND_ORE); + } + + @Test + void testStoneInAcidWaterIsReplacedIfOptionIsDisabled() { + settings.setAcidIslandAware(false); + setGameMode("AcidIsland", new AcidWorldSettings(10)); + + listener.onBlockFormEvent(new BlockFormEvent(block, newState)); + + verify(newState).setType(Material.DIAMOND_ORE); + } + + @Test + void testStoneInWaterIsReplacedInOtherGameModes() { + setGameMode("BSkyBlock", new TestWorldSettings()); + + listener.onBlockFormEvent(new BlockFormEvent(block, newState)); + + verify(newState).setType(Material.DIAMOND_ORE); + } + + @Test + void testCobblestoneGeneratorStillWorksInAcidIsland() { + setGameMode("AcidIsland", new AcidWorldSettings(10)); + + // A classic generator forms cobblestone at the lava block, not at the water block. + when(block.getType()).thenReturn(Material.LAVA); + when(newState.getType()).thenReturn(Material.COBBLESTONE); + + listener.onBlockFormEvent(new BlockFormEvent(block, newState)); + + verify(newState).setType(Material.DIAMOND_ORE); + } +}