feat(chunkloader): add comprehensive configuration for simulation, passive entities, mob cap, and fuel

This commit is contained in:
Marcos Paulo
2026-08-20 17:43:50 -03:00
parent 45f129df56
commit f35e4a5bff
4 changed files with 133 additions and 20 deletions
@@ -36,7 +36,7 @@ final class ChunkLoaders {
private static final String PERM_LIMIT_PREFIX = "canalhandia.chunkloader.limite."; private static final String PERM_LIMIT_PREFIX = "canalhandia.chunkloader.limite.";
private final Plugin plugin; private final Canalhandia plugin;
private final File file; private final File file;
private final List<ChunkLoader> loaders = new ArrayList<>(); private final List<ChunkLoader> loaders = new ArrayList<>();
private final AtomicLong nextId = new AtomicLong(1); private final AtomicLong nextId = new AtomicLong(1);
@@ -48,7 +48,7 @@ final class ChunkLoaders {
return t; return t;
}); });
ChunkLoaders(Plugin plugin, File file) { ChunkLoaders(Canalhandia plugin, File file) {
this.plugin = plugin; this.plugin = plugin;
this.file = file; this.file = file;
load(); load();
@@ -97,11 +97,21 @@ final class ChunkLoaders {
* Registers a new chunk loader and activates the ticket in the world. * Registers a new chunk loader and activates the ticket in the world.
*/ */
ChunkLoader add(String ownerUuid, String ownerName, String world, int x, int y, int z) { ChunkLoader add(String ownerUuid, String ownerName, String world, int x, int y, int z) {
return add(ownerUuid, ownerName, world, x, y, z, "", 0L); return add(ownerUuid, ownerName, world, x, y, z, "", defaultInitialExpiration());
} }
ChunkLoader add(String ownerUuid, String ownerName, String world, int x, int y, int z, String name) { ChunkLoader add(String ownerUuid, String ownerName, String world, int x, int y, int z, String name) {
return add(ownerUuid, ownerName, world, x, y, z, name, 0L); return add(ownerUuid, ownerName, world, x, y, z, name, defaultInitialExpiration());
}
private long defaultInitialExpiration() {
if (plugin != null && plugin.settings() != null && plugin.settings().chunkLoaderRequiresTime()) {
double hours = plugin.settings().chunkLoaderInitialHours();
if (hours > 0) {
return System.currentTimeMillis() + (long) (hours * 3600_000L);
}
}
return 0L;
} }
ChunkLoader add(String ownerUuid, String ownerName, String world, int x, int y, int z, String name, long expiresAt) { ChunkLoader add(String ownerUuid, String ownerName, String world, int x, int y, int z, String name, long expiresAt) {
@@ -417,28 +427,34 @@ final class ChunkLoaders {
} }
Chunk chunk = w.getChunkAt(loader.chunkX(), loader.chunkZ()); Chunk chunk = w.getChunkAt(loader.chunkX(), loader.chunkZ());
// 1. Keep mob spawners (dungeon / blaze / skeleton cages) active boolean spawnersActive = plugin.settings() == null || plugin.settings().chunkLoaderSpawnersActive();
for (BlockState state : chunk.getTileEntities()) { boolean monsterSpawning = plugin.settings() == null || plugin.settings().chunkLoaderMonsterSpawning();
if (state instanceof CreatureSpawner spawner) { int monsterCap = plugin.settings() != null ? plugin.settings().chunkLoaderMonsterCap() : 20;
if (spawner.getRequiredPlayerRange() < 1024) {
spawner.setRequiredPlayerRange(2048); // 1. Keep mob spawners (dungeon / blaze / skeleton cages) active if enabled
spawner.update(true, false); if (spawnersActive) {
for (BlockState state : chunk.getTileEntities()) {
if (state instanceof CreatureSpawner spawner) {
if (spawner.getRequiredPlayerRange() < 1024) {
spawner.setRequiredPlayerRange(2048);
spawner.update(true, false);
}
} }
} }
} }
// 2. Simulate natural mob spawning (dark platforms / slime / nether farms) // 2. Simulate natural mob spawning (dark platforms / slime / nether farms) if enabled
long mobCount = 0; if (monsterSpawning) {
for (Entity entity : chunk.getEntities()) { long mobCount = 0;
if (entity instanceof Mob) { for (Entity entity : chunk.getEntities()) {
mobCount++; if (entity instanceof Mob) {
mobCount++;
}
}
if (mobCount < monsterCap) {
simulateNaturalSpawning(w, loader, chunk);
} }
} }
if (mobCount >= 20) {
continue; // Respect chunk mob cap
}
simulateNaturalSpawning(w, loader, chunk);
} catch (Exception ignored) { } catch (Exception ignored) {
} }
} }
@@ -597,6 +597,54 @@ final class Settings {
set("chunkloader.bluemap", enabled); set("chunkloader.bluemap", enabled);
} }
boolean chunkLoaderSpawnersActive() {
return plugin.getConfig().getBoolean("chunkloader.simulacao.gaiolas-ativas", true);
}
void chunkLoaderSpawnersActive(boolean active) {
set("chunkloader.simulacao.gaiolas-ativas", active);
}
boolean chunkLoaderMonsterSpawning() {
return plugin.getConfig().getBoolean("chunkloader.simulacao.spawn-monstros", true);
}
void chunkLoaderMonsterSpawning(boolean spawning) {
set("chunkloader.simulacao.spawn-monstros", spawning);
}
int chunkLoaderMonsterCap() {
return Math.max(1, plugin.getConfig().getInt("chunkloader.simulacao.limite-monstros-chunk", 20));
}
void chunkLoaderMonsterCap(int cap) {
set("chunkloader.simulacao.limite-monstros-chunk", Math.max(1, cap));
}
boolean chunkLoaderPassiveEntities() {
return plugin.getConfig().getBoolean("chunkloader.simulacao.tick-entidades-passivas", true);
}
void chunkLoaderPassiveEntities(boolean active) {
set("chunkloader.simulacao.tick-entidades-passivas", active);
}
boolean chunkLoaderRequiresTime() {
return plugin.getConfig().getBoolean("chunkloader.combustivel.requer-tempo", true);
}
void chunkLoaderRequiresTime(boolean required) {
set("chunkloader.combustivel.requer-tempo", required);
}
double chunkLoaderInitialHours() {
return Math.max(0.0, plugin.getConfig().getDouble("chunkloader.combustivel.tempo-inicial-horas", 24.0));
}
void chunkLoaderInitialHours(double hours) {
set("chunkloader.combustivel.tempo-inicial-horas", Math.max(0.0, hours));
}
// --- salvavoid ---------------------------------------------------------- // --- salvavoid ----------------------------------------------------------
int voidProtectionRadius() { int voidProtectionRadius() {
+40
View File
@@ -344,3 +344,43 @@ mortes:
- "BONE | Osso da Sorte | Um ossinho pra você, campeão." - "BONE | Osso da Sorte | Um ossinho pra você, campeão."
- "COOKIE | Cookie de Consolação | Cookie de consolação. Vai que melhora." - "COOKIE | Cookie de Consolação | Cookie de consolação. Vai que melhora."
- "ROTTEN_FLESH | Carne Podre | É o que tinha sobrado na despensa." - "ROTTEN_FLESH | Carne Podre | É o que tinha sobrado na despensa."
# --- Âncoras de Chunk (Chunk Loaders) ----------------------------------------
chunkloader:
# Limite máximo de âncoras ativas por jogador por padrão.
# Pode ser alterado por jogador com /chunkloader limite <jogador> <qtd>.
limite-padrao: 1
# Exibe os marcadores de âncoras ativas no BlueMap.
bluemap: true
# Configurações de simulação de entidades na chunk carregada:
simulacao:
# Se true, mantém ativas gaiolas de monstros (spawners de blaze, esqueletos, etc.)
# mesmo sem jogadores por perto.
gaiolas-ativas: true
# Se true, simula o surgimento natural de monstros em plataformas escuras,
# slime chunks e fortalezas do nether. Se false, apenas entidades existentes funcionam.
spawn-monstros: true
# Teto máximo de monstros na chunk antes de pausar o spawn natural (evita acúmulo de lag).
limite-monstros-chunk: 20
# Se true, mantém animais, aldeões, ferro-golems e colheitas sendo processados na chunk.
tick-entidades-passivas: true
# Sistema de combustível e tempo de expiração:
combustivel:
# Se true, âncoras requerem abastecimento com itens e têm tempo de expiração.
# Se false, todas as âncoras colocadas são permanentes por padrão.
requer-tempo: true
# Tempo inicial padrão dado ao criar e colocar uma nova âncora (em horas).
tempo-inicial-horas: 24
# --- Salva-Void (Resgate de Morte no Vazio) -----------------------------------
salvavoid:
# Raio máximo de busca em blocos a partir de onde o jogador caiu para achar
# terreno seguro e colocar o baú com os itens resgatados.
raio-busca: 32
@@ -203,6 +203,15 @@ class ChunkLoaderTest {
assertNull(ChunkLoaders.pickEntityType(org.bukkit.World.Environment.NORMAL, false, 64, 5, 10)); assertNull(ChunkLoaders.pickEntityType(org.bukkit.World.Environment.NORMAL, false, 64, 5, 10));
} }
@Test
void createsLoaderWithInitialExpiration() {
long targetExpiry = System.currentTimeMillis() + 86400_000L;
ChunkLoader loader = loaders.add("uuid-1", "Marcos", "world", 100, 64, 200, "Base", targetExpiry);
assertEquals(targetExpiry, loader.expiresAt());
assertFalse(loader.isExpired());
assertTrue(loader.timeLeft().contains("h"));
}
@Test @Test
void simulationSafelyNoopsWithNullPlugin() { void simulationSafelyNoopsWithNullPlugin() {
loaders.startSimulation(); loaders.startSimulation();