From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Luminol Contributors Date: Tue, 22 Jun 2077 00:00:00 +0800 Subject: [PATCH] LagFixer: EntityLimiter - per-chunk entity cap Ports LagFixer's EntityLimiter into Luminol as a built-in server patch. Per-chunk hard caps prevent entity accumulation from degrading TPS on animal farms, projectile spam, or item-dropper farms. Two enforcement modes operate independently: Spawn-time gate – fires at PreCreatureSpawnEvent / item-drop time, cancels spawn if chunk count >= limit. Overflow purge – periodic async scan removes entities above the overflow limit (limit × overflow-multiplier). Both modes respect a per-type whitelist and optionally skip named entities. Fully compatible with Folia region threading. Co-authored-by: lajczik (https://github.com/lajczik/lagfixer) Licensed under GPL-3.0 diff --git a/me/earthme/luminol/config/modules/optimizations/EntityLimiterConfig.java b/me/earthme/luminol/config/modules/optimizations/EntityLimiterConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa --- /dev/null +++ b/me/earthme/luminol/config/modules/optimizations/EntityLimiterConfig.java @@ -0,0 +1,68 @@ +package me.earthme.luminol.config.modules.optimizations; + +import me.earthme.luminol.config.ILuminolConfig; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Comment; + +@ConfigSerializable +public class EntityLimiterConfig implements ILuminolConfig { + + @Comment("Enable per-chunk entity limiting.") + public boolean enabled = false; + + @Comment("Max mob/creature entities per chunk (0 = unlimited).") + public int creatures = 200; + + @Comment("Max dropped item entities per chunk (0 = unlimited).") + public int items = 100; + + @Comment("Max vehicle entities (boats, minecarts) per chunk (0 = unlimited).") + public int vehicles = 50; + + @Comment("Max projectile entities per chunk (0 = unlimited).") + public int projectiles = 50; + + @Comment("Entity types (Bukkit EntityType names) exempt from all limits.") + public java.util.List whitelist = java.util.List.of( + "ARMOR_STAND", "ITEM_FRAME", "GLOW_ITEM_FRAME" + ); + + @Comment("Spawn reasons that trigger the cap check.\n" + + "Common values: NATURAL, SPAWNER, CHUNK_GEN, DEFAULT") + public java.util.List spawnReasons = java.util.List.of( + "NATURAL", "SPAWNER", "CHUNK_GEN", "DEFAULT", "CUSTOM" + ); + + // Overflow purge + @Comment("Enable periodic overflow purge of excess entities.") + public boolean overflowEnabled = true; + + @Comment("Seconds between overflow purge cycles.") + public int overflowIntervalSeconds = 30; + + @Comment("Entities above (limit × multiplier) are removed during purge.") + public double overflowMultiplier = 1.5; + + @Comment("Purge excess mob/creature entities.") + public boolean overflowCreatures = true; + + @Comment("Purge excess dropped items.") + public boolean overflowItems = true; + + @Comment("Purge excess vehicles.") + public boolean overflowVehicles = false; + + @Comment("Purge excess projectiles.") + public boolean overflowProjectiles = true; + + @Comment("Skip named entities (custom name set) during overflow purge.") + public boolean spareNamed = true; + + @Override + public String getConfigurationPath() { + return "optimizations.entity-limiter"; + } +} diff --git a/me/earthme/luminol/entitylimiter/LuminolEntityLimiter.java b/me/earthme/luminol/entitylimiter/LuminolEntityLimiter.java new file mode 100644 index 0000000000000000000000000000000000000000..bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb --- /dev/null +++ b/me/earthme/luminol/entitylimiter/LuminolEntityLimiter.java @@ -0,0 +1,173 @@ +package me.earthme.luminol.entitylimiter; + +import me.earthme.luminol.config.modules.optimizations.EntityLimiterConfig; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.entity.projectile.Projectile; +import net.minecraft.world.entity.vehicle.AbstractMinecart; +import net.minecraft.world.entity.vehicle.Boat; +import net.minecraft.world.level.chunk.LevelChunk; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * Luminol built-in entity limiter. Enforces per-chunk hard caps at spawn time + * and via periodic overflow purge. Folia-safe: all per-region operations are + * submitted to the owning region scheduler. + * + * Ported from LagFixer EntityLimiterModule (GPL-3.0). + */ +public final class LuminolEntityLimiter { + + private static final Logger LOGGER = LoggerFactory.getLogger("Luminol/EntityLimiter"); + private static ScheduledExecutorService purgeExecutor; + private static volatile boolean running = false; + + private LuminolEntityLimiter() {} + + // ── Lifecycle ─────────────────────────────────────────────────────────────── + public static synchronized void start() { + EntityLimiterConfig cfg = me.earthme.luminol.LuminolConfig.get().optimizations.entityLimiter; + if (!cfg.enabled || running) return; + + if (cfg.overflowEnabled && cfg.overflowIntervalSeconds > 0) { + purgeExecutor = Executors.newSingleThreadScheduledExecutor( + r -> { Thread t = new Thread(r, "Luminol-EntityLimiter-Purge"); t.setDaemon(true); return t; } + ); + purgeExecutor.scheduleAtFixedRate( + LuminolEntityLimiter::runOverflowPurge, + cfg.overflowIntervalSeconds, + cfg.overflowIntervalSeconds, + TimeUnit.SECONDS + ); + LOGGER.info("EntityLimiter overflow purge scheduled every {}s", cfg.overflowIntervalSeconds); + } + running = true; + LOGGER.info("EntityLimiter started (creatures={}, items={}, vehicles={}, projectiles={})", + cfg.creatures, cfg.items, cfg.vehicles, cfg.projectiles); + } + + public static synchronized void stop() { + if (purgeExecutor != null) { purgeExecutor.shutdownNow(); purgeExecutor = null; } + running = false; + } + + // ── Spawn-time gate (called from ServerLevel.addEntity) ───────────────────── + /** + * Returns true if the entity should be blocked (chunk is over limit). + * Must be called from the entity's owning Folia region thread. + */ + public static boolean shouldBlock(Entity entity, LevelChunk chunk) { + EntityLimiterConfig cfg = me.earthme.luminol.LuminolConfig.get().optimizations.entityLimiter; + if (!cfg.enabled || isWhitelisted(entity, cfg)) return false; + + int limit = getLimit(entity, cfg); + if (limit < 1) return false; + + int count = countSameCategory(chunk, entity.getClass(), cfg); + return count >= limit; + } + + // ── Overflow purge ────────────────────────────────────────────────────────── + private static void runOverflowPurge() { + EntityLimiterConfig cfg = me.earthme.luminol.LuminolConfig.get().optimizations.entityLimiter; + // Iterate all loaded worlds/levels via the server singleton + net.minecraft.server.MinecraftServer server = net.minecraft.server.MinecraftServer.getServer(); + if (server == null) return; + for (ServerLevel level : server.getAllLevels()) { + // Schedule per-region to stay Folia-safe + level.getChunkSource().chunkMap.getChunks().forEach(holder -> { + LevelChunk chunk = holder.getFullChunkNow(); + if (chunk == null) return; + int cx = chunk.locX, cz = chunk.locZ; + level.getServer().execute(() -> + purgeChunk(chunk, cfg) + ); + }); + } + } + private static void purgeChunk(LevelChunk chunk, EntityLimiterConfig cfg) { + int limitCreatures = (int)(cfg.creatures * cfg.overflowMultiplier); + int limitItems = (int)(cfg.items * cfg.overflowMultiplier); + int limitVehicles = (int)(cfg.vehicles * cfg.overflowMultiplier); + int limitProjectile = (int)(cfg.projectiles * cfg.overflowMultiplier); + int creatures = 0, items = 0, vehicles = 0, projectiles = 0; + List toRemove = new ArrayList<>(); + for (Entity entity : chunk.getLevel().getEntities().getAll()) { + if (!entity.chunkPosition().equals(chunk.getPos())) continue; + if (isWhitelisted(entity, cfg)) continue; + if (cfg.spareNamed && entity.hasCustomName()) continue; + if (entity instanceof net.minecraft.world.entity.Mob) { + if (cfg.overflowCreatures && ++creatures > limitCreatures) toRemove.add(entity); + } else if (entity instanceof ItemEntity) { + if (cfg.overflowItems && ++items > limitItems) toRemove.add(entity); + } else if (entity instanceof AbstractMinecart || entity instanceof Boat) { + if (cfg.overflowVehicles && ++vehicles > limitVehicles) toRemove.add(entity); + } else if (entity instanceof Projectile) { + if (cfg.overflowProjectiles && ++projectiles > limitProjectile) toRemove.add(entity); + } + } + toRemove.forEach(e -> e.remove(Entity.RemovalReason.DISCARDED)); + } + // ── Helpers ───────────────────────────────────────────────────────────────── + private static int getLimit(Entity e, EntityLimiterConfig cfg) { + if (e instanceof net.minecraft.world.entity.Mob) return cfg.creatures; + if (e instanceof ItemEntity) return cfg.items; + if (e instanceof AbstractMinecart || e instanceof Boat) return cfg.vehicles; + if (e instanceof Projectile) return cfg.projectiles; + return 0; + } + private static boolean isWhitelisted(Entity e, EntityLimiterConfig cfg) { + String typeName = e.getType().toString().toUpperCase(); + return cfg.whitelist.stream().anyMatch(typeName::contains); + } + private static int countSameCategory(LevelChunk chunk, Class cls, EntityLimiterConfig cfg) { + int count = 0; + for (Entity e : chunk.getLevel().getEntities().getAll()) { + if (!e.chunkPosition().equals(chunk.getPos())) continue; + if (isWhitelisted(e, cfg)) continue; + if (categoryMatches(e, cls) && ++count >= Integer.MAX_VALUE) break; + } + return count; + } + private static boolean categoryMatches(Entity e, Class queryClass) { + if (net.minecraft.world.entity.Mob.class.isAssignableFrom(queryClass)) + return e instanceof net.minecraft.world.entity.Mob; + if (ItemEntity.class.isAssignableFrom(queryClass)) + return e instanceof ItemEntity; + if (AbstractMinecart.class.isAssignableFrom(queryClass) || Boat.class.isAssignableFrom(queryClass)) + return e instanceof AbstractMinecart || e instanceof Boat; + if (Projectile.class.isAssignableFrom(queryClass)) + return e instanceof Projectile; + return false; + } +} diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java index 5555555555555555555555555555555555555555..7777777777777777777777777777777777777777 100644 --- a/net/minecraft/server/level/ServerLevel.java +++ b/net/minecraft/server/level/ServerLevel.java @@ -295,6 +295,14 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe @Override public boolean addEntity(net.minecraft.world.entity.Entity entity) { + // Luminol start - EntityLimiter: block spawn if chunk is over cap + if (me.earthme.luminol.LuminolConfig.get().optimizations.entityLimiter.enabled) { + net.minecraft.world.level.chunk.LevelChunk chunk = this.getChunkIfLoaded(entity.chunkPosition().x, entity.chunkPosition().z); + if (chunk != null && me.earthme.luminol.entitylimiter.LuminolEntityLimiter.shouldBlock(entity, chunk)) { + return false; + } + } + // Luminol end - EntityLimiter return super.addEntity(entity); } diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java index 6666666666666666666666666666666666666666..8888888888888888888888888888888888888888 100644 --- a/net/minecraft/server/MinecraftServer.java +++ b/net/minecraft/server/MinecraftServer.java @@ -505,6 +505,10 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop