fix(pr-reviews): address spawner reset, numeric name resolution, and activation validation
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -191,21 +191,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 +217,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -503,6 +515,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) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,6 +129,31 @@ public final class VoidProtection {
|
|||||||
|| 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);
|
||||||
|
|||||||
@@ -37,4 +37,12 @@ class VoidProtectionTest {
|
|||||||
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