295 lines
14 KiB
Diff
295 lines
14 KiB
Diff
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||
From: Luminol Contributors <luminol@example.com>
|
||
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<String> 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<String> 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<Entity> 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<TickTa
|
||
if (this.initServer()) {
|
||
if (me.earthme.luminol.LuminolConfig.get().optimizations.mobAiReducer.enabled) {
|
||
me.earthme.luminol.mobaireducer.MobAiReducerHandler.register();
|
||
}
|
||
+ // Luminol start - EntityLimiter startup
|
||
+ me.earthme.luminol.entitylimiter.LuminolEntityLimiter.start();
|
||
+ // Luminol end - EntityLimiter startup
|
||
this.nextTickTimeNanos = Util.getNanos();
|
||
@@ -720,6 +724,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
||
public void stopServer() {
|
||
+ me.earthme.luminol.entitylimiter.LuminolEntityLimiter.stop(); // Luminol - EntityLimiter shutdown
|
||
super.stopServer();
|