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