diff --git a/pom.xml b/pom.xml index fc91f8455..696d9087e 100644 --- a/pom.xml +++ b/pom.xml @@ -321,6 +321,13 @@ RC-36 provided + + + com.github.LoneDev6 + api-itemsadder + 3.6.1 + provided + diff --git a/src/main/java/world/bentobox/bentobox/BentoBox.java b/src/main/java/world/bentobox/bentobox/BentoBox.java index 1fb0ed916..bea18a9c6 100644 --- a/src/main/java/world/bentobox/bentobox/BentoBox.java +++ b/src/main/java/world/bentobox/bentobox/BentoBox.java @@ -22,6 +22,7 @@ import world.bentobox.bentobox.api.user.Notifier; import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.commands.BentoBoxCommand; import world.bentobox.bentobox.database.DatabaseSetup; +import world.bentobox.bentobox.hooks.ItemsAdderHook; import world.bentobox.bentobox.hooks.MultiverseCoreHook; import world.bentobox.bentobox.hooks.MyWorldsHook; import world.bentobox.bentobox.hooks.SlimefunHook; @@ -235,6 +236,9 @@ public class BentoBox extends JavaPlugin implements Listener { // Register Slimefun hooksManager.registerHook(new SlimefunHook()); + // Register ItemsAdder + hooksManager.registerHook(new ItemsAdderHook(this)); + // TODO: re-enable after implementation //hooksManager.registerHook(new DynmapHook()); // TODO: re-enable after rework diff --git a/src/main/java/world/bentobox/bentobox/api/hooks/Hook.java b/src/main/java/world/bentobox/bentobox/api/hooks/Hook.java index 046679aca..63a6a9a25 100644 --- a/src/main/java/world/bentobox/bentobox/api/hooks/Hook.java +++ b/src/main/java/world/bentobox/bentobox/api/hooks/Hook.java @@ -69,5 +69,7 @@ public abstract class Hook { * Returns an explanation that will be sent to the user to tell them why the hook process did not succeed. * @return the probable causes why the hook process did not succeed. */ - public abstract String getFailureCause(); + public String getFailureCause() { + return ""; + } } diff --git a/src/main/java/world/bentobox/bentobox/hooks/ItemsAdderHook.java b/src/main/java/world/bentobox/bentobox/hooks/ItemsAdderHook.java new file mode 100644 index 000000000..9a96bd5fc --- /dev/null +++ b/src/main/java/world/bentobox/bentobox/hooks/ItemsAdderHook.java @@ -0,0 +1,124 @@ +package world.bentobox.bentobox.hooks; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.entity.EntityExplodeEvent; + +import dev.lone.itemsadder.api.CustomBlock; +import world.bentobox.bentobox.BentoBox; +import world.bentobox.bentobox.api.flags.Flag; +import world.bentobox.bentobox.api.flags.FlagListener; +import world.bentobox.bentobox.api.flags.clicklisteners.CycleClick; +import world.bentobox.bentobox.api.hooks.Hook; +import world.bentobox.bentobox.api.user.User; +import world.bentobox.bentobox.managers.RanksManager; + +/** + * Hook to enable itemsadder blocks to be deleted when islands are deleted. + * It also includes a flag to track explosion access + */ +public class ItemsAdderHook extends Hook { + + /** + * This flag allows to switch which island member group can use explosive items from Items Adder. + */ + public static final Flag ITEMS_ADDER_EXPLOSIONS = + new Flag.Builder("ITEMS_ADDER_EXPLOSIONS", Material.TNT). + type(Flag.Type.PROTECTION). + defaultRank(RanksManager.MEMBER_RANK). + clickHandler(new CycleClick("ITEMS_ADDER_EXPLOSIONS", + RanksManager.VISITOR_RANK, RanksManager.OWNER_RANK)) + . + build(); + + private BentoBox plugin; + + private BlockInteractListener listener; + + /** + * Register the hook + * @param plugin BentoBox + */ + public ItemsAdderHook(BentoBox plugin) { + super("ItemsAdder", Material.NETHER_STAR); + this.plugin = plugin; + } + + @Override + public boolean hook() { + // See if ItemsAdder is around + if (Bukkit.getPluginManager().getPlugin("ItemsAdder") == null) { + return false; + } + // Register listener + listener = new BlockInteractListener(); + Bukkit.getPluginManager().registerEvents(listener, plugin); + plugin.getFlagsManager().registerFlag(ITEMS_ADDER_EXPLOSIONS); + return true; + } + + /** + * @return the listener + */ + protected BlockInteractListener getListener() { + return listener; + } + + /** + * Remove the CustomBlock at location + * @param location + */ + public void clearBlockInfo(Location location) { + CustomBlock.remove(location); + } + + class BlockInteractListener extends FlagListener { + + /** + * Handles explosions of ItemAdder items + * @param event explosion event + */ + @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) + public void onExplosion(EntityExplodeEvent event) + { + if (!EntityType.PLAYER.equals(event.getEntityType())) { + // Ignore non-player explosions. + return; + } + + Player player = (Player) event.getEntity(); + + if (!player.hasPermission("XXXXXX")) { + // Ignore players that does not have magic XXXXXX permission. + return; + } + + // Use BentoBox flag processing system to validate usage. + // Technically not necessary as internally it should be cancelled by BentoBox. + + if (!this.checkIsland(event, player, event.getLocation(), ITEMS_ADDER_EXPLOSIONS)) { + // Remove any blocks from the explosion list if required + event.blockList().removeIf(block -> this.protect(player, block.getLocation())); + event.setCancelled(this.protect(player, event.getLocation())); + } + } + + + /** + * This method returns if the protection in given location is enabled or not. + * @param player Player who triggers explosion. + * @param location Location where explosion happens. + * @return {@code true} if location is protected, {@code false} otherwise. + */ + private boolean protect(Player player, Location location) + { + return plugin.getIslands().getProtectedIslandAt(location) + .map(island -> !island.isAllowed(User.getInstance(player), ITEMS_ADDER_EXPLOSIONS)).orElse(false); + } + } +} diff --git a/src/main/java/world/bentobox/bentobox/nms/CopyWorldRegenerator.java b/src/main/java/world/bentobox/bentobox/nms/CopyWorldRegenerator.java index 6b6a2800c..2e0c4fd8a 100644 --- a/src/main/java/world/bentobox/bentobox/nms/CopyWorldRegenerator.java +++ b/src/main/java/world/bentobox/bentobox/nms/CopyWorldRegenerator.java @@ -38,6 +38,7 @@ import io.papermc.lib.PaperLib; import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.api.addons.GameModeAddon; import world.bentobox.bentobox.database.objects.IslandDeletion; +import world.bentobox.bentobox.hooks.ItemsAdderHook; import world.bentobox.bentobox.hooks.SlimefunHook; import world.bentobox.bentobox.util.MyBiomeGrid; @@ -194,10 +195,12 @@ public abstract class CopyWorldRegenerator implements WorldRegenerator { if (x % 4 == 0 && y % 4 == 0 && z % 4 == 0) { toChunk.getBlock(x, y, z).setBiome(fromChunk.getBlock(x, y, z).getBiome()); } - // Delete any slimefun blocks + // Delete any 3rd party blocks Location loc = new Location(toChunk.getWorld(), baseX + x, y, baseZ + z); plugin.getHooks().getHook("Slimefun") - .ifPresent(sf -> ((SlimefunHook) sf).clearBlockInfo(loc, true)); + .ifPresent(hook -> ((SlimefunHook) hook).clearBlockInfo(loc, true)); + plugin.getHooks().getHook("ItemsAdder") + .ifPresent(hook -> ((ItemsAdderHook) hook).clearBlockInfo(loc)); } } } @@ -377,10 +380,12 @@ public abstract class CopyWorldRegenerator implements WorldRegenerator { if (x % 4 == 0 && y % 4 == 0 && z % 4 == 0) { chunk.getBlock(x, y, z).setBiome(biomeGrid.getBiome(x, y, z)); } - // Delete any slimefun blocks + // Delete any 3rd party blocks Location loc = new Location(chunk.getWorld(), baseX + x, y, baseZ + z); plugin.getHooks().getHook("Slimefun") .ifPresent(sf -> ((SlimefunHook) sf).clearBlockInfo(loc, true)); + plugin.getHooks().getHook("ItemsAdder") + .ifPresent(hook -> ((ItemsAdderHook) hook).clearBlockInfo(loc)); } } } diff --git a/src/main/java/world/bentobox/bentobox/nms/SimpleWorldRegenerator.java b/src/main/java/world/bentobox/bentobox/nms/SimpleWorldRegenerator.java deleted file mode 100644 index 218c72af7..000000000 --- a/src/main/java/world/bentobox/bentobox/nms/SimpleWorldRegenerator.java +++ /dev/null @@ -1,154 +0,0 @@ -package world.bentobox.bentobox.nms; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Random; -import java.util.concurrent.CompletableFuture; - -import org.bukkit.Chunk; -import org.bukkit.Location; -import org.bukkit.World; -import org.bukkit.block.data.BlockData; -import org.bukkit.entity.Entity; -import org.bukkit.entity.Player; -import org.bukkit.generator.BlockPopulator; -import org.bukkit.generator.ChunkGenerator; -import org.bukkit.inventory.InventoryHolder; -import org.bukkit.scheduler.BukkitRunnable; -import org.bukkit.util.BoundingBox; - -import io.papermc.lib.PaperLib; -import world.bentobox.bentobox.BentoBox; -import world.bentobox.bentobox.api.addons.GameModeAddon; -import world.bentobox.bentobox.database.objects.IslandDeletion; -import world.bentobox.bentobox.hooks.SlimefunHook; -import world.bentobox.bentobox.util.MyBiomeGrid; - -public abstract class SimpleWorldRegenerator implements WorldRegenerator { - private final BentoBox plugin; - - protected SimpleWorldRegenerator() { - this.plugin = BentoBox.getInstance(); - } - - /** - * Update the low-level chunk information for the given block to the new block ID and data. This - * change will not be propagated to clients until the chunk is refreshed to them. - * - * @param chunk - chunk to be changed - * @param x - x coordinate within chunk 0 - 15 - * @param y - y coordinate within chunk 0 - world height, e.g. 255 - * @param z - z coordinate within chunk 0 - 15 - * @param blockData - block data to set the block - * @param applyPhysics - apply physics or not - */ - protected abstract void setBlockInNativeChunk(Chunk chunk, int x, int y, int z, BlockData blockData, boolean applyPhysics); - - @Override - public CompletableFuture regenerate(GameModeAddon gm, IslandDeletion di, World world) { - CompletableFuture bigFuture = new CompletableFuture<>(); - new BukkitRunnable() { - private int chunkX = di.getMinXChunk(); - private int chunkZ = di.getMinZChunk(); - CompletableFuture currentTask = CompletableFuture.completedFuture(null); - - @Override - public void run() { - if (!currentTask.isDone()) return; - if (isEnded(chunkX)) { - cancel(); - bigFuture.complete(null); - return; - } - List> newTasks = new ArrayList<>(); - for (int i = 0; i < plugin.getSettings().getDeleteSpeed(); i++) { - if (isEnded(chunkX)) { - break; - } - final int x = chunkX; - final int z = chunkZ; - - // Only process non-generated chunks - if (world.isChunkGenerated(x, z)) { - newTasks.add(regenerateChunk(gm, di, world, x, z)); - } - - chunkZ++; - if (chunkZ > di.getMaxZChunk()) { - chunkZ = di.getMinZChunk(); - chunkX++; - } - } - currentTask = CompletableFuture.allOf(newTasks.toArray(new CompletableFuture[0])); - } - - private boolean isEnded(int chunkX) { - return chunkX > di.getMaxXChunk(); - } - }.runTaskTimer(plugin, 0L, 20L); - return bigFuture; - } - - @SuppressWarnings("deprecation") - private CompletableFuture regenerateChunk(GameModeAddon gm, IslandDeletion di, World world, int chunkX, int chunkZ) { - CompletableFuture chunkFuture = PaperLib.getChunkAtAsync(world, chunkX, chunkZ); - CompletableFuture invFuture = chunkFuture.thenAccept(chunk -> - Arrays.stream(chunk.getTileEntities()).filter(InventoryHolder.class::isInstance) - .filter(te -> di.inBounds(te.getLocation().getBlockX(), te.getLocation().getBlockZ())) - .forEach(te -> ((InventoryHolder) te).getInventory().clear()) - ); - CompletableFuture entitiesFuture = chunkFuture.thenAccept(chunk -> { - for (Entity e : chunk.getEntities()) { - if (!(e instanceof Player)) { - e.remove(); - } - } - }); - CompletableFuture copyFuture = chunkFuture.thenApply(chunk -> { - // Reset blocks - MyBiomeGrid grid = new MyBiomeGrid(chunk.getWorld().getEnvironment()); - ChunkGenerator cg = gm.getDefaultWorldGenerator(chunk.getWorld().getName(), "delete"); - // Will be null if use-own-generator is set to true - if (cg != null) { - ChunkGenerator.ChunkData cd = cg.generateChunkData(chunk.getWorld(), new Random(), chunk.getX(), chunk.getZ(), grid); - copyChunkDataToChunk(chunk, cd, grid, di.getBox()); - for (BlockPopulator pop : cg.getDefaultPopulators(world)) { - pop.populate(world, new Random(), chunkX, chunkZ, null); - } - } - return chunk; - }); - CompletableFuture postCopyFuture = copyFuture.thenAccept(chunk -> - // Remove all entities in chunk, including any dropped items as a result of clearing the blocks above - Arrays.stream(chunk.getEntities()).filter(e -> !(e instanceof Player) && di.inBounds(e.getLocation().getBlockX(), e.getLocation().getBlockZ())).forEach(Entity::remove)); - return CompletableFuture.allOf(invFuture, entitiesFuture, postCopyFuture); - } - - @SuppressWarnings("deprecation") - private void copyChunkDataToChunk(Chunk chunk, ChunkGenerator.ChunkData chunkData, ChunkGenerator.BiomeGrid biomeGrid, BoundingBox limitBox) { - double baseX = chunk.getX() << 4; - double baseZ = chunk.getZ() << 4; - int minHeight = chunk.getWorld().getMinHeight(); - int maxHeight = chunk.getWorld().getMaxHeight(); - for (int x = 0; x < 16; x++) { - for (int z = 0; z < 16; z++) { - if (!limitBox.contains(baseX + x, 0, baseZ + z)) { - continue; - } - for (int y = minHeight; y < maxHeight; y++) { - setBlockInNativeChunk(chunk, x, y, z, chunkData.getBlockData(x, y, z), false); - // 3D biomes, 4 blocks separated - if (x % 4 == 0 && y % 4 == 0 && z % 4 == 0) { - chunk.getBlock(x, y, z).setBiome(biomeGrid.getBiome(x, y, z)); - } - // Delete any slimefun blocks - Location loc = new Location(chunk.getWorld(), baseX + x, y, baseZ + z); - BentoBox.getInstance().logDebug(loc + " " + plugin.getHooks().getHook("Slimefun").isPresent()); - plugin.getHooks().getHook("Slimefun") - .ifPresent(sf -> ((SlimefunHook) sf).clearBlockInfo(loc, true)); - } - } - } - } -} diff --git a/src/test/java/world/bentobox/bentobox/hooks/ItemsAdderHookTest.java b/src/test/java/world/bentobox/bentobox/hooks/ItemsAdderHookTest.java new file mode 100644 index 000000000..cef05e26d --- /dev/null +++ b/src/test/java/world/bentobox/bentobox/hooks/ItemsAdderHookTest.java @@ -0,0 +1,218 @@ +package world.bentobox.bentobox.hooks; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +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.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.bukkit.event.entity.EntityExplodeEvent; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.PluginManager; +import org.eclipse.jdt.annotation.Nullable; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.stubbing.Answer; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; + +import dev.lone.itemsadder.api.CustomBlock; +import world.bentobox.bentobox.BentoBox; +import world.bentobox.bentobox.api.user.Notifier; +import world.bentobox.bentobox.api.user.User; +import world.bentobox.bentobox.database.objects.Island; +import world.bentobox.bentobox.database.objects.Players; +import world.bentobox.bentobox.hooks.ItemsAdderHook.BlockInteractListener; +import world.bentobox.bentobox.managers.FlagsManager; +import world.bentobox.bentobox.managers.IslandWorldManager; +import world.bentobox.bentobox.managers.IslandsManager; +import world.bentobox.bentobox.managers.LocalesManager; +import world.bentobox.bentobox.managers.PlaceholdersManager; +import world.bentobox.bentobox.managers.PlayersManager; + +/** + * Test class for ItemsAdder hook + */ +@RunWith(PowerMockRunner.class) +@PrepareForTest({ BentoBox.class, Bukkit.class, CustomBlock.class }) +public class ItemsAdderHookTest { + + @Mock + private BentoBox plugin; + private ItemsAdderHook hook; + @Mock + private PluginManager pim; + @Mock + private Plugin itemsAdder; + @Mock + private FlagsManager fm; + @Mock + private Location location; + @Mock + private Player entity; + @Mock + private IslandWorldManager iwm; + @Mock + private World world; + @Mock + private IslandsManager im; + @Mock + private Island island; + @Mock + private PlayersManager pm; + @Mock + private PlaceholdersManager phm; + @Mock + private Notifier notifier; + + /** + * @throws java.lang.Exception + */ + @Before + public void setUp() throws Exception { + // Set up plugin + plugin = mock(BentoBox.class); + Whitebox.setInternalState(BentoBox.class, "instance", plugin); + + // User + UUID uuid = UUID.randomUUID(); + when(entity.getUniqueId()).thenReturn(uuid); + User.setPlugin(plugin); + User.getInstance(entity); + + // Flags Manager + when(plugin.getFlagsManager()).thenReturn(fm); + + // Bukkit + PowerMockito.mockStatic(Bukkit.class, Mockito.RETURNS_MOCKS); + when(Bukkit.getPluginManager()).thenReturn(pim); + when(pim.getPlugin("ItemsAdder")).thenReturn(itemsAdder); + + // IWM + when(plugin.getIWM()).thenReturn(iwm); + when(iwm.inWorld(location)).thenReturn(true); + + // CustomBlock + PowerMockito.mockStatic(CustomBlock.class, Mockito.RETURNS_MOCKS); + + // Location + when(world.getName()).thenReturn("bskyblock"); + when(location.getWorld()).thenReturn(world); + + // Island manager + when(plugin.getIslands()).thenReturn(im); + + when(im.getProtectedIslandAt(location)).thenReturn(Optional.of(island)); + + // Players Manager + when(plugin.getPlayers()).thenReturn(pm); + @Nullable + Players playerObject = new Players(); + playerObject.setUniqueId(uuid.toString()); + when(pm.getPlayer(uuid)).thenReturn(playerObject); + + // Locales + LocalesManager lm = mock(LocalesManager.class); + when(lm.get(any(), any())).thenAnswer((Answer) invocation -> invocation.getArgument(1, String.class)); + when(plugin.getLocalesManager()).thenReturn(lm); + + // Return the same string + when(phm.replacePlaceholders(any(), anyString())) + .thenAnswer((Answer) invocation -> invocation.getArgument(1, String.class)); + when(plugin.getPlaceholdersManager()).thenReturn(phm); + + // Notifier + when(plugin.getNotifier()).thenReturn(notifier); + + hook = new ItemsAdderHook(plugin); + } + + /** + * @throws java.lang.Exception + */ + @After + public void tearDown() throws Exception { + User.clearUsers(); + } + + /** + * Test method for {@link world.bentobox.bentobox.hooks.ItemsAdderHook#hook()}. + */ + @Test + public void testHook() { + assertTrue(hook.hook()); + verify(pim).registerEvents(hook.getListener(), plugin); + verify(fm).registerFlag(ItemsAdderHook.ITEMS_ADDER_EXPLOSIONS); + } + + /** + * Test method for {@link world.bentobox.bentobox.hooks.ItemsAdderHook#hook()}. + */ + @Test + public void testHookFail() { + // No plugin + when(pim.getPlugin("ItemsAdder")).thenReturn(null); + assertFalse(hook.hook()); + verify(pim, never()).registerEvents(any(), any()); + verify(fm, never()).registerFlag(any()); + } + + /** + * Test method for {@link world.bentobox.bentobox.hooks.ItemsAdderHook.BlockInteractListener#onExplosion(EntityExplodeEvent)} + */ + @Test + public void testListener() { + // Make listener + assertTrue(hook.hook()); + BlockInteractListener listener = hook.getListener(); + when(entity.getType()).thenReturn(EntityType.PLAYER); + when(entity.hasPermission("XXXXXX")).thenReturn(true); + List list = new ArrayList<>(); + EntityExplodeEvent event = new EntityExplodeEvent(entity, location, list, 0); + listener.onExplosion(event); + assertTrue(event.isCancelled()); + } + + /** + * Test method for {@link world.bentobox.bentobox.hooks.ItemsAdderHook#ItemsAdderHook(world.bentobox.bentobox.BentoBox)}. + */ + @Test + public void testItemsAdderHook() { + assertNotNull(hook); + assertEquals(Material.NETHER_STAR, hook.getIcon()); + } + + /** + * Test method for {@link world.bentobox.bentobox.hooks.ItemsAdderHook#clearBlockInfo(org.bukkit.Location)}. + */ + @Test + public void testClearBlockInfo() { + hook.clearBlockInfo(location); + PowerMockito.verifyStatic(CustomBlock.class); + CustomBlock.remove(location); + } + +}