diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml
index 0d8c753..19ce86b 100644
--- a/.github/ISSUE_TEMPLATE/bug-report.yml
+++ b/.github/ISSUE_TEMPLATE/bug-report.yml
@@ -25,7 +25,7 @@ body:
LophineMC服务端发行版本 LophineMC server release version (contains the git commit's hash)
通过执行`/version`命令将获取到的内容复制到这里。 Run `/version` on your server and paste the full, unmodified output here.
placeholder: >
- e.g. git-Lophine-"0635a89" (MC: 1.20.4)
+ e.g. git-Lophine-"fa1ab90" (MC: 1.21.8)
validations:
required: true
- type: textarea
@@ -65,6 +65,20 @@ body:
To list plugins, run `/plugins`. For datapacks, run `/datapack list`.
validations:
required: true
+ - type: textarea
+ id: config-file-list
+ attributes:
+ label: 配置文件列表(如果可以)。 Config File List (if possible).
+ description:
+ 服务器上的配置文件列表。
+ All config files on your server.
+ 例如以下文件(或者文件夹):
+ Example files (or folders):
+ - \spigot.yml
+ - \bukkit.yml
+ - \config\*
+ - \lophine_config\*
+ - \luminol_config\*
- type: textarea
id: other
attributes:
diff --git a/gradle.properties b/gradle.properties
index a04fa2f..d2f64cf 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -4,7 +4,7 @@ mcVersion=1.21.8
release=2
# 0 for skip release, 1 for pre-release, 2 for release
-luminolRef=ae1af655f57cd8666c2ae913fa53d3606019bf7b
+luminolRef=1958fdc8b66b75b23a001490288cc618a76808e3
org.gradle.configuration-cache=true
org.gradle.caching=true
diff --git a/lophine-server/minecraft-patches/features/0041-Global-Entities-Counter.patch b/lophine-server/minecraft-patches/features/0041-Global-Entities-Counter.patch
new file mode 100644
index 0000000..be0855b
--- /dev/null
+++ b/lophine-server/minecraft-patches/features/0041-Global-Entities-Counter.patch
@@ -0,0 +1,93 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Helvetica Volubi
+Date: Wed, 3 Dec 2025 16:28:34 +0800
+Subject: [PATCH] Global Entities Counter
+
+
+diff --git a/io/papermc/paper/threadedregions/RegionizedWorldData.java b/io/papermc/paper/threadedregions/RegionizedWorldData.java
+index 3b39b8f7ccb278cf910e3cc4e00da29790465a81..8a985f9b1fed90e167d625815eb5fa6586a00972 100644
+--- a/io/papermc/paper/threadedregions/RegionizedWorldData.java
++++ b/io/papermc/paper/threadedregions/RegionizedWorldData.java
+@@ -529,6 +529,8 @@ public final class RegionizedWorldData {
+ this.wireHandler = new alternate.current.wire.WireHandler(world);
+ this.turbo = new io.papermc.paper.redstone.RedstoneWireTurbo((RedStoneWireBlock)Blocks.REDSTONE_WIRE);
+
++ fun.bm.lophine.utils.EntitiesCounterUtil.addDataToLoaded(loadedEntities, world); // Lophine - Global Entities Counter
++
+ // tasks may be drained before the region ticks, so we must set up the tick data early just in case
+ this.updateTickData();
+ }
+diff --git a/net/minecraft/server/level/ServerChunkCache.java b/net/minecraft/server/level/ServerChunkCache.java
+index 05bad0366bd36b8134290a0cceb4d4dfcbbd0544..446dc2e3db99fe7f081b8d3df48ba8a9778ae379 100644
+--- a/net/minecraft/server/level/ServerChunkCache.java
++++ b/net/minecraft/server/level/ServerChunkCache.java
+@@ -580,7 +580,13 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon
+ }
+ spawnState = NaturalSpawner.createState(naturalSpawnChunkCount, regionizedWorldData.getLoadedEntities(), this::getFullChunk, null, true); // Folia - region threading - note: function only cares about loaded entities, doesn't need all
+ } else {
++ // Lophine start - Global Entities Counter
++ if (fun.bm.lophine.config.modules.experiment.GlobalEntitiesCounter.enabled) {
++ spawnState = fun.bm.lophine.utils.EntitiesCounterUtil.runRemainingTasks(level, naturalSpawnChunkCount, regionizedWorldData.getLoadedEntities(), this::getFullChunk, !this.level.paperConfig().entities.spawning.perPlayerMobSpawns ? new LocalMobCapCalculator(this.chunkMap) : null, false);
++ } else {
+ spawnState = NaturalSpawner.createState(naturalSpawnChunkCount, regionizedWorldData.getLoadedEntities(), this::getFullChunk, !this.level.paperConfig().entities.spawning.perPlayerMobSpawns ? new LocalMobCapCalculator(this.chunkMap) : null, false); // Folia - region threading - note: function only cares about loaded entities, doesn't need all
++ }
++ // Lophine end - Global Entities Counter
+ }
+ } finally { foliaProfiler.stopTimer(ca.spottedleaf.leafprofiler.LProfilerRegistry.MOB_SPAWN_ENTITY_COUNT); } // Folia - profiler
+ // Paper end - Optional per player mob spawns
+@@ -589,7 +595,7 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon
+ boolean _boolean = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && !this.level.getLocalPlayers().isEmpty(); // CraftBukkit // Folia - region threading
+ int _int = this.level.getGameRules().getInt(GameRules.RULE_RANDOMTICKING);
+ List filteredSpawningCategories;
+- if (_boolean && (this.spawnEnemies || this.spawnFriendlies)) {
++ if (spawnState != null && _boolean && (this.spawnEnemies || this.spawnFriendlies)) { // Lophine - Global Entities Counter
+ // Paper start - PlayerNaturallySpawnCreaturesEvent
+ for (ServerPlayer entityPlayer : this.level.getLocalPlayers()) { // Folia - region threading
+ int chunkRange = Math.min(level.spigotConfig.mobSpawnRange, entityPlayer.getBukkitEntity().getViewDistance());
+@@ -646,7 +652,7 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon
+ this.level.tickThunder(chunk);
+ }
+
+- if (!spawnCategories.isEmpty()) {
++ if (spawnState != null && !spawnCategories.isEmpty()) { // Lophine - Global Entities Counter
+ if (this.level.getWorldBorder().isWithinBounds(pos)) { // Paper - rewrite chunk system
+ NaturalSpawner.spawnForChunk(this.level, chunk, spawnState, spawnCategories);
+ }
+diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java
+index c0ba36849da0983117901f0cd04b2c785df0010b..a51ae9326907c8a1ecdbc3112ca6d76520791678 100644
+--- a/net/minecraft/server/level/ServerLevel.java
++++ b/net/minecraft/server/level/ServerLevel.java
+@@ -729,6 +729,11 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+ }
+
+ public void tick(BooleanSupplier hasTimeLeft, io.papermc.paper.threadedregions.TickRegions.TickRegionData region) { // Folia - regionised ticking
++ // Lophine start - Global Entities Counter
++ if (fun.bm.lophine.config.modules.experiment.GlobalEntitiesCounter.enabled) {
++ fun.bm.lophine.utils.EntitiesCounterUtil.tick(this);
++ }
++ // Lophine end - Global Entities Counter
+ final io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.getCurrentWorldData(); // Folia - regionised ticking
+ final ca.spottedleaf.leafprofiler.RegionizedProfiler.Handle profiler = io.papermc.paper.threadedregions.TickRegionScheduler.getProfiler(); // Folia - profiler
+ ProfilerFiller profilerFiller = Profiler.get();
+diff --git a/net/minecraft/world/level/NaturalSpawner.java b/net/minecraft/world/level/NaturalSpawner.java
+index 81f4707cf1a7cbdfcfc519cfbe1052cde77aa755..5c53e2d61ce8483d5d9e93e9f3112074c704a546 100644
+--- a/net/minecraft/world/level/NaturalSpawner.java
++++ b/net/minecraft/world/level/NaturalSpawner.java
+@@ -113,7 +113,7 @@ public final class NaturalSpawner {
+ return new NaturalSpawner.SpawnState(spawnableChunkCount, map, potentialCalculator, calculator);
+ }
+
+- static Biome getRoughBiome(BlockPos pos, ChunkAccess chunk) {
++ public static Biome getRoughBiome(BlockPos pos, ChunkAccess chunk) { // Lophine - protected -> public
+ return chunk.getNoiseBiome(QuartPos.fromBlock(pos.getX()), QuartPos.fromBlock(pos.getY()), QuartPos.fromBlock(pos.getZ())).value();
+ }
+
+@@ -584,7 +584,7 @@ public final class NaturalSpawner {
+ private EntityType> lastCheckedType;
+ private double lastCharge;
+
+- SpawnState(
++ public SpawnState( // Lophine - protected -> public
+ int spawnableChunkCount,
+ Object2IntOpenHashMap mobCategoryCounts,
+ PotentialCalculator spawnPotential,
diff --git a/lophine-server/src/main/java/fun/bm/lophine/config/modules/experiment/GlobalEntitiesCounter.java b/lophine-server/src/main/java/fun/bm/lophine/config/modules/experiment/GlobalEntitiesCounter.java
new file mode 100644
index 0000000..a2c028a
--- /dev/null
+++ b/lophine-server/src/main/java/fun/bm/lophine/config/modules/experiment/GlobalEntitiesCounter.java
@@ -0,0 +1,20 @@
+package fun.bm.lophine.config.modules.experiment;
+
+import me.earthme.luminol.config.IConfigModule;
+import me.earthme.luminol.config.flags.ConfigClassInfo;
+import me.earthme.luminol.config.flags.ConfigInfo;
+import me.earthme.luminol.config.flags.HotReloadUnsupported;
+import me.earthme.luminol.enums.EnumConfigCategory;
+
+@ConfigClassInfo(name = "global_entities_counter", category = EnumConfigCategory.EXPERIMENT)
+public class GlobalEntitiesCounter implements IConfigModule {
+ @HotReloadUnsupported
+ @ConfigInfo(name = "enabled", comments = """
+ Enable global entities counter.
+ You need to set per-player-mob-spawns to false on paper-world-defaults.yml or paper-world.yml""")
+ public static boolean enabled = false;
+
+ @HotReloadUnsupported
+ @ConfigInfo(name = "async", comments = "Enable Asynchronous(maybe cause bugs)")
+ public static boolean async = false;
+}
diff --git a/lophine-server/src/main/java/fun/bm/lophine/utils/EntitiesCounterUtil.java b/lophine-server/src/main/java/fun/bm/lophine/utils/EntitiesCounterUtil.java
new file mode 100644
index 0000000..5af8701
--- /dev/null
+++ b/lophine-server/src/main/java/fun/bm/lophine/utils/EntitiesCounterUtil.java
@@ -0,0 +1,100 @@
+package fun.bm.lophine.utils;
+
+import ca.spottedleaf.moonrise.common.list.ReferenceList;
+import fun.bm.lophine.config.modules.experiment.GlobalEntitiesCounter;
+import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
+import net.minecraft.core.BlockPos;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.Mob;
+import net.minecraft.world.entity.MobCategory;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.LocalMobCapCalculator;
+import net.minecraft.world.level.NaturalSpawner;
+import net.minecraft.world.level.PotentialCalculator;
+import net.minecraft.world.level.biome.MobSpawnSettings;
+import org.bukkit.craftbukkit.util.WeakCollection;
+import org.bukkit.event.entity.CreatureSpawnEvent;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.WeakHashMap;
+import java.util.concurrent.CompletableFuture;
+
+import static net.minecraft.world.level.NaturalSpawner.getRoughBiome;
+
+public class EntitiesCounterUtil {
+ private static final WeakHashMap>> globalLoadedEntities = new WeakHashMap<>();
+ private static final WeakHashMap> mobsMap = new WeakHashMap<>();
+ private static final WeakHashMap> tasks = new WeakHashMap<>();
+
+ public static void addDataToLoaded(ReferenceList data, ServerLevel level) {
+ WeakCollection> data0 = globalLoadedEntities.computeIfAbsent(level, k -> new WeakCollection<>());
+ data0.add(data);
+ }
+
+ public static @Nullable Object2IntOpenHashMap getMobsMap(ServerLevel level) {
+ return mobsMap.get(level);
+ }
+
+ public static boolean canRunNewTask(ServerLevel level) {
+ return tasks.get(level).isDone();
+ }
+
+ public static void tick(ServerLevel level) {
+ Runnable task = () -> {
+ WeakCollection> data0 = globalLoadedEntities.get(level);
+ Object2IntOpenHashMap map = new Object2IntOpenHashMap<>();
+ for (ReferenceList data : data0) {
+ for (Entity entity : GlobalEntitiesCounter.enabled ? data.copy() : data) {
+ // Lophine start - Copy from net/minecraft/world/level/NaturalSpawner
+ MobCategory category = entity.getType().getCategory();
+ if (category != MobCategory.MISC) {
+ // Paper start - Only count natural spawns
+ if (!entity.level().paperConfig().entities.spawning.countAllMobsForSpawning &&
+ !(entity.spawnReason == CreatureSpawnEvent.SpawnReason.NATURAL ||
+ entity.spawnReason == CreatureSpawnEvent.SpawnReason.CHUNK_GEN)) {
+ continue;
+ }
+ map.addTo(category, 1);
+ // Paper end - Only count natural spawns
+ }
+ // Lophine start - Copy from net/minecraft/world/level/NaturalSpawner
+ }
+ }
+ mobsMap.put(level, map);
+ };
+ if (GlobalEntitiesCounter.async) {
+ tasks.put(level, CompletableFuture.runAsync(task));
+ } else {
+ task.run();
+ }
+ }
+
+ public static NaturalSpawner.SpawnState runRemainingTasks(
+ ServerLevel level, int spawnableChunkCount, Iterable entities, NaturalSpawner.ChunkGetter chunkGetter, LocalMobCapCalculator calculator, final boolean countMobs
+ ) {
+ Object2IntOpenHashMap map = mobsMap.get(level);
+ if (map == null) return null; // skip if no data
+ PotentialCalculator potentialCalculator = new PotentialCalculator();
+ for (Entity entity : entities) {
+ BlockPos blockPos = entity.blockPosition();
+ chunkGetter.query(ChunkPos.asLong(blockPos), chunk -> {
+ MobSpawnSettings.MobSpawnCost mobSpawnCost = getRoughBiome(blockPos, chunk).getMobSettings().getMobSpawnCost(entity.getType());
+ if (mobSpawnCost != null) {
+ potentialCalculator.addCharge(entity.blockPosition(), mobSpawnCost.charge());
+ }
+
+ if (calculator != null && entity instanceof Mob) { // Paper - Optional per player mob spawns
+ calculator.addMob(chunk.getPos(), entity.getType().getCategory());
+ }
+
+ // Paper start - Optional per player mob spawns
+ if (countMobs) {
+ chunk.level.getChunkSource().chunkMap.updatePlayerMobTypeMap(entity);
+ }
+ // Paper end - Optional per player mob spawns
+ });
+ }
+ return new NaturalSpawner.SpawnState(spawnableChunkCount, map, potentialCalculator, calculator);
+ }
+}