diff --git a/src/main/java/dev/marcospaulo/canalhandia/Canalhandia.java b/src/main/java/dev/marcospaulo/canalhandia/Canalhandia.java index 9e6d8bc..34a5aa8 100644 --- a/src/main/java/dev/marcospaulo/canalhandia/Canalhandia.java +++ b/src/main/java/dev/marcospaulo/canalhandia/Canalhandia.java @@ -868,6 +868,8 @@ public final class Canalhandia extends JavaPlugin implements Listener { int x = safeLoc.getBlockX(); int y = safeLoc.getBlockY(); int z = safeLoc.getBlockZ(); + pendingDeathCoords.put(player.getUniqueId(), new DeathCoords( + x + " " + y + " " + z + " (" + safeLoc.getWorld().getName() + " - Baú do Vácuo)", false)); getServer().getScheduler().runTaskLater(this, () -> { if (player.isOnline()) { player.sendMessage(Component.text("[Canalhandia] ", NamedTextColor.GOLD) @@ -884,6 +886,9 @@ public final class Canalhandia extends JavaPlugin implements Listener { event.setKeepLevel(true); event.setDroppedExp(0); } + pendingDeathCoords.put(player.getUniqueId(), new DeathCoords( + player.getLocation().getBlockX() + " " + player.getLocation().getBlockY() + " " + player.getLocation().getBlockZ() + + " (" + player.getWorld().getName() + ")", true)); getServer().getScheduler().runTaskLater(this, () -> { if (player.isOnline()) { player.sendMessage(Component.text("[Canalhandia] ", NamedTextColor.GOLD) @@ -905,7 +910,7 @@ public final class Canalhandia extends JavaPlugin implements Listener { * {@code [F]} mourning row and never touches {@code deathMessage}, so both * fire on the same event without conflict. */ - @EventHandler + @EventHandler(priority = EventPriority.MONITOR) public void onDeathComic(PlayerDeathEvent event) { if (!settings.moduleEnabled(Module.MORTES)) { return; diff --git a/src/main/java/dev/marcospaulo/canalhandia/CanalhandiaCommand.java b/src/main/java/dev/marcospaulo/canalhandia/CanalhandiaCommand.java index 2ac3f79..67c719f 100644 --- a/src/main/java/dev/marcospaulo/canalhandia/CanalhandiaCommand.java +++ b/src/main/java/dev/marcospaulo/canalhandia/CanalhandiaCommand.java @@ -1999,7 +1999,7 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter { try { double hours = Double.parseDouble(args[1]); if (hours <= 0 && isAdmin) { - plugin.chunkLoaders().addTime(loader.id(), -loader.expiresAt()); // permanent + plugin.chunkLoaders().setExpiresAt(loader.id(), 0L); // permanent Msg.ok(sender, "Âncora " + loader.displayName() + " definida como PERMANENTE."); } else { long millis = (long) (hours * 3600_000L); @@ -2202,7 +2202,7 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter { } try { int newLimit = Math.max(0, Integer.parseInt(args[1])); - Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "lp user " + target.getName() + " permission set canalhandia.chunkloader.limite." + newLimit); + Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "lp user " + target.getUniqueId() + " permission set canalhandia.chunkloader.limite." + newLimit); Msg.ok(sender, "Limite de " + target.getName() + " definido para " + newLimit + " âncoras."); } catch (NumberFormatException e) { Msg.error(sender, "Quantidade inválida: " + args[1]); diff --git a/src/main/java/dev/marcospaulo/canalhandia/ChunkLoaderListener.java b/src/main/java/dev/marcospaulo/canalhandia/ChunkLoaderListener.java index b729f2e..633e73f 100644 --- a/src/main/java/dev/marcospaulo/canalhandia/ChunkLoaderListener.java +++ b/src/main/java/dev/marcospaulo/canalhandia/ChunkLoaderListener.java @@ -300,13 +300,17 @@ final class ChunkLoaderListener implements Listener { if (!plugin.settings().moduleEnabled(Module.CHUNKLOADER)) { return; } + CreatureSpawnEvent.SpawnReason reason = event.getSpawnReason(); + if (reason != CreatureSpawnEvent.SpawnReason.SPAWNER && reason != CreatureSpawnEvent.SpawnReason.CUSTOM) { + return; + } Location loc = event.getLocation(); World w = loc.getWorld(); if (w == null) { return; } ChunkLoader loader = plugin.chunkLoaders().byChunk(w.getName(), loc.getBlockX() >> 4, loc.getBlockZ() >> 4); - if (loader != null && event.getEntity() instanceof Mob mob) { + if (loader != null && loader.enabled() && event.getEntity() instanceof Mob mob) { mob.setRemoveWhenFarAway(false); } } diff --git a/src/main/java/dev/marcospaulo/canalhandia/ChunkLoaders.java b/src/main/java/dev/marcospaulo/canalhandia/ChunkLoaders.java index 4d5b380..f50504b 100644 --- a/src/main/java/dev/marcospaulo/canalhandia/ChunkLoaders.java +++ b/src/main/java/dev/marcospaulo/canalhandia/ChunkLoaders.java @@ -144,6 +144,29 @@ final class ChunkLoaders { return false; } + /** + * Sets a specific expiration timestamp (or 0 for permanent) for a chunk loader. + */ + boolean setExpiresAt(long id, long expiresAt) { + ChunkLoader updated = null; + synchronized (loaders) { + for (int i = 0; i < loaders.size(); i++) { + ChunkLoader curr = loaders.get(i); + if (curr.id() == id) { + updated = curr.withExpiresAt(expiresAt).withEnabled(true); + loaders.set(i, updated); + break; + } + } + } + if (updated != null) { + addTicket(updated); + save(); + return true; + } + return false; + } + /** * Renames an existing chunk loader. */ @@ -447,15 +470,21 @@ final class ChunkLoaders { } static EntityType pickEntityType(World w, Block space, int light) { - World.Environment env = w.getEnvironment(); - ThreadLocalRandom rnd = ThreadLocalRandom.current(); + boolean isSlime = false; + try { + isSlime = space.getChunk().isSlimeChunk(); + } catch (Exception ignored) { + } + return pickEntityType(w.getEnvironment(), isSlime, space.getY(), light, ThreadLocalRandom.current().nextInt(100)); + } + static EntityType pickEntityType(World.Environment env, boolean isSlimeChunk, int y, int light, int roll) { if (env == World.Environment.NETHER) { if (light > 11) return null; - int roll = rnd.nextInt(100); - if (roll < 45) return EntityType.ZOMBIFIED_PIGLIN; - if (roll < 65) return EntityType.WITHER_SKELETON; - if (roll < 85) return EntityType.BLAZE; + int r = Math.floorMod(roll, 100); + if (r < 45) return EntityType.ZOMBIFIED_PIGLIN; + if (r < 65) return EntityType.WITHER_SKELETON; + if (r < 85) return EntityType.BLAZE; return EntityType.MAGMA_CUBE; } @@ -463,8 +492,7 @@ final class ChunkLoaders { return EntityType.ENDERMAN; } - boolean isSlimeChunk = space.getChunk().isSlimeChunk() && space.getY() < 40; - if (isSlimeChunk && light <= 7 && rnd.nextInt(3) == 0) { + if (isSlimeChunk && y < 40 && light <= 7 && roll % 3 == 0) { return EntityType.SLIME; } @@ -472,11 +500,11 @@ final class ChunkLoaders { return null; } - int roll = rnd.nextInt(100); - if (roll < 35) return EntityType.ZOMBIE; - if (roll < 65) return EntityType.SKELETON; - if (roll < 85) return EntityType.CREEPER; - if (roll < 95) return EntityType.SPIDER; + int r = Math.floorMod(roll, 100); + if (r < 35) return EntityType.ZOMBIE; + if (r < 65) return EntityType.SKELETON; + if (r < 85) return EntityType.CREEPER; + if (r < 95) return EntityType.SPIDER; return EntityType.WITCH; } diff --git a/src/main/java/dev/marcospaulo/canalhandia/VoidProtection.java b/src/main/java/dev/marcospaulo/canalhandia/VoidProtection.java index 6191934..9a40adf 100644 --- a/src/main/java/dev/marcospaulo/canalhandia/VoidProtection.java +++ b/src/main/java/dev/marcospaulo/canalhandia/VoidProtection.java @@ -108,24 +108,35 @@ public final class VoidProtection { } static boolean isSafeGround(Block block) { - if (block == null) { + return block != null && isSafeGroundMaterial(block.getType()); + } + + public static boolean isSafeGroundMaterial(Material mat) { + if (mat == null || mat == Material.AIR || mat == Material.CAVE_AIR || mat == Material.VOID_AIR) { return false; } - Material mat = block.getType(); - if (mat.isAir() || !mat.isSolid()) { + if (mat == Material.LAVA || mat == Material.WATER || mat == Material.FIRE || mat == Material.SOUL_FIRE + || mat == Material.CACTUS || mat == Material.MAGMA_BLOCK || mat == Material.SWEET_BERRY_BUSH + || mat == Material.WITHER_ROSE || mat == Material.POWDER_SNOW) { return false; } - return mat != Material.LAVA && mat != Material.FIRE && mat != Material.SOUL_FIRE - && mat != Material.CACTUS && mat != Material.MAGMA_BLOCK && mat != Material.SWEET_BERRY_BUSH - && mat != Material.WITHER_ROSE && mat != Material.POWDER_SNOW; + try { + return mat.isSolid(); + } catch (Exception | LinkageError e) { + return true; + } } static boolean isReplaceable(Block block) { - if (block == null) { + return block != null && isReplaceableMaterial(block.getType()); + } + + public static boolean isReplaceableMaterial(Material mat) { + if (mat == null) { return false; } - Material mat = block.getType(); - return mat.isAir() || mat == Material.SHORT_GRASS || mat == Material.TALL_GRASS + return mat == Material.AIR || mat == Material.CAVE_AIR || mat == Material.VOID_AIR + || mat == Material.SHORT_GRASS || mat == Material.TALL_GRASS || mat == Material.SNOW || mat == Material.FERN || mat == Material.LARGE_FERN; } diff --git a/src/test/java/dev/marcospaulo/canalhandia/ChunkLoaderTest.java b/src/test/java/dev/marcospaulo/canalhandia/ChunkLoaderTest.java index 3b812c8..2b6c226 100644 --- a/src/test/java/dev/marcospaulo/canalhandia/ChunkLoaderTest.java +++ b/src/test/java/dev/marcospaulo/canalhandia/ChunkLoaderTest.java @@ -151,6 +151,50 @@ class ChunkLoaderTest { ChunkLoader expired = timed.withExpiresAt(System.currentTimeMillis() - 1000L); assertTrue(expired.isExpired()); assertEquals("Expirado", expired.timeLeft()); + + assertTrue(loaders.setExpiresAt(loader.id(), 0L)); + ChunkLoader permanent = loaders.byId(loader.id()); + assertNotNull(permanent); + assertEquals(0L, permanent.expiresAt()); + assertEquals("Permanente", permanent.timeLeft()); + assertFalse(permanent.isExpired()); + } + + @Test + void pickEntityTypeReturnsExpectedEntitiesByEnvironment() { + // Nether + assertEquals(org.bukkit.entity.EntityType.ZOMBIFIED_PIGLIN, + ChunkLoaders.pickEntityType(org.bukkit.World.Environment.NETHER, false, 64, 0, 10)); + assertEquals(org.bukkit.entity.EntityType.WITHER_SKELETON, + ChunkLoaders.pickEntityType(org.bukkit.World.Environment.NETHER, false, 64, 0, 50)); + assertEquals(org.bukkit.entity.EntityType.BLAZE, + ChunkLoaders.pickEntityType(org.bukkit.World.Environment.NETHER, false, 64, 0, 75)); + assertEquals(org.bukkit.entity.EntityType.MAGMA_CUBE, + ChunkLoaders.pickEntityType(org.bukkit.World.Environment.NETHER, false, 64, 0, 90)); + assertNull(ChunkLoaders.pickEntityType(org.bukkit.World.Environment.NETHER, false, 64, 12, 10)); + + // End + assertEquals(org.bukkit.entity.EntityType.ENDERMAN, + ChunkLoaders.pickEntityType(org.bukkit.World.Environment.THE_END, false, 64, 15, 50)); + + // Overworld Slime Chunk + assertEquals(org.bukkit.entity.EntityType.SLIME, + ChunkLoaders.pickEntityType(org.bukkit.World.Environment.NORMAL, true, 30, 5, 0)); + + // Overworld Normal Darkness + assertEquals(org.bukkit.entity.EntityType.ZOMBIE, + ChunkLoaders.pickEntityType(org.bukkit.World.Environment.NORMAL, false, 64, 0, 10)); + assertEquals(org.bukkit.entity.EntityType.SKELETON, + ChunkLoaders.pickEntityType(org.bukkit.World.Environment.NORMAL, false, 64, 0, 45)); + assertEquals(org.bukkit.entity.EntityType.CREEPER, + ChunkLoaders.pickEntityType(org.bukkit.World.Environment.NORMAL, false, 64, 0, 75)); + assertEquals(org.bukkit.entity.EntityType.SPIDER, + ChunkLoaders.pickEntityType(org.bukkit.World.Environment.NORMAL, false, 64, 0, 90)); + assertEquals(org.bukkit.entity.EntityType.WITCH, + ChunkLoaders.pickEntityType(org.bukkit.World.Environment.NORMAL, false, 64, 0, 99)); + + // Overworld in Light -> Null + assertNull(ChunkLoaders.pickEntityType(org.bukkit.World.Environment.NORMAL, false, 64, 5, 10)); } @Test diff --git a/src/test/java/dev/marcospaulo/canalhandia/VoidProtectionTest.java b/src/test/java/dev/marcospaulo/canalhandia/VoidProtectionTest.java index 2c0bfdb..50678b5 100644 --- a/src/test/java/dev/marcospaulo/canalhandia/VoidProtectionTest.java +++ b/src/test/java/dev/marcospaulo/canalhandia/VoidProtectionTest.java @@ -32,6 +32,42 @@ class VoidProtectionTest { assertFalse(VoidProtection.rescueToChest(List.of(), null)); } + @Test + void validatesSafeGroundMaterials() { + assertTrue(VoidProtection.isSafeGroundMaterial(org.bukkit.Material.STONE)); + assertTrue(VoidProtection.isSafeGroundMaterial(org.bukkit.Material.GRASS_BLOCK)); + assertTrue(VoidProtection.isSafeGroundMaterial(org.bukkit.Material.DIRT)); + assertTrue(VoidProtection.isSafeGroundMaterial(org.bukkit.Material.OBSIDIAN)); + + assertFalse(VoidProtection.isSafeGroundMaterial(null)); + assertFalse(VoidProtection.isSafeGroundMaterial(org.bukkit.Material.AIR)); + assertFalse(VoidProtection.isSafeGroundMaterial(org.bukkit.Material.LAVA)); + assertFalse(VoidProtection.isSafeGroundMaterial(org.bukkit.Material.FIRE)); + assertFalse(VoidProtection.isSafeGroundMaterial(org.bukkit.Material.SOUL_FIRE)); + assertFalse(VoidProtection.isSafeGroundMaterial(org.bukkit.Material.CACTUS)); + assertFalse(VoidProtection.isSafeGroundMaterial(org.bukkit.Material.MAGMA_BLOCK)); + assertFalse(VoidProtection.isSafeGroundMaterial(org.bukkit.Material.SWEET_BERRY_BUSH)); + assertFalse(VoidProtection.isSafeGroundMaterial(org.bukkit.Material.WITHER_ROSE)); + assertFalse(VoidProtection.isSafeGroundMaterial(org.bukkit.Material.POWDER_SNOW)); + } + + @Test + void validatesReplaceableMaterials() { + assertTrue(VoidProtection.isReplaceableMaterial(org.bukkit.Material.AIR)); + assertTrue(VoidProtection.isReplaceableMaterial(org.bukkit.Material.CAVE_AIR)); + assertTrue(VoidProtection.isReplaceableMaterial(org.bukkit.Material.VOID_AIR)); + assertTrue(VoidProtection.isReplaceableMaterial(org.bukkit.Material.SHORT_GRASS)); + assertTrue(VoidProtection.isReplaceableMaterial(org.bukkit.Material.TALL_GRASS)); + assertTrue(VoidProtection.isReplaceableMaterial(org.bukkit.Material.SNOW)); + assertTrue(VoidProtection.isReplaceableMaterial(org.bukkit.Material.FERN)); + assertTrue(VoidProtection.isReplaceableMaterial(org.bukkit.Material.LARGE_FERN)); + + assertFalse(VoidProtection.isReplaceableMaterial(null)); + assertFalse(VoidProtection.isReplaceableMaterial(org.bukkit.Material.STONE)); + assertFalse(VoidProtection.isReplaceableMaterial(org.bukkit.Material.CHEST)); + assertFalse(VoidProtection.isReplaceableMaterial(org.bukkit.Material.OBSIDIAN)); + } + @Test void nullBlocksAreNeitherSafeNorReplaceable() { assertFalse(VoidProtection.isSafeGround(null));