diff --git a/src/main/java/world/bentobox/bentobox/listeners/flags/worldsettings/LiquidsFlowingOutListener.java b/src/main/java/world/bentobox/bentobox/listeners/flags/worldsettings/LiquidsFlowingOutListener.java new file mode 100644 index 000000000..572378c7b --- /dev/null +++ b/src/main/java/world/bentobox/bentobox/listeners/flags/worldsettings/LiquidsFlowingOutListener.java @@ -0,0 +1,44 @@ +package world.bentobox.bentobox.listeners.flags.worldsettings; + +import org.bukkit.block.Block; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.block.BlockFromToEvent; +import world.bentobox.bentobox.api.flags.FlagListener; +import world.bentobox.bentobox.lists.Flags; + +/** + * Handles {@link world.bentobox.bentobox.lists.Flags#LIQUIDS_FLOWING_OUT}. + * @author Poslovitch + * @since 1.3.0 + */ +public class LiquidsFlowingOutListener extends FlagListener { + + @EventHandler(priority = EventPriority.NORMAL) + public void onLiquidFlow(BlockFromToEvent e) { + Block from = e.getBlock(); + if (!from.isLiquid()) { + return; + } + + Block to = e.getToBlock(); + if (!getIWM().inWorld(from.getLocation()) || Flags.LIQUIDS_FLOWING_OUT.isSetForWorld(from.getWorld())) { + // We do not want to run any check if this is not the right world or if it is allowed. + return; + } + + // TODO: Make a less restrictive check - try to see where the border is, so that + // water can still flows sideways. + // see: https://github.com/BentoBoxWorld/BentoBox/issues/511#issuecomment-460040287 + // Time to do some maths! We've got the vector FromTo, let's check if its y coordinate is different from zero. + if (to.getLocation().toVector().subtract(from.getLocation().toVector()).getY() != 0) { + // We do not run any checks if this is a vertical flow - would be too much resource consuming. + return; + } + + // Only prevent if it is flowing into the area between islands. + if (!getIslands().getProtectedIslandAt(to.getLocation()).isPresent()) { + e.setCancelled(true); + } + } +} diff --git a/src/main/java/world/bentobox/bentobox/lists/Flags.java b/src/main/java/world/bentobox/bentobox/lists/Flags.java index ebff35fa8..3fdc7c6f8 100644 --- a/src/main/java/world/bentobox/bentobox/lists/Flags.java +++ b/src/main/java/world/bentobox/bentobox/lists/Flags.java @@ -10,6 +10,7 @@ import org.bukkit.Material; import world.bentobox.bentobox.api.flags.Flag; import world.bentobox.bentobox.api.flags.Flag.Type; import world.bentobox.bentobox.api.flags.clicklisteners.CycleClick; +import world.bentobox.bentobox.listeners.flags.worldsettings.LiquidsFlowingOutListener; import world.bentobox.bentobox.listeners.flags.worldsettings.ObsidianScoopingListener; import world.bentobox.bentobox.listeners.flags.protection.BlockInteractionListener; import world.bentobox.bentobox.listeners.flags.protection.BreakBlocksListener; @@ -73,7 +74,7 @@ public final class Flags { public static final Flag PLACE_BLOCKS = new Flag.Builder("PLACE_BLOCKS", Material.GRASS).listener(new PlaceBlocksListener()).build(); /** - * Prevents players from generated Frosted Ice on one's island using boots enchanted with "Frost Walker". + * Prevents players from generating Frosted Ice on one's island using "Frost Walker" enchanted boots. * @see PlaceBlocksListener */ public static final Flag FROST_WALKER = new Flag.Builder("FROST_WALKER", Material.ICE).build(); @@ -129,7 +130,7 @@ public final class Flags { // Throwing things public static final Flag EGGS = new Flag.Builder("EGGS", Material.EGG).listener(new EggListener()).build(); /** - * Prevents players from throwing potions / exp bottles. + * Prevents players from throwing potions / experience bottles. * @since 1.1 */ public static final Flag POTION_THROWING = new Flag.Builder("POTION_THROWING", Material.SPLASH_POTION).listener(new ThrowingListener()).build(); @@ -271,6 +272,14 @@ public final class Flags { public static final Flag OBSIDIAN_SCOOPING = new Flag.Builder("OBSIDIAN_SCOOPING", Material.OBSIDIAN).type(Type.WORLD_SETTING) .listener(new ObsidianScoopingListener()).defaultSetting(true).build(); + /** + * Prevents liquids from flowing outside the protection range, in order to avoid cobblestone/stone/obsidian being generated and remaining unbreakable by players. + * @since 1.3.0 + * @see LiquidsFlowingOutListener + */ + public static final Flag LIQUIDS_FLOWING_OUT = new Flag.Builder("LIQUIDS_FLOWING_OUT", Material.WATER_BUCKET).type(Type.WORLD_SETTING) + .listener(new LiquidsFlowingOutListener()).build(); + /** * Provides a list of all the Flag instances contained in this class using reflection. * @return List of all the flags in this class diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml index a47f9b836..dd38c5d1c 100644 --- a/src/main/resources/locales/en-US.yml +++ b/src/main/resources/locales/en-US.yml @@ -651,6 +651,14 @@ protection: description: "Toggle use" name: "Lever use" hint: "No lever use" + LIQUIDS_FLOWING_OUT: + name: "Liquids flowing outside islands" + description: |- + &aToggle whether liquids can flow outside + &aof the island's protection range. + &aDisabling it helps avoiding lava and water + &agenerating cobblestone in the area between + &atwo islands. LOCK: description: "Toggle lock" name: "Lock island" diff --git a/src/test/java/world/bentobox/bentobox/listeners/flags/LiquidsFlowingOutListenerTest.java b/src/test/java/world/bentobox/bentobox/listeners/flags/LiquidsFlowingOutListenerTest.java new file mode 100644 index 000000000..1c4e3055c --- /dev/null +++ b/src/test/java/world/bentobox/bentobox/listeners/flags/LiquidsFlowingOutListenerTest.java @@ -0,0 +1,184 @@ +package world.bentobox.bentobox.listeners.flags; + +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.event.block.BlockFromToEvent; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; +import world.bentobox.bentobox.BentoBox; +import world.bentobox.bentobox.api.configuration.WorldSettings; +import world.bentobox.bentobox.database.objects.Island; +import world.bentobox.bentobox.listeners.flags.worldsettings.LiquidsFlowingOutListener; +import world.bentobox.bentobox.lists.Flags; +import world.bentobox.bentobox.managers.IslandWorldManager; +import world.bentobox.bentobox.managers.IslandsManager; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Tests {@link world.bentobox.bentobox.listeners.flags.worldsettings.LiquidsFlowingOutListener}. + * @author Poslovitch + * @since 1.3.0 + */ +@RunWith(PowerMockRunner.class) +@PrepareForTest({BentoBox.class}) +public class LiquidsFlowingOutListenerTest { + + /* IslandWorldManager */ + private IslandWorldManager iwm; + + /* Blocks */ + private Block from; + private Block to; + + /* Event */ + private BlockFromToEvent event; + + /* World */ + private World world; + + /* Islands */ + private IslandsManager islandsManager; + + @Before + public void setUp() throws Exception { + // Set up plugin + BentoBox plugin = mock(BentoBox.class); + Whitebox.setInternalState(BentoBox.class, "instance", plugin); + + /* Blocks */ + from = mock(Block.class); + when(from.isLiquid()).thenReturn(true); + to = mock(Block.class); + + /* World */ + world = mock(World.class); + when(from.getWorld()).thenReturn(world); + + // Give them locations + Location fromLocation = new Location(world, 0, 0, 0); + when(from.getLocation()).thenReturn(fromLocation); + + Location toLocation = new Location(world, 1, 0, 0); + when(to.getLocation()).thenReturn(toLocation); + + /* Event */ + event = new BlockFromToEvent(from, to); + + /* Island World Manager */ + iwm = mock(IslandWorldManager.class); + when(plugin.getIWM()).thenReturn(iwm); + + // WorldSettings and World Flags + WorldSettings ws = mock(WorldSettings.class); + when(iwm.getWorldSettings(Mockito.any())).thenReturn(ws); + Map worldFlags = new HashMap<>(); + when(ws.getWorldFlags()).thenReturn(worldFlags); + + // By default everything is in world + when(iwm.inWorld(any(World.class))).thenReturn(true); + when(iwm.inWorld(any(Location.class))).thenReturn(true); + + /* Flags */ + // By default, it is not allowed + Flags.LIQUIDS_FLOWING_OUT.setSetting(world, false); + + /* Islands */ + islandsManager = mock(IslandsManager.class); + when(plugin.getIslands()).thenReturn(islandsManager); + // By default, there should be no island's protection range at toLocation. + when(islandsManager.getProtectedIslandAt(toLocation)).thenReturn(Optional.empty()); + } + + /** + * Asserts that the event is never cancelled when the 'from' block is not liquid. + */ + @Test + public void testFromIsNotLiquid() { + // The 'from' block is not liquid + when(from.isLiquid()).thenReturn(false); + + // Run + new LiquidsFlowingOutListener().onLiquidFlow(event); + assertFalse(event.isCancelled()); + } + + /** + * Asserts that the event is never cancelled when the 'from' block is not in the world. + */ + @Test + public void testFromIsNotInWorld() { + // Not in world + when(iwm.inWorld(any(World.class))).thenReturn(false); + when(iwm.inWorld(any(Location.class))).thenReturn(false); + + // Run + new LiquidsFlowingOutListener().onLiquidFlow(event); + assertFalse(event.isCancelled()); + } + + /** + * Asserts that the event is never cancelled when {@link Flags#LIQUIDS_FLOWING_OUT} is allowed. + */ + @Test + public void testFlagIsAllowed() { + // Allowed + Flags.LIQUIDS_FLOWING_OUT.setSetting(world, true); + + // Run + new LiquidsFlowingOutListener().onLiquidFlow(event); + assertFalse(event.isCancelled()); + } + + /** + * Asserts that the event is never cancelled when the liquid flows vertically. + */ + @Test + public void testLiquidFlowsVertically() { + // "To" is at (1,0,0) + // Set "from" at (1,1,0) so that the vector's y coordinate != 0, which means the liquid flows vertically. + when(from.getLocation()).thenReturn(new Location(world, 1, 1, 0)); + + // Run + new LiquidsFlowingOutListener().onLiquidFlow(event); + assertFalse(event.isCancelled()); + } + + /** + * Asserts that the event is never cancelled when the liquid flows to a location in an island's protection range. + */ + @Test + public void testLiquidFlowsToLocationInIslandProtectionRange() { + // There's a protected island at the "to" + Island island = mock(Island.class); + when(islandsManager.getProtectedIslandAt(to.getLocation())).thenReturn(Optional.of(island)); + + // Run + new LiquidsFlowingOutListener().onLiquidFlow(event); + assertFalse(event.isCancelled()); + } + + /** + * Asserts that the event is cancelled with the default configuration provided in {@link LiquidsFlowingOutListenerTest#setUp()}. + */ + @Test + public void testLiquidFlowIsBlocked() { + // Run + new LiquidsFlowingOutListener().onLiquidFlow(event); + assertTrue(event.isCancelled()); + } +}