From 9fa4302bf243ea4f4e1e993a8212497b6de43664 Mon Sep 17 00:00:00 2001 From: NONPLAYT <76615486+NONPLAYT@users.noreply.github.com> Date: Wed, 19 Mar 2025 23:30:41 +0300 Subject: [PATCH] async mob spawning --- build-data/divinemc.at | 1 + .../0008-Parallel-world-ticking.patch | 4 +- .../features/0045-Async-mob-spawning.patch | 177 ++++++++++++++++++ .../org/bxteam/divinemc/DivineConfig.java | 3 + .../bxteam/divinemc/util/AsyncProcessor.java | 59 ++++++ 5 files changed, 242 insertions(+), 2 deletions(-) create mode 100644 divinemc-server/minecraft-patches/features/0045-Async-mob-spawning.patch create mode 100644 divinemc-server/src/main/java/org/bxteam/divinemc/util/AsyncProcessor.java diff --git a/build-data/divinemc.at b/build-data/divinemc.at index 866c347..14aee74 100644 --- a/build-data/divinemc.at +++ b/build-data/divinemc.at @@ -28,6 +28,7 @@ public net.minecraft.world.level.chunk.storage.RegionFile isOversized(II)Z public net.minecraft.world.level.chunk.storage.RegionFile recalculateHeader()Z public net.minecraft.world.level.chunk.storage.RegionFile setOversized(IIZ)V public net.minecraft.world.level.chunk.storage.RegionFile write(Lnet/minecraft/world/level/ChunkPos;Ljava/nio/ByteBuffer;)V +public net.minecraft.world.level.entity.EntityTickList entities public net.minecraft.world.level.levelgen.DensityFunctions$BlendAlpha public net.minecraft.world.level.levelgen.DensityFunctions$BlendDensity public net.minecraft.world.level.levelgen.DensityFunctions$BlendOffset diff --git a/divinemc-server/minecraft-patches/features/0008-Parallel-world-ticking.patch b/divinemc-server/minecraft-patches/features/0008-Parallel-world-ticking.patch index f8fa0e0..c9f66e5 100644 --- a/divinemc-server/minecraft-patches/features/0008-Parallel-world-ticking.patch +++ b/divinemc-server/minecraft-patches/features/0008-Parallel-world-ticking.patch @@ -994,13 +994,13 @@ index 048ad8e11c6913ef08c977c7fab3200cc610519c..66c7f0d2adc8c00b53125b8cdc5da8c4 int y = pos.getY(); LevelChunkSection section = this.getSection(this.getSectionIndex(y)); diff --git a/net/minecraft/world/level/entity/EntityTickList.java b/net/minecraft/world/level/entity/EntityTickList.java -index 423779a2b690f387a4f0bd07b97b50e0baefda76..2567e3a0988fcd21a39b0c82e1a4b43946810987 100644 +index c89701d7bdc9b889038d3c52f2232fb17624b113..3c6ec711bf9a75657c13da647e4ae7947257b627 100644 --- a/net/minecraft/world/level/entity/EntityTickList.java +++ b/net/minecraft/world/level/entity/EntityTickList.java @@ -10,17 +10,27 @@ import net.minecraft.world.entity.Entity; public class EntityTickList { - private final ca.spottedleaf.moonrise.common.list.IteratorSafeOrderedReferenceSet entities = new ca.spottedleaf.moonrise.common.list.IteratorSafeOrderedReferenceSet<>(); // Paper - rewrite chunk system + public final ca.spottedleaf.moonrise.common.list.IteratorSafeOrderedReferenceSet entities = new ca.spottedleaf.moonrise.common.list.IteratorSafeOrderedReferenceSet<>(); // Paper - rewrite chunk system + // DivineMC start - parallel world ticking + // Used to track async entity additions/removals/loops + private final net.minecraft.server.level.ServerLevel serverLevel; diff --git a/divinemc-server/minecraft-patches/features/0045-Async-mob-spawning.patch b/divinemc-server/minecraft-patches/features/0045-Async-mob-spawning.patch new file mode 100644 index 0000000..85f8183 --- /dev/null +++ b/divinemc-server/minecraft-patches/features/0045-Async-mob-spawning.patch @@ -0,0 +1,177 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: NONPLAYT <76615486+NONPLAYT@users.noreply.github.com> +Date: Wed, 19 Mar 2025 23:24:32 +0300 +Subject: [PATCH] Async mob spawning + + +diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java +index 466fc421e0191ce1854e114fd3a013d241bc89f3..3ec8f7f0f6fdd00d4c2926b19bedd0c5e4e07e09 100644 +--- a/net/minecraft/server/MinecraftServer.java ++++ b/net/minecraft/server/MinecraftServer.java +@@ -309,6 +309,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop entitiesWithScheduledTasks = java.util.concurrent.ConcurrentHashMap.newKeySet(); // DivineMC - Skip EntityScheduler's executeTick checks if there isn't any tasks to be run ++ public org.bxteam.divinemc.util.AsyncProcessor mobSpawnExecutor = new org.bxteam.divinemc.util.AsyncProcessor("mob_spawning"); // DivineMC - Async mob spawning + + public static S spin(Function threadFunction) { + ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.init(); // Paper - rewrite data converter system +diff --git a/net/minecraft/server/level/ServerChunkCache.java b/net/minecraft/server/level/ServerChunkCache.java +index 0c542bfa8f6d8fd47427dcf74f71500ed5772f35..39b2a29ec3eb94a00b08b4f1cc05dece549e8173 100644 +--- a/net/minecraft/server/level/ServerChunkCache.java ++++ b/net/minecraft/server/level/ServerChunkCache.java +@@ -185,6 +185,10 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon + } + // Paper end - chunk tick iteration optimisations + ++ // DivineMC start - Async mob spawning ++ public boolean firstRunSpawnCounts = true; ++ public final java.util.concurrent.atomic.AtomicBoolean spawnCountsReady = new java.util.concurrent.atomic.AtomicBoolean(false); ++ // DivineMC end - Async mob spawning + + public ServerChunkCache( + ServerLevel level, +@@ -609,6 +613,35 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon + this.broadcastChangedChunks(profilerFiller); + profilerFiller.pop(); + } ++ ++ // DivineMC start - Async mob spawning ++ if (org.bxteam.divinemc.DivineConfig.enableAsyncSpawning) { ++ for (ServerPlayer player : this.level.players) { ++ for (int ii = 0; ii < ServerPlayer.MOBCATEGORY_TOTAL_ENUMS; ii++) { ++ player.mobCounts[ii] = 0; ++ ++ int newBackoff = player.mobBackoffCounts[ii] - 1; ++ if (newBackoff < 0) { ++ newBackoff = 0; ++ } ++ player.mobBackoffCounts[ii] = newBackoff; ++ } ++ } ++ if (firstRunSpawnCounts) { ++ firstRunSpawnCounts = false; ++ spawnCountsReady.set(true); ++ } ++ if (spawnCountsReady.getAndSet(false)) { ++ MinecraftServer.getServer().mobSpawnExecutor.submit(() -> { ++ int mapped = distanceManager.getNaturalSpawnChunkCount(); ++ try { ++ lastSpawnState = NaturalSpawner.createState(mapped, new ArrayList<>(level.entityTickList.entities), this::getFullChunk, new LocalMobCapCalculator(this.chunkMap), true); ++ } finally { } ++ spawnCountsReady.set(true); ++ }); ++ } ++ } ++ // DivineMC end - Async mob spawning + } + + private void broadcastChangedChunks(ProfilerFiller profiler) { +@@ -653,27 +686,31 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon + int naturalSpawnChunkCount = this.distanceManager.getNaturalSpawnChunkCount(); + // Paper start - Optional per player mob spawns + NaturalSpawner.SpawnState spawnState; ++ // DivineMC start - Async mob spawning + if ((this.spawnFriendlies || this.spawnEnemies) && this.level.paperConfig().entities.spawning.perPlayerMobSpawns) { // don't count mobs when animals and monsters are disabled +- // re-set mob counts +- for (ServerPlayer player : this.level.players) { +- // Paper start - per player mob spawning backoff +- for (int ii = 0; ii < ServerPlayer.MOBCATEGORY_TOTAL_ENUMS; ii++) { +- player.mobCounts[ii] = 0; +- +- int newBackoff = player.mobBackoffCounts[ii] - 1; // TODO make configurable bleed // TODO use nonlinear algorithm? +- if (newBackoff < 0) { +- newBackoff = 0; ++ if (!org.bxteam.divinemc.DivineConfig.enableAsyncSpawning) { ++ // re-set mob counts ++ for (ServerPlayer player : this.level.players) { ++ // Paper start - per player mob spawning backoff ++ for (int ii = 0; ii < ServerPlayer.MOBCATEGORY_TOTAL_ENUMS; ii++) { ++ player.mobCounts[ii] = 0; ++ ++ int newBackoff = player.mobBackoffCounts[ii] - 1; // TODO make configurable bleed // TODO use nonlinear algorithm? ++ if (newBackoff < 0) { ++ newBackoff = 0; ++ } ++ player.mobBackoffCounts[ii] = newBackoff; + } +- player.mobBackoffCounts[ii] = newBackoff; ++ // Paper end - per player mob spawning backoff + } +- // Paper end - per player mob spawning backoff ++ lastSpawnState = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, new LocalMobCapCalculator(this.chunkMap), true); + } +- spawnState = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, null, true); + } else { +- spawnState = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, !this.level.paperConfig().entities.spawning.perPlayerMobSpawns ? new LocalMobCapCalculator(this.chunkMap) : null, false); ++ lastSpawnState = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, new LocalMobCapCalculator(this.chunkMap), false); ++ spawnCountsReady.set(true); + } ++ // DivineMC end - Async mob spawning + // Paper end - Optional per player mob spawns +- this.lastSpawnState = spawnState; + profiler.popPush("spawnAndTick"); + boolean _boolean = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && !this.level.players().isEmpty(); // CraftBukkit + int _int = this.level.getGameRules().getInt(GameRules.RULE_RANDOMTICKING); +@@ -688,7 +725,7 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon + } + // Paper end - PlayerNaturallySpawnCreaturesEvent + boolean flag = this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) != 0L && this.level.getLevelData().getGameTime() % this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) == 0L; // CraftBukkit +- filteredSpawningCategories = NaturalSpawner.getFilteredSpawningCategories(spawnState, this.spawnFriendlies, this.spawnEnemies, flag, this.level); // CraftBukkit ++ filteredSpawningCategories = NaturalSpawner.getFilteredSpawningCategories(lastSpawnState, this.spawnFriendlies, this.spawnEnemies, flag, this.level); // CraftBukkit // DivineMC - Async mob spawning + } else { + filteredSpawningCategories = List.of(); + } +@@ -696,8 +733,10 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon + for (LevelChunk levelChunk : chunks) { + ChunkPos pos = levelChunk.getPos(); + levelChunk.incrementInhabitedTime(timeInhabited); +- if (!filteredSpawningCategories.isEmpty() && this.level.getWorldBorder().isWithinBounds(pos) && this.chunkMap.anyPlayerCloseEnoughForSpawning(pos, true)) { // Spigot +- NaturalSpawner.spawnForChunk(this.level, levelChunk, spawnState, filteredSpawningCategories); ++ // DivineMC start - Async mob spawning ++ if (!filteredSpawningCategories.isEmpty() && this.level.getWorldBorder().isWithinBounds(pos) && (!org.bxteam.divinemc.DivineConfig.enableAsyncSpawning || spawnCountsReady.get()) && this.chunkMap.anyPlayerCloseEnoughForSpawning(pos, true)) { // Spigot ++ NaturalSpawner.spawnForChunk(this.level, levelChunk, lastSpawnState, filteredSpawningCategories); ++ // DivineMC end - Async mob spawning + } + + if (true) { // Paper - rewrite chunk system +diff --git a/net/minecraft/world/level/NaturalSpawner.java b/net/minecraft/world/level/NaturalSpawner.java +index 8aeeeffae50b3ec68ab972e793bb5edb4530ca78..3ba648ead9680ea9a4c7962543caa24357015cca 100644 +--- a/net/minecraft/world/level/NaturalSpawner.java ++++ b/net/minecraft/world/level/NaturalSpawner.java +@@ -157,10 +157,21 @@ public final class NaturalSpawner { + return list; + } + ++ private static int maxCapPerPlayer = -1; // DivineMC - Async mob spawning ++ + public static void spawnForChunk(ServerLevel level, LevelChunk chunk, NaturalSpawner.SpawnState spawnState, List categories) { + ProfilerFiller profilerFiller = Profiler.get(); + profilerFiller.push("spawner"); + ++ // DivineMC start - Async mob spawning ++ if (maxCapPerPlayer < 0) { ++ maxCapPerPlayer = 0; ++ for (final MobCategory value : MobCategory.values()) { ++ maxCapPerPlayer += value.getMaxInstancesPerChunk(); ++ } ++ } ++ // DivineMC end - Async mob spawning ++ + for (MobCategory mobCategory : categories) { + // Paper start - Optional per player mob spawns + final boolean canSpawn; +@@ -649,6 +660,13 @@ public final class NaturalSpawner { + } + + boolean canSpawnForCategoryLocal(MobCategory category, ChunkPos chunkPos) { ++ // DivineMC start - Async mob spawning ++ if (this.localMobCapCalculator == null) { ++ LOGGER.warn("Local mob cap calculator was null! Report to DivineMC!"); ++ return false; ++ } ++ // DivineMC end - Async mob spawning ++ + return this.localMobCapCalculator.canSpawn(category, chunkPos); + } + } diff --git a/divinemc-server/src/main/java/org/bxteam/divinemc/DivineConfig.java b/divinemc-server/src/main/java/org/bxteam/divinemc/DivineConfig.java index 9b3d578..bd91fd8 100644 --- a/divinemc-server/src/main/java/org/bxteam/divinemc/DivineConfig.java +++ b/divinemc-server/src/main/java/org/bxteam/divinemc/DivineConfig.java @@ -299,6 +299,7 @@ public class DivineConfig { public static boolean forceMinecraftCommand = false; public static boolean disableLeafDecay = false; public static boolean commandBlockParseResultsCaching = true; + public static boolean enableAsyncSpawning = true; private static void miscSettings() { skipUselessSecondaryPoiSensor = getBoolean("settings.misc.skip-useless-secondary-poi-sensor", skipUselessSecondaryPoiSensor); clumpOrbs = getBoolean("settings.misc.clump-orbs", clumpOrbs, @@ -318,6 +319,8 @@ public class DivineConfig { "Disables leaf block decay."); commandBlockParseResultsCaching = getBoolean("settings.misc.command-block-parse-results-caching", commandBlockParseResultsCaching, "Caches the parse results of command blocks, can significantly reduce performance impact."); + enableAsyncSpawning = getBoolean("settings.misc.enable-async-spawning", enableAsyncSpawning, + "Enables optimization that will offload much of the computational effort involved with spawning new mobs to a different thread."); } public static boolean disableDisconnectSpam = false; diff --git a/divinemc-server/src/main/java/org/bxteam/divinemc/util/AsyncProcessor.java b/divinemc-server/src/main/java/org/bxteam/divinemc/util/AsyncProcessor.java new file mode 100644 index 0000000..76ca616 --- /dev/null +++ b/divinemc-server/src/main/java/org/bxteam/divinemc/util/AsyncProcessor.java @@ -0,0 +1,59 @@ +package org.bxteam.divinemc.util; + +import ca.spottedleaf.moonrise.common.util.TickThread; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +public class AsyncProcessor { + private static final Logger LOGGER = LogManager.getLogger(AsyncProcessor.class); + + private final BlockingQueue taskQueue; + private final Thread workerThread; + private volatile boolean isRunning; + + public AsyncProcessor(String threadName) { + this.taskQueue = new LinkedBlockingQueue<>(); + this.isRunning = true; + + this.workerThread = new TickThread(() -> { + while (isRunning || !taskQueue.isEmpty()) { + try { + Runnable task = taskQueue.take(); + task.run(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } catch (Exception e) { + LOGGER.error("An unexpected error occurred when running async processor: {}", e.getMessage(), e); + } + } + }, threadName); + + this.workerThread.start(); + } + + public void submit(Runnable task) { + if (!isRunning) { + throw new IllegalStateException("AsyncExecutor is not running."); + } + + taskQueue.offer(task); + } + + public void shutdown() { + isRunning = false; + workerThread.interrupt(); + } + + public void shutdownNow() { + isRunning = false; + workerThread.interrupt(); + taskQueue.clear(); + } + + public boolean isRunning() { + return isRunning; + } +}