Luminol-Core/luminol-server/minecraft-patches/_fabricated_reference/0075-LagFixer-EntityLimiter-per-chunk-entity-cap.patch
2026-06-30 18:32:29 +08:00

295 lines
14 KiB
Diff
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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();