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);
+ }
+}