From 376857ff97c9228304b2196b6adae297b28edfb5 Mon Sep 17 00:00:00 2001 From: Helvetica Volubi Date: Wed, 3 Dec 2025 16:31:34 +0800 Subject: [PATCH] feat: global entities counter We provided a new way to use entities counter to spawn mobs, maybe have some bugs, so we set this to experiment config - Update file update - Update Luminol --- .github/ISSUE_TEMPLATE/bug-report.yml | 16 ++- gradle.properties | 2 +- .../0041-Global-Entities-Counter.patch | 93 ++++++++++++++++ .../experiment/GlobalEntitiesCounter.java | 20 ++++ .../bm/lophine/utils/EntitiesCounterUtil.java | 100 ++++++++++++++++++ 5 files changed, 229 insertions(+), 2 deletions(-) create mode 100644 lophine-server/minecraft-patches/features/0041-Global-Entities-Counter.patch create mode 100644 lophine-server/src/main/java/fun/bm/lophine/config/modules/experiment/GlobalEntitiesCounter.java create mode 100644 lophine-server/src/main/java/fun/bm/lophine/utils/EntitiesCounterUtil.java 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); + } +}