Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bc7a88b9e2 | |||
| 8a34565f3c | |||
| b6e508c8c9 | |||
| cc5c983e41 | |||
| 07e5bb6018 |
+23
-14
@@ -1,21 +1,30 @@
|
|||||||
{
|
{
|
||||||
"languages": [
|
|
||||||
"java",
|
|
||||||
"yaml",
|
|
||||||
"markdown"
|
|
||||||
],
|
|
||||||
"focus": [
|
"focus": [
|
||||||
"thread-safety",
|
"correctness",
|
||||||
"paper-chunk-ticketing",
|
"security",
|
||||||
"item-loss-prevention",
|
"performance"
|
||||||
"resource-cleanup",
|
|
||||||
"performance-and-chunk-loading",
|
|
||||||
"null-safety-and-unit-tests"
|
|
||||||
],
|
],
|
||||||
"exclude_paths": [
|
"exclude_paths": [
|
||||||
"target/**",
|
"target/**",
|
||||||
"*.bak*",
|
"*.class"
|
||||||
"docs/**"
|
|
||||||
],
|
],
|
||||||
"instructions": "Canalhandia is a Minecraft Paper 1.21.x server plugin written in modern Java 25. Enforce these core invariants:\n1. Thread Safety: Bukkit API, World, Entity, and Inventory mutations MUST run on the main server thread. Async threads only do pure calculation or async file I/O.\n2. Item & Inventory Safety: Never discard player items. Always handle full-inventory overflow by dropping excess items at the player's location. On multi-step container placement (e.g. chests), roll back blocks if not all items fit.\n3. Lifecycle & Cleanup: All registered chunk tickets, recipes, schedulers, and I/O executors must be cleanly flushed and unloaded in onDisable() and module toggles.\n4. Chunk Loading: Never trigger synchronous chunk generation or loading inside event handlers. Always check world.isChunkLoaded() before querying blocks.\n5. Test Coverage: All domain logic, coordinates math, parsers, and pure helpers must have corresponding JUnit tests in src/test/java."
|
"languages": [
|
||||||
|
"java"
|
||||||
|
],
|
||||||
|
"style": "balanced",
|
||||||
|
"require_tests": true,
|
||||||
|
"exclude_tests": false,
|
||||||
|
"max_findings": 15,
|
||||||
|
"severity_threshold": "low",
|
||||||
|
"patterns": {
|
||||||
|
"deny": [
|
||||||
|
"**/README.md",
|
||||||
|
"**/*.md"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"cost_target": "claude-sonnet-5",
|
||||||
|
"additional_context_urls": [
|
||||||
|
"http://nexus-service.nexus.svc.cluster.local:8081/repository/raw-hosted/canalhandia/architecture.md"
|
||||||
|
],
|
||||||
|
"instructions": "Minecraft plugin (Paper 26.2, pt-BR, JDK 25 build). Chat-only — never touch gameplay. Watch thread-safety on event handlers (PlayerDeathEvent, PlayerInteractEvent) — the Bukkit main thread is single-threaded but async chunks/events cross it. Avoid main-thread I/O; defer expensive scans (chunk loading, spiral search) to scheduled tasks or async paths. Flag mutable shared state across listener invocations. Hard constraints: chat messages are immutable after send (counts baked into buttons freeze at send time); names go out as translatable components so the singular-form rule applies (number never agrees with the noun); Geyser/Bedrock cannot click and cannot show emoji (every click has a typed fallback); vanilla statistics are the only data source (offline path is <world>/players/stats/<uuid>.json, NOT <world>/stats); reactions keep counting late (reacao-validade-minutos); Floodgate is optional runtime dep. Flag: real bugs, missing persistence of new settings, comando/permission not in plugin.yml, breaking Bedrock equivalent invariant, removing the frozen-at-send assumption, violating singular-form rule, missing Stats.resolve() on renames."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -611,3 +611,4 @@ Statistic constants get renamed between Minecraft releases, so resolve them via
|
|||||||
`Stats.resolve("NEW_NAME", "OLD_NAME")` — a rename then degrades one curiosity
|
`Stats.resolve("NEW_NAME", "OLD_NAME")` — a rename then degrades one curiosity
|
||||||
instead of breaking the whole announcement.
|
instead of breaking the whole announcement.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1975,6 +1975,10 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
|
|||||||
Msg.error(sender, "A âncora " + loader.displayName() + " já está " + (enabled ? "ativa" : "pausada") + ".");
|
Msg.error(sender, "A âncora " + loader.displayName() + " já está " + (enabled ? "ativa" : "pausada") + ".");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (enabled && loader.isExpired()) {
|
||||||
|
Msg.error(sender, "A âncora " + loader.displayName() + " está expirada! Adicione tempo (/chunkloader tempo " + loader.id() + " <horas>) ou abasteça com combustível antes de reativar.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
plugin.chunkLoaders().setEnabled(loader.id(), enabled);
|
plugin.chunkLoaders().setEnabled(loader.id(), enabled);
|
||||||
plugin.blueMap().syncChunkLoaders();
|
plugin.blueMap().syncChunkLoaders();
|
||||||
Msg.ok(sender, "Âncora " + loader.displayName() + " " + (enabled ? "ATIVADA." : "PAUSADA/DESATIVADA."));
|
Msg.ok(sender, "Âncora " + loader.displayName() + " " + (enabled ? "ATIVADA." : "PAUSADA/DESATIVADA."));
|
||||||
@@ -1998,16 +2002,20 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
double hours = Double.parseDouble(args[1]);
|
double hours = Double.parseDouble(args[1]);
|
||||||
if (hours <= 0 && isAdmin) {
|
if (hours <= 0) {
|
||||||
plugin.chunkLoaders().addTime(loader.id(), -loader.expiresAt()); // permanent
|
if (isAdmin) {
|
||||||
|
plugin.chunkLoaders().setExpiresAt(loader.id(), 0L); // permanent
|
||||||
Msg.ok(sender, "Âncora " + loader.displayName() + " definida como PERMANENTE.");
|
Msg.ok(sender, "Âncora " + loader.displayName() + " definida como PERMANENTE.");
|
||||||
} else {
|
} else {
|
||||||
|
Msg.error(sender, "A quantidade de horas deve ser maior que zero.");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
long millis = (long) (hours * 3600_000L);
|
long millis = (long) (hours * 3600_000L);
|
||||||
plugin.chunkLoaders().addTime(loader.id(), millis);
|
plugin.chunkLoaders().addTime(loader.id(), millis);
|
||||||
ChunkLoader updated = plugin.chunkLoaders().byId(loader.id());
|
ChunkLoader updated = plugin.chunkLoaders().byId(loader.id());
|
||||||
String time = (updated != null) ? updated.timeLeft() : "Permanente";
|
String time = (updated != null) ? updated.timeLeft() : "Permanente";
|
||||||
Msg.ok(sender, "Adicionado " + hours + "h à âncora " + loader.displayName() + ". Tempo restante: " + time);
|
Msg.ok(sender, "Adicionado " + hours + "h à âncora " + loader.displayName() + ". Tempo restante: " + time);
|
||||||
}
|
|
||||||
plugin.blueMap().syncChunkLoaders();
|
plugin.blueMap().syncChunkLoaders();
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
Msg.error(sender, "Horas inválidas: " + args[1]);
|
Msg.error(sender, "Horas inválidas: " + args[1]);
|
||||||
|
|||||||
@@ -209,6 +209,10 @@ final class ChunkLoaderListener implements Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (player.isSneaking() && (isOwner || isAdmin)) {
|
if (player.isSneaking() && (isOwner || isAdmin)) {
|
||||||
|
if (!loader.enabled() && loader.isExpired()) {
|
||||||
|
Msg.error(player, "A âncora " + loader.displayName() + " está expirada! Clique com combustível (Pérola do End, Blaze, Diamante, etc.) ou adicione tempo antes de reativar.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
boolean newState = !loader.enabled();
|
boolean newState = !loader.enabled();
|
||||||
plugin.chunkLoaders().setEnabled(loader.id(), newState);
|
plugin.chunkLoaders().setEnabled(loader.id(), newState);
|
||||||
Location l = block.getLocation().add(0.5, 0.5, 0.5);
|
Location l = block.getLocation().add(0.5, 0.5, 0.5);
|
||||||
|
|||||||
@@ -144,6 +144,29 @@ final class ChunkLoaders {
|
|||||||
return false;
|
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.
|
* Renames an existing chunk loader.
|
||||||
*/
|
*/
|
||||||
@@ -191,21 +214,24 @@ final class ChunkLoaders {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds a chunk loader by its numeric ID (with or without '#') or custom name.
|
* Finds a chunk loader by its numeric ID (with '#' prefix or plain ID) or custom name.
|
||||||
|
* Custom names take precedence over raw numeric IDs to avoid shadowing named loaders.
|
||||||
*/
|
*/
|
||||||
ChunkLoader find(String query) {
|
ChunkLoader find(String query) {
|
||||||
if (query == null || query.isBlank()) {
|
if (query == null || query.isBlank()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
String trimmed = query.trim();
|
String trimmed = query.trim();
|
||||||
|
if (trimmed.startsWith("#")) {
|
||||||
try {
|
try {
|
||||||
long id = Long.parseLong(trimmed.startsWith("#") ? trimmed.substring(1) : trimmed);
|
long id = Long.parseLong(trimmed.substring(1));
|
||||||
ChunkLoader loader = byId(id);
|
ChunkLoader loader = byId(id);
|
||||||
if (loader != null) {
|
if (loader != null) {
|
||||||
return loader;
|
return loader;
|
||||||
}
|
}
|
||||||
} catch (NumberFormatException ignored) {
|
} catch (NumberFormatException ignored) {
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
synchronized (loaders) {
|
synchronized (loaders) {
|
||||||
for (ChunkLoader loader : loaders) {
|
for (ChunkLoader loader : loaders) {
|
||||||
@@ -214,6 +240,15 @@ final class ChunkLoaders {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
long id = Long.parseLong(trimmed);
|
||||||
|
ChunkLoader loader = byId(id);
|
||||||
|
if (loader != null) {
|
||||||
|
return loader;
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,15 +482,21 @@ final class ChunkLoaders {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static EntityType pickEntityType(World w, Block space, int light) {
|
static EntityType pickEntityType(World w, Block space, int light) {
|
||||||
World.Environment env = w.getEnvironment();
|
boolean isSlime = false;
|
||||||
ThreadLocalRandom rnd = ThreadLocalRandom.current();
|
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 (env == World.Environment.NETHER) {
|
||||||
if (light > 11) return null;
|
if (light > 11) return null;
|
||||||
int roll = rnd.nextInt(100);
|
int r = Math.floorMod(roll, 100);
|
||||||
if (roll < 45) return EntityType.ZOMBIFIED_PIGLIN;
|
if (r < 45) return EntityType.ZOMBIFIED_PIGLIN;
|
||||||
if (roll < 65) return EntityType.WITHER_SKELETON;
|
if (r < 65) return EntityType.WITHER_SKELETON;
|
||||||
if (roll < 85) return EntityType.BLAZE;
|
if (r < 85) return EntityType.BLAZE;
|
||||||
return EntityType.MAGMA_CUBE;
|
return EntityType.MAGMA_CUBE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -463,8 +504,7 @@ final class ChunkLoaders {
|
|||||||
return EntityType.ENDERMAN;
|
return EntityType.ENDERMAN;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isSlimeChunk = space.getChunk().isSlimeChunk() && space.getY() < 40;
|
if (isSlimeChunk && y < 40 && light <= 7 && roll % 3 == 0) {
|
||||||
if (isSlimeChunk && light <= 7 && rnd.nextInt(3) == 0) {
|
|
||||||
return EntityType.SLIME;
|
return EntityType.SLIME;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -472,11 +512,11 @@ final class ChunkLoaders {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
int roll = rnd.nextInt(100);
|
int r = Math.floorMod(roll, 100);
|
||||||
if (roll < 35) return EntityType.ZOMBIE;
|
if (r < 35) return EntityType.ZOMBIE;
|
||||||
if (roll < 65) return EntityType.SKELETON;
|
if (r < 65) return EntityType.SKELETON;
|
||||||
if (roll < 85) return EntityType.CREEPER;
|
if (r < 85) return EntityType.CREEPER;
|
||||||
if (roll < 95) return EntityType.SPIDER;
|
if (r < 95) return EntityType.SPIDER;
|
||||||
return EntityType.WITCH;
|
return EntityType.WITCH;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -503,6 +543,15 @@ final class ChunkLoaders {
|
|||||||
if (w != null) {
|
if (w != null) {
|
||||||
w.removePluginChunkTicket(loader.chunkX(), loader.chunkZ(), plugin);
|
w.removePluginChunkTicket(loader.chunkX(), loader.chunkZ(), plugin);
|
||||||
w.setChunkForceLoaded(loader.chunkX(), loader.chunkZ(), false);
|
w.setChunkForceLoaded(loader.chunkX(), loader.chunkZ(), false);
|
||||||
|
if (w.isChunkLoaded(loader.chunkX(), loader.chunkZ())) {
|
||||||
|
Chunk chunk = w.getChunkAt(loader.chunkX(), loader.chunkZ());
|
||||||
|
for (BlockState state : chunk.getTileEntities()) {
|
||||||
|
if (state instanceof CreatureSpawner spawner && spawner.getRequiredPlayerRange() > 16) {
|
||||||
|
spawner.setRequiredPlayerRange(16);
|
||||||
|
spawner.update(true, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception ignored) {
|
} catch (Exception ignored) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,27 +108,63 @@ public final class VoidProtection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static boolean isSafeGround(Block block) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
Material mat = block.getType();
|
if (mat == Material.LAVA || mat == Material.WATER || mat == Material.FIRE || mat == Material.SOUL_FIRE
|
||||||
if (mat.isAir() || !mat.isSolid()) {
|
|| mat == Material.CACTUS || mat == Material.MAGMA_BLOCK || mat == Material.SWEET_BERRY_BUSH
|
||||||
|
|| mat == Material.WITHER_ROSE || mat == Material.POWDER_SNOW) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return mat != Material.LAVA && mat != Material.FIRE && mat != Material.SOUL_FIRE
|
try {
|
||||||
&& mat != Material.CACTUS && mat != Material.MAGMA_BLOCK && mat != Material.SWEET_BERRY_BUSH
|
return mat.isSolid();
|
||||||
&& mat != Material.WITHER_ROSE && mat != Material.POWDER_SNOW;
|
} catch (Exception | LinkageError e) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static boolean isReplaceable(Block block) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
Material mat = block.getType();
|
return mat == Material.AIR || mat == Material.CAVE_AIR || mat == Material.VOID_AIR
|
||||||
return mat.isAir() || mat == Material.SHORT_GRASS || mat == Material.TALL_GRASS
|
|| mat == Material.SHORT_GRASS || mat == Material.TALL_GRASS
|
||||||
|| mat == Material.SNOW || mat == Material.FERN || mat == Material.LARGE_FERN;
|
|| mat == Material.SNOW || mat == Material.FERN || mat == Material.LARGE_FERN;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the estimated number of inventory slots needed to store the given items.
|
||||||
|
*/
|
||||||
|
public static int calculateRequiredSlots(List<ItemStack> items) {
|
||||||
|
if (items == null || items.isEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int slots = 0;
|
||||||
|
for (ItemStack item : items) {
|
||||||
|
if (item != null && item.getType() != Material.AIR && item.getAmount() > 0) {
|
||||||
|
int maxStack = Math.max(1, item.getMaxStackSize());
|
||||||
|
slots += (int) Math.ceil((double) item.getAmount() / maxStack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return slots;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean canFitInSingleChest(List<ItemStack> items) {
|
||||||
|
return calculateRequiredSlots(items) <= 27;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean canFitInDoubleChest(List<ItemStack> items) {
|
||||||
|
return calculateRequiredSlots(items) <= 54;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores items into a chest (and an adjacent chest if needed) at the target location.
|
* Stores items into a chest (and an adjacent chest if needed) at the target location.
|
||||||
* All items must be stored without overflow; on any failure, blocks are rolled back
|
* All items must be stored without overflow; on any failure, blocks are rolled back
|
||||||
|
|||||||
@@ -122,6 +122,12 @@ class ChunkLoaderTest {
|
|||||||
assertEquals(renamed, loaders.find("#1"));
|
assertEquals(renamed, loaders.find("#1"));
|
||||||
assertEquals(renamed, loaders.find("1"));
|
assertEquals(renamed, loaders.find("1"));
|
||||||
|
|
||||||
|
// Loader 2 named "1" should be prioritized over ID 1 when querying by plain "1"
|
||||||
|
ChunkLoader loader2 = loaders.add("uuid-marcos", "Marcos", "world", 200, 64, 300, "1", 0L);
|
||||||
|
assertEquals(loader2, loaders.find("1"));
|
||||||
|
assertEquals(renamed, loaders.find("#1"));
|
||||||
|
assertEquals(loader2, loaders.find("#2"));
|
||||||
|
|
||||||
assertTrue(loaders.setEnabled(loader.id(), false));
|
assertTrue(loaders.setEnabled(loader.id(), false));
|
||||||
ChunkLoader disabled = loaders.byId(loader.id());
|
ChunkLoader disabled = loaders.byId(loader.id());
|
||||||
assertNotNull(disabled);
|
assertNotNull(disabled);
|
||||||
@@ -151,6 +157,50 @@ class ChunkLoaderTest {
|
|||||||
ChunkLoader expired = timed.withExpiresAt(System.currentTimeMillis() - 1000L);
|
ChunkLoader expired = timed.withExpiresAt(System.currentTimeMillis() - 1000L);
|
||||||
assertTrue(expired.isExpired());
|
assertTrue(expired.isExpired());
|
||||||
assertEquals("Expirado", expired.timeLeft());
|
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
|
@Test
|
||||||
|
|||||||
@@ -32,9 +32,53 @@ class VoidProtectionTest {
|
|||||||
assertFalse(VoidProtection.rescueToChest(List.of(), null));
|
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
|
@Test
|
||||||
void nullBlocksAreNeitherSafeNorReplaceable() {
|
void nullBlocksAreNeitherSafeNorReplaceable() {
|
||||||
assertFalse(VoidProtection.isSafeGround(null));
|
assertFalse(VoidProtection.isSafeGround(null));
|
||||||
assertFalse(VoidProtection.isReplaceable(null));
|
assertFalse(VoidProtection.isReplaceable(null));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void calculatesRequiredSlotsAndChestFit() {
|
||||||
|
assertEquals(0, VoidProtection.calculateRequiredSlots(null));
|
||||||
|
assertEquals(0, VoidProtection.calculateRequiredSlots(List.of()));
|
||||||
|
assertTrue(VoidProtection.canFitInSingleChest(List.of()));
|
||||||
|
assertTrue(VoidProtection.canFitInDoubleChest(List.of()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user