feat(chunkloader): add mob spawning simulation, spawner activation and despawn protection
This commit is contained in:
@@ -131,6 +131,7 @@ public final class Canalhandia extends JavaPlugin implements Listener {
|
||||
chunkLoaders = new ChunkLoaders(this, new java.io.File(getDataFolder(), "chunks.yml"));
|
||||
if (settings.moduleEnabled(Module.CHUNKLOADER)) {
|
||||
chunkLoaders.loadAllTickets();
|
||||
chunkLoaders.startSimulation();
|
||||
ChunkAnchorItem.registerRecipe(this);
|
||||
}
|
||||
aiBudget = new Budget(settings.aiSpontaneousPerDay(),
|
||||
@@ -192,6 +193,7 @@ public final class Canalhandia extends JavaPlugin implements Listener {
|
||||
poll.hide();
|
||||
}
|
||||
if (chunkLoaders != null) {
|
||||
chunkLoaders.stopSimulation();
|
||||
chunkLoaders.unloadAllTickets();
|
||||
chunkLoaders.close();
|
||||
ChunkAnchorItem.unregisterRecipe(this);
|
||||
|
||||
@@ -570,8 +570,10 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
|
||||
if (module == Module.CHUNKLOADER) {
|
||||
if (on) {
|
||||
plugin.chunkLoaders().loadAllTickets();
|
||||
plugin.chunkLoaders().startSimulation();
|
||||
ChunkAnchorItem.registerRecipe(plugin);
|
||||
} else {
|
||||
plugin.chunkLoaders().stopSimulation();
|
||||
plugin.chunkLoaders().unloadAllTickets();
|
||||
ChunkAnchorItem.unregisterRecipe(plugin);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Particle;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.entity.Mob;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
@@ -16,6 +18,7 @@ import org.bukkit.event.block.BlockExplodeEvent;
|
||||
import org.bukkit.event.block.BlockPistonExtendEvent;
|
||||
import org.bukkit.event.block.BlockPistonRetractEvent;
|
||||
import org.bukkit.event.block.BlockPlaceEvent;
|
||||
import org.bukkit.event.entity.CreatureSpawnEvent;
|
||||
import org.bukkit.event.entity.EntityExplodeEvent;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
@@ -211,4 +214,20 @@ final class ChunkLoaderListener implements Listener {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
|
||||
public void onCreatureSpawn(CreatureSpawnEvent event) {
|
||||
if (!plugin.settings().moduleEnabled(Module.CHUNKLOADER)) {
|
||||
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) {
|
||||
mob.setRemoveWhenFarAway(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
package dev.marcospaulo.canalhandia;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockState;
|
||||
import org.bukkit.block.CreatureSpawner;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.entity.Mob;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.entity.CreatureSpawnEvent;
|
||||
import org.bukkit.permissions.PermissionAttachmentInfo;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
@@ -15,6 +25,7 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
@@ -29,6 +40,7 @@ final class ChunkLoaders {
|
||||
private final File file;
|
||||
private final List<ChunkLoader> loaders = new ArrayList<>();
|
||||
private final AtomicLong nextId = new AtomicLong(1);
|
||||
private BukkitTask simulationTask;
|
||||
|
||||
private final ExecutorService io = Executors.newSingleThreadExecutor(r -> {
|
||||
Thread t = new Thread(r, "canalhandia-chunkloaders-io");
|
||||
@@ -220,6 +232,135 @@ final class ChunkLoaders {
|
||||
}
|
||||
}
|
||||
|
||||
void startSimulation() {
|
||||
if (plugin == null || simulationTask != null) {
|
||||
return;
|
||||
}
|
||||
simulationTask = Bukkit.getScheduler().runTaskTimer(plugin, this::tickMobSimulation, 20L, 20L);
|
||||
}
|
||||
|
||||
void stopSimulation() {
|
||||
if (simulationTask != null) {
|
||||
simulationTask.cancel();
|
||||
simulationTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
void tickMobSimulation() {
|
||||
if (plugin == null) {
|
||||
return;
|
||||
}
|
||||
List<ChunkLoader> currentLoaders;
|
||||
synchronized (loaders) {
|
||||
currentLoaders = new ArrayList<>(loaders);
|
||||
}
|
||||
|
||||
for (ChunkLoader loader : currentLoaders) {
|
||||
try {
|
||||
World w = Bukkit.getWorld(loader.world());
|
||||
if (w == null || !w.isChunkLoaded(loader.chunkX(), loader.chunkZ())) {
|
||||
continue;
|
||||
}
|
||||
Chunk chunk = w.getChunkAt(loader.chunkX(), loader.chunkZ());
|
||||
|
||||
// 1. Keep mob spawners (dungeon / blaze / skeleton cages) active
|
||||
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)
|
||||
long mobCount = 0;
|
||||
for (Entity entity : chunk.getEntities()) {
|
||||
if (entity instanceof Mob) {
|
||||
mobCount++;
|
||||
}
|
||||
}
|
||||
if (mobCount >= 20) {
|
||||
continue; // Respect chunk mob cap
|
||||
}
|
||||
|
||||
simulateNaturalSpawning(w, loader, chunk);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void simulateNaturalSpawning(World w, ChunkLoader loader, Chunk chunk) {
|
||||
int baseX = loader.chunkX() << 4;
|
||||
int baseZ = loader.chunkZ() << 4;
|
||||
ThreadLocalRandom rnd = ThreadLocalRandom.current();
|
||||
|
||||
for (int i = 0; i < 2; i++) {
|
||||
int rx = baseX + rnd.nextInt(16);
|
||||
int rz = baseZ + rnd.nextInt(16);
|
||||
int topY = Math.max(w.getMinHeight() + 2, Math.min(w.getMaxHeight() - 2, loader.y() + rnd.nextInt(-24, 25)));
|
||||
|
||||
Block ground = w.getBlockAt(rx, topY - 1, rz);
|
||||
Block space = w.getBlockAt(rx, topY, rz);
|
||||
Block spaceAbove = w.getBlockAt(rx, topY + 1, rz);
|
||||
|
||||
if (!ground.getType().isSolid() || !space.getType().isAir() || !spaceAbove.getType().isAir()) {
|
||||
continue;
|
||||
}
|
||||
if (ground.isLiquid() || ground.getType() == Material.LAVA || ground.getType() == Material.WATER) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int light = space.getLightLevel();
|
||||
EntityType toSpawn = pickEntityType(w, space, light);
|
||||
if (toSpawn == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
Entity spawned = w.spawnEntity(space.getLocation().add(0.5, 0, 0.5), toSpawn, CreatureSpawnEvent.SpawnReason.NATURAL);
|
||||
if (spawned instanceof Mob mob) {
|
||||
mob.setRemoveWhenFarAway(false);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static EntityType pickEntityType(World w, Block space, int light) {
|
||||
World.Environment env = w.getEnvironment();
|
||||
ThreadLocalRandom rnd = ThreadLocalRandom.current();
|
||||
|
||||
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;
|
||||
return EntityType.MAGMA_CUBE;
|
||||
}
|
||||
|
||||
if (env == World.Environment.THE_END) {
|
||||
return EntityType.ENDERMAN;
|
||||
}
|
||||
|
||||
boolean isSlimeChunk = space.getChunk().isSlimeChunk() && space.getY() < 40;
|
||||
if (isSlimeChunk && light <= 7 && rnd.nextInt(3) == 0) {
|
||||
return EntityType.SLIME;
|
||||
}
|
||||
|
||||
if (light > 0) {
|
||||
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;
|
||||
return EntityType.WITCH;
|
||||
}
|
||||
|
||||
private void addTicket(ChunkLoader loader) {
|
||||
if (plugin == null) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user