From 9218371c67cf3692d5afc7c84aa6095ada8eb88e Mon Sep 17 00:00:00 2001 From: dan28000 <84990628+dan28000@users.noreply.github.com> Date: Tue, 28 Oct 2025 00:29:04 +0100 Subject: [PATCH] RCT update (#37) * Move regionized chunk ticking into it's own package * minor fix * rename config variable and move it --- .../0052-Regionized-Chunk-Ticking.patch | 391 ++++++------------ .../0062-Cleanup-dead-code-from-Paper.patch | 8 +- .../0069-Optimize-level-ticking.patch | 6 +- .../features/0070-Optimize-Moonrise.patch | 12 +- .../0073-lithium-sleeping_block_entity.patch | 4 +- .../bxteam/divinemc/async/rct/RegionData.java | 11 + .../async/rct/RegionizedChunkTicking.java | 245 +++++++++++ .../bxteam/divinemc/async/rct/TickPair.java | 7 + .../bxteam/divinemc/config/DivineConfig.java | 3 + 9 files changed, 412 insertions(+), 275 deletions(-) create mode 100644 divinemc-server/src/main/java/org/bxteam/divinemc/async/rct/RegionData.java create mode 100644 divinemc-server/src/main/java/org/bxteam/divinemc/async/rct/RegionizedChunkTicking.java create mode 100644 divinemc-server/src/main/java/org/bxteam/divinemc/async/rct/TickPair.java diff --git a/divinemc-server/minecraft-patches/features/0052-Regionized-Chunk-Ticking.patch b/divinemc-server/minecraft-patches/features/0052-Regionized-Chunk-Ticking.patch index 464d920..a6ccee7 100644 --- a/divinemc-server/minecraft-patches/features/0052-Regionized-Chunk-Ticking.patch +++ b/divinemc-server/minecraft-patches/features/0052-Regionized-Chunk-Ticking.patch @@ -20,19 +20,33 @@ index 411e1284a208ca1a097cf6eaa92e1e0d2203d83d..3f60d1b0ac91cfd3418e791222cd7267 } catch (Exception var2) { if (var2 instanceof ClosedChannelException) { LOGGER.info("Connection closed during protocol change"); +diff --git a/net/minecraft/server/level/ChunkMap.java b/net/minecraft/server/level/ChunkMap.java +index 7ca147cf9da67c399806056e5092841f7ca32321..b77f702398d7c04258974cc7cd25ed044603b417 100644 +--- a/net/minecraft/server/level/ChunkMap.java ++++ b/net/minecraft/server/level/ChunkMap.java +@@ -730,7 +730,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + void collectSpawningChunks(List output) { + final ca.spottedleaf.moonrise.common.list.ReferenceList tickingChunks = ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel)this.level).moonrise$getPlayerTickingChunks(); + +- final LevelChunk[] raw = tickingChunks.getRawDataUnchecked(); ++ final LevelChunk[] raw = tickingChunks.toArray(new LevelChunk[0]); //DivineMC Regionized Chunk Ticking sync fix + final int size = tickingChunks.size(); + + Objects.checkFromToIndex(0, size, raw.length); diff --git a/net/minecraft/server/level/ServerChunkCache.java b/net/minecraft/server/level/ServerChunkCache.java -index 2039e636b1a52aff5403621e7281d618e4b87864..45bf13dc23b886ea2d660c38c74becf0e3754963 100644 +index 2039e636b1a52aff5403621e7281d618e4b87864..6c0398a8118463ef7c606229cc0078324ebba3f5 100644 --- a/net/minecraft/server/level/ServerChunkCache.java +++ b/net/minecraft/server/level/ServerChunkCache.java -@@ -57,6 +57,7 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon +@@ -56,7 +56,7 @@ import org.slf4j.Logger; + public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemServerChunkCache { // Paper - rewrite chunk system private static final Logger LOGGER = LogUtils.getLogger(); private final DistanceManager distanceManager; - private final ServerLevel level; -+ public static final Executor REGION_EXECUTOR = java.util.concurrent.Executors.newFixedThreadPool(org.bxteam.divinemc.config.DivineConfig.AsyncCategory.regionizedChunkTickingExecutorThreadCount, new org.bxteam.divinemc.util.NamedAgnosticThreadFactory<>("Region Ticking", ca.spottedleaf.moonrise.common.util.TickThread::new, org.bxteam.divinemc.config.DivineConfig.AsyncCategory.regionizedChunkTickingExecutorThreadPriority)); // DivineMC - Regionized Chunk Ticking +- private final ServerLevel level; ++ protected final ServerLevel level; //DivineMC Regionized Chunk Ticking private -> protected public final Thread mainThread; final ThreadedLevelLightEngine lightEngine; public final ServerChunkCache.MainThreadExecutor mainThreadProcessor; -@@ -70,8 +71,10 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon +@@ -70,8 +70,10 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon private final long[] lastChunkPos = new long[4]; private final ChunkStatus[] lastChunkStatus = new ChunkStatus[4]; private final ChunkAccess[] lastChunk = new ChunkAccess[4]; @@ -45,141 +59,15 @@ index 2039e636b1a52aff5403621e7281d618e4b87864..45bf13dc23b886ea2d660c38c74becf0 @Nullable @VisibleForDebug private NaturalSpawner.SpawnState lastSpawnState; -@@ -153,36 +156,253 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon +@@ -153,36 +155,49 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon return load ? this.syncLoad(chunkX, chunkZ, toStatus) : null; } // Paper end - rewrite chunk system -+ -+ // DivineMC start - Regionized Chunk Ticking -+ private record RegionData(it.unimi.dsi.fastutil.longs.LongOpenHashSet chunks, Set entities) { -+ public boolean isEmpty() { -+ return chunks.isEmpty(); -+ } -+ } -+ -+ private record Output(RegionData[] regions, Set entities) {} -+ -+ private Output computePlayerRegionsParallel() { -+ int tickViewDistance = level.moonrise$getViewDistanceHolder().getViewDistances().tickViewDistance(); -+ List players = new java.util.ArrayList<>(level.players); -+ int max = maxChunksForViewDistance(); -+ -+ List playerChunkSets = players.parallelStream() -+ .map(player -> { -+ ChunkPos playerChunk = player.chunkPosition(); -+ int px = playerChunk.x; -+ int pz = playerChunk.z; -+ it.unimi.dsi.fastutil.longs.LongOpenHashSet chunkKeys = new it.unimi.dsi.fastutil.longs.LongOpenHashSet(max); -+ for (int dx = -tickViewDistance; dx <= tickViewDistance; dx++) { -+ for (int dz = -tickViewDistance; dz <= tickViewDistance; dz++) { -+ long key = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(px + dx, pz + dz); -+ chunkKeys.add(key); -+ } -+ } -+ return chunkKeys; -+ }).toList(); -+ -+ List mergedRegions = new java.util.ArrayList<>(); -+ boolean[] merged = new boolean[playerChunkSets.size()]; -+ -+ for (int i = 0; i < playerChunkSets.size(); i++) { -+ if (merged[i]) continue; -+ -+ it.unimi.dsi.fastutil.longs.LongOpenHashSet region = new it.unimi.dsi.fastutil.longs.LongOpenHashSet(playerChunkSets.get(i)); -+ merged[i] = true; -+ -+ boolean madeChanges; -+ do { -+ madeChanges = false; -+ for (int j = i + 1; j < playerChunkSets.size(); j++) { -+ if (merged[j]) continue; -+ -+ it.unimi.dsi.fastutil.longs.LongOpenHashSet set = playerChunkSets.get(j); -+ -+ boolean hasIntersection = false; -+ it.unimi.dsi.fastutil.longs.LongIterator iter = set.iterator(); -+ while (iter.hasNext()) { -+ if (region.contains(iter.nextLong())) { -+ hasIntersection = true; -+ break; -+ } -+ } -+ -+ if (hasIntersection) { -+ region.addAll(set); -+ merged[j] = true; -+ madeChanges = true; -+ } -+ } -+ } while (madeChanges); -+ -+ mergedRegions.add(region); -+ } -+ -+ ObjectArrayList regions = new ObjectArrayList<>(); -+ it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap chunkToRegion = new it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap(max * mergedRegions.size()); -+ chunkToRegion.defaultReturnValue(-1); -+ for (int i = 0; i < mergedRegions.size(); i++) { -+ regions.add(new RegionData(mergedRegions.get(i), java.util.Collections.newSetFromMap(new java.util.concurrent.ConcurrentHashMap<>()))); -+ for (long key : mergedRegions.get(i)) { -+ chunkToRegion.put(key, i); -+ } -+ } -+ -+ final Set firstTick = java.util.Collections.newSetFromMap(new java.util.concurrent.ConcurrentHashMap<>()); -+ -+ synchronized (level.entityTickList.entities) { -+ final ca.spottedleaf.moonrise.common.list.IteratorSafeOrderedReferenceSet.Iterator iterator = level.entityTickList.entities.iterator(); -+ try { -+ while (iterator.hasNext()) { -+ Entity entity = iterator.next(); -+ long chunkKey = entity.chunkPosition().longKey; -+ int regionIndex = chunkToRegion.get(chunkKey); -+ if (regionIndex != -1) { -+ regions.get(regionIndex).entities().add(entity); -+ } else { -+ firstTick.add(entity); -+ } -+ } -+ } finally { -+ iterator.finishedIterating(); -+ } -+ } -+ -+ return new Output(regions.toArray(new RegionData[0]), firstTick); -+ } -+ -+ // Should be max safe estimate of ticking chunks in a region -+ private int maxChunksForViewDistance() { -+ ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistances distances = level.moonrise$getViewDistanceHolder().getViewDistances(); -+ int diameter = 2 * distances.tickViewDistance() + 1; -+ return diameter * diameter; -+ } -+ -+ private void tickEntity(Entity entity) { -+ if (!entity.isRemoved()) { -+ if (!level.tickRateManager().isEntityFrozen(entity)) { -+ entity.checkDespawn(); -+ // Paper - rewrite chunk system -+ Entity vehicle = entity.getVehicle(); -+ if (vehicle != null) { -+ if (!vehicle.isRemoved() && vehicle.hasPassenger(entity)) { -+ return; -+ } -+ -+ entity.stopRiding(); -+ } -+ -+ level.guardEntityTick(level::tickNonPassenger, entity); -+ } -+ } -+ } -+ // DivineMC end - Regionized Chunk Ticking + // Paper start - chunk tick iteration optimisations private final ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom shuffleRandom = new ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom(0L); - private void iterateTickingChunksFaster() { -+ private void iterateTickingChunksFaster(final java.util.concurrent.CompletableFuture spawns) { // DivineMC - Regionized Chunk Ticking ++ private void iterateTickingChunksFaster(final CompletableFuture spawns) { // DivineMC - Regionized Chunk Ticking final ServerLevel world = this.level; final int randomTickSpeed = world.getGameRules().getInt(GameRules.RULE_RANDOMTICKING); @@ -198,98 +86,17 @@ index 2039e636b1a52aff5403621e7281d618e4b87864..45bf13dc23b886ea2d660c38c74becf0 - java.util.Objects.checkFromToIndex(0, size, raw.length); - for (int i = 0; i < size; ++i) { - world.tickChunk(raw[i], randomTickSpeed); -+ // DivineMC start - Regionized Chunk Ticking -+ if (org.bxteam.divinemc.config.DivineConfig.AsyncCategory.enableRegionizedChunkTicking) { -+ final Output output = computePlayerRegionsParallel(); -+ final RegionData[] regions = output.regions(); -+ int regionCount = regions.length; -+ -+ java.util.concurrent.CountDownLatch latch = new java.util.concurrent.CountDownLatch(regionCount); -+ io.papermc.paper.entity.activation.ActivationRange.activateEntities(level); // Paper - EAR -+ -+ try { -+ java.util.concurrent.ForkJoinPool.managedBlock(new java.util.concurrent.ForkJoinPool.ManagedBlocker() { -+ @Override -+ public boolean block() throws InterruptedException { -+ ObjectArrayList> ticked = new ObjectArrayList<>(regionCount); -+ for (final RegionData region : regions) { -+ if (region == null || region.isEmpty()) { -+ latch.countDown(); -+ continue; -+ } -+ -+ ticked.add(java.util.concurrent.CompletableFuture.supplyAsync(() -> { -+ ObjectArrayList regionChunks = new ObjectArrayList<>(region.chunks().size()); -+ it.unimi.dsi.fastutil.longs.LongOpenHashSet regionChunksIDs = new it.unimi.dsi.fastutil.longs.LongOpenHashSet(region.chunks().size()); -+ for (long key : region.chunks()) { -+ LevelChunk chunk = fullChunks.get(key); -+ if (chunk != null) { -+ regionChunks.add(chunk); -+ regionChunksIDs.add(key); -+ } -+ } -+ -+ try { -+ for (LevelChunk chunk : regionChunks) { -+ world.tickChunk(chunk, randomTickSpeed); -+ } -+ for (net.minecraft.world.entity.Entity entity : region.entities()) { -+ tickEntity(entity); -+ } -+ } finally { -+ latch.countDown(); -+ } -+ return regionChunksIDs; -+ }, REGION_EXECUTOR)); -+ } - +- - // call mid-tick tasks for chunk system - if ((i & 7) == 0) { - // DivineMC start - Parallel world ticking - if (!org.bxteam.divinemc.config.DivineConfig.AsyncCategory.enableParallelWorldTicking) { - ((ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer) this.level.getServer()).moonrise$executeMidTickTasks(); - continue; -+ CompletableFuture.runAsync(() -> { -+ try { -+ CompletableFuture.allOf(ticked.toArray(new CompletableFuture[0])).join(); -+ } catch (java.util.concurrent.CompletionException ex) { -+ LOGGER.error("Error during region chunk ticking", ex.getCause()); -+ } -+ -+ it.unimi.dsi.fastutil.longs.LongOpenHashSet tickedChunkKeys = new it.unimi.dsi.fastutil.longs.LongOpenHashSet(raw.length); -+ -+ for (CompletableFuture future : ticked) { -+ if (!future.isCompletedExceptionally()) { -+ try { -+ it.unimi.dsi.fastutil.longs.LongOpenHashSet regionChunks = future.join(); -+ tickedChunkKeys.addAll(regionChunks); -+ } catch (Exception e) { -+ LOGGER.error("Exception in region ticking future", e); -+ } -+ } -+ } -+ -+ for (LevelChunk chunk : raw) { -+ if (!tickedChunkKeys.contains(chunk.coordinateKey)) { -+ world.tickChunk(chunk, randomTickSpeed); -+ } -+ } -+ -+ output.entities.forEach(ServerChunkCache.this::tickEntity); -+ spawns.join(); -+ }, REGION_EXECUTOR).join(); -+ -+ latch.await(); -+ return true; -+ } -+ -+ @Override -+ public boolean isReleasable() { -+ return latch.getCount() == 0; -+ } -+ }); -+ } catch (InterruptedException ex) { -+ throw new RuntimeException("Interrupted managed block during region ticking", ex); ++ // DivineMC start - Regionized Chunk Ticking ++ if (org.bxteam.divinemc.config.DivineConfig.AsyncCategory.enableRegionizedChunkTicking) { ++ if (this instanceof org.bxteam.divinemc.async.rct.RegionizedChunkTicking rct) { ++ rct.execute(spawns, raw); + } + } else { + java.util.Objects.checkFromToIndex(0, size, raw.length); @@ -306,13 +113,17 @@ index 2039e636b1a52aff5403621e7281d618e4b87864..45bf13dc23b886ea2d660c38c74becf0 + // DivineMC end - Parallel world ticking } - // DivineMC end - Parallel world ticking ++ } ++ ++ if (org.bxteam.divinemc.config.DivineConfig.AsyncCategory.asyncNaturalSpawn) { ++ spawns.join(); } } + // DivineMC end - Regionized Chunk Ticking } // Paper end - chunk tick iteration optimisations -@@ -507,14 +727,21 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon +@@ -507,14 +522,21 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon long gameTime = this.level.getGameTime(); long l = gameTime - this.lastInhabitedUpdate; this.lastInhabitedUpdate = gameTime; @@ -338,7 +149,7 @@ index 2039e636b1a52aff5403621e7281d618e4b87864..45bf13dc23b886ea2d660c38c74becf0 // DivineMC start - Pufferfish: Optimize mob spawning if (org.bxteam.divinemc.config.DivineConfig.AsyncCategory.enableAsyncSpawning) { for (ServerPlayer player : this.level.players) { -@@ -558,14 +785,18 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon +@@ -558,14 +580,18 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon } private void broadcastChangedChunks() { @@ -363,51 +174,59 @@ index 2039e636b1a52aff5403621e7281d618e4b87864..45bf13dc23b886ea2d660c38c74becf0 } private void tickChunks(long timeInhabited) { -@@ -615,23 +846,28 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon +@@ -615,13 +641,32 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon filteredSpawningCategories = List.of(); } -- List list = this.spawningChunks; + // DivineMC start - Regionized Chunk Ticking -+ final java.util.concurrent.CompletableFuture spawns = java.util.concurrent.CompletableFuture.runAsync(() -> { -+ List list = this.spawningChunks; - -- try { -- this.chunkMap.collectSpawningChunks(list); -- // Paper start - chunk tick iteration optimisation -- this.shuffleRandom.setSeed(this.level.random.nextLong()); -- if (!this.level.paperConfig().entities.spawning.perPlayerMobSpawns) Util.shuffle(list, this.shuffleRandom); // Paper - Optional per player mob spawns; do not need this when per-player is enabled -- // Paper end - chunk tick iteration optimisation -- -- for (LevelChunk levelChunk : list) { -- this.tickSpawningChunk(levelChunk, timeInhabited, filteredSpawningCategories, lastSpawnState); // DivineMC - Pufferfish: Optimize mob spawning -+ try { -+ this.chunkMap.collectSpawningChunks(list); -+ // Paper start - chunk tick iteration optimisation -+ this.shuffleRandom.setSeed(this.level.random.nextLong()); -+ if (!this.level.paperConfig().entities.spawning.perPlayerMobSpawns) -+ Util.shuffle(list, this.shuffleRandom); // Paper - Optional per player mob spawns; do not need this when per-player is enabled -+ // Paper end - chunk tick iteration optimisation -+ -+ for (LevelChunk levelChunk : list) { -+ this.tickSpawningChunk(levelChunk, timeInhabited, filteredSpawningCategories, lastSpawnState); // DivineMC - Pufferfish: Optimize mob spawning -+ } -+ } finally { -+ list.clear(); - } -- } finally { -- list.clear(); -- } -+ }, REGION_EXECUTOR); ++ final CompletableFuture spawns; ++ if (org.bxteam.divinemc.config.DivineConfig.AsyncCategory.asyncNaturalSpawn) { ++ spawns = CompletableFuture.runAsync(() -> naturalSpawn(filteredSpawningCategories, timeInhabited), org.bxteam.divinemc.async.rct.RegionizedChunkTicking.REGION_EXECUTOR); ++ } else { ++ naturalSpawn(filteredSpawningCategories, timeInhabited); ++ spawns = new CompletableFuture<>(); ++ } + // DivineMC end - Regionized Chunk Ticking ++ ++ this.iterateTickingChunksFaster(spawns); // Paper - chunk tick iteration optimisations // DivineMC - Regionized Chunk Ticking ++ if (_boolean) { ++ this.level.tickCustomSpawners(this.spawnEnemies, this.spawnFriendlies); ++ } ++ } ++ ++ // DivineMC start - Regionized Chunk Ticking ++ private void naturalSpawn(List filteredSpawningCategories, long timeInhabited) { + List list = this.spawningChunks; + + try { + this.chunkMap.collectSpawningChunks(list); + // Paper start - chunk tick iteration optimisation + this.shuffleRandom.setSeed(this.level.random.nextLong()); +- if (!this.level.paperConfig().entities.spawning.perPlayerMobSpawns) Util.shuffle(list, this.shuffleRandom); // Paper - Optional per player mob spawns; do not need this when per-player is enabled ++ if (!this.level.paperConfig().entities.spawning.perPlayerMobSpawns) ++ Util.shuffle(list, this.shuffleRandom); // Paper - Optional per player mob spawns; do not need this when per-player is enabled + // Paper end - chunk tick iteration optimisation + + for (LevelChunk levelChunk : list) { +@@ -630,12 +675,12 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon + } finally { + list.clear(); + } ++ } - this.iterateTickingChunksFaster(); // Paper - chunk tick iteration optimisations -+ this.iterateTickingChunksFaster(spawns); // Paper - chunk tick iteration optimisations // DivineMC - Regionized Chunk Ticking - if (_boolean) { - this.level.tickCustomSpawners(this.spawnEnemies, this.spawnFriendlies); - } +- if (_boolean) { +- this.level.tickCustomSpawners(this.spawnEnemies, this.spawnFriendlies); +- } ++ protected net.minecraft.world.level.entity.EntityTickList getEntityTickList() { ++ return level.entityTickList; + } ++ // DivineMC end - Regionized Chunk Ticking + + private void tickSpawningChunk(LevelChunk chunk, long timeInhabited, List spawnCategories, NaturalSpawner.SpawnState spawnState) { + ChunkPos pos = chunk.getPos(); diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java -index f9091b2daf735fd0788f8d6d60e3c812fd6dd4f2..0ad18866c323308ad9b87322932e03a283f740b1 100644 +index f9091b2daf735fd0788f8d6d60e3c812fd6dd4f2..6c59d11107958f835ebe09317ed0d129f64d4583 100644 --- a/net/minecraft/server/level/ServerLevel.java +++ b/net/minecraft/server/level/ServerLevel.java @@ -191,7 +191,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe @@ -419,7 +238,59 @@ index f9091b2daf735fd0788f8d6d60e3c812fd6dd4f2..0ad18866c323308ad9b87322932e03a2 volatile boolean isUpdatingNavigations; protected final Raids raids; private final ObjectLinkedOpenHashSet blockEvents = new ObjectLinkedOpenHashSet<>(); -@@ -806,6 +806,13 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -640,20 +640,37 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + boolean flag = server.forceSynchronousWrites(); + DataFixer fixerUpper = server.getFixerUpper(); + // Paper - rewrite chunk system +- this.chunkSource = new ServerChunkCache( +- this, +- levelStorageAccess, +- fixerUpper, +- server.getStructureManager(), +- dispatcher, +- chunkGenerator, +- this.spigotConfig.viewDistance, // Spigot +- this.spigotConfig.simulationDistance, // Spigot +- flag, +- progressListener, +- null, // Paper - rewrite chunk system +- () -> server.overworld().getDataStorage() +- ); ++ if (org.bxteam.divinemc.config.DivineConfig.AsyncCategory.enableRegionizedChunkTicking) { ++ this.chunkSource = new org.bxteam.divinemc.async.rct.RegionizedChunkTicking(this, ++ levelStorageAccess, ++ fixerUpper, ++ server.getStructureManager(), ++ dispatcher, ++ chunkGenerator, ++ this.spigotConfig.viewDistance, // Spigot ++ this.spigotConfig.simulationDistance, // Spigot ++ flag, ++ progressListener, ++ null, // Paper - rewrite chunk system ++ () -> server.overworld().getDataStorage() ++ ); ++ } else { ++ this.chunkSource = new ServerChunkCache( ++ this, ++ levelStorageAccess, ++ fixerUpper, ++ server.getStructureManager(), ++ dispatcher, ++ chunkGenerator, ++ this.spigotConfig.viewDistance, // Spigot ++ this.spigotConfig.simulationDistance, // Spigot ++ flag, ++ progressListener, ++ null, // Paper - rewrite chunk system ++ () -> server.overworld().getDataStorage() ++ ); ++ } ++ + this.chunkSource.getGeneratorState().ensureStructuresGenerated(); + this.portalForcer = new PortalForcer(this); + this.updateSkyBrightness(); +@@ -806,6 +823,13 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe this.dragonFight.tick(); } @@ -433,7 +304,7 @@ index f9091b2daf735fd0788f8d6d60e3c812fd6dd4f2..0ad18866c323308ad9b87322932e03a2 io.papermc.paper.entity.activation.ActivationRange.activateEntities(this); // Paper - EAR this.entityTickList .forEach( -@@ -1828,22 +1835,16 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -1828,22 +1852,16 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe if (Shapes.joinIsNotEmpty(collisionShape, collisionShape1, BooleanOp.NOT_SAME)) { List list = new ObjectArrayList<>(); diff --git a/divinemc-server/minecraft-patches/features/0062-Cleanup-dead-code-from-Paper.patch b/divinemc-server/minecraft-patches/features/0062-Cleanup-dead-code-from-Paper.patch index 5c2e74f..3e74b64 100644 --- a/divinemc-server/minecraft-patches/features/0062-Cleanup-dead-code-from-Paper.patch +++ b/divinemc-server/minecraft-patches/features/0062-Cleanup-dead-code-from-Paper.patch @@ -73,10 +73,10 @@ index aa4dd7517e8be167aef1eaf7aa907e3ce7cc0e62..e3d3b062e273fee4a9d3ba3cadc21278 - // Paper end - detailed watchdog information } diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java -index 0ad18866c323308ad9b87322932e03a283f740b1..386fdc23b35675a7db66d16bf2a8a6dd5b44059a 100644 +index 6c59d11107958f835ebe09317ed0d129f64d4583..ab14baf5a8a0fd7090702390197366b5a118cc6f 100644 --- a/net/minecraft/server/level/ServerLevel.java +++ b/net/minecraft/server/level/ServerLevel.java -@@ -1349,13 +1349,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -1366,13 +1366,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe // Paper end - log detailed entity tick information public void tickNonPassenger(Entity entity) { @@ -90,7 +90,7 @@ index 0ad18866c323308ad9b87322932e03a283f740b1..386fdc23b35675a7db66d16bf2a8a6dd entity.setOldPosAndRot(); entity.tickCount++; entity.totalEntityAge++; // Paper - age-like counter for all entities -@@ -1368,13 +1362,6 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -1385,13 +1379,6 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe for (Entity entity1 : entity.getPassengers()) { this.tickPassenger(entity, entity1, isActive); // Paper - EAR 2 } @@ -105,7 +105,7 @@ index 0ad18866c323308ad9b87322932e03a283f740b1..386fdc23b35675a7db66d16bf2a8a6dd private void tickPassenger(Entity ridingEntity, Entity passengerEntity, final boolean isActive) { // Paper - EAR 2 diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java -index fa48496222ea922204163d48988246c44e09851f..2c7c5dab268625e1328f57ac3ec2a735a82fea42 100644 +index a5f6b4827019d2041d2b4bad5eab34e1bb1cbe23..d82ed6dcd49b4f0bd040d2bd3a6f0f54cd87758f 100644 --- a/net/minecraft/world/entity/Entity.java +++ b/net/minecraft/world/entity/Entity.java @@ -1111,29 +1111,10 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess diff --git a/divinemc-server/minecraft-patches/features/0069-Optimize-level-ticking.patch b/divinemc-server/minecraft-patches/features/0069-Optimize-level-ticking.patch index b5cc125..14b7d2f 100644 --- a/divinemc-server/minecraft-patches/features/0069-Optimize-level-ticking.patch +++ b/divinemc-server/minecraft-patches/features/0069-Optimize-level-ticking.patch @@ -5,10 +5,10 @@ Subject: [PATCH] Optimize level ticking diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java -index 386fdc23b35675a7db66d16bf2a8a6dd5b44059a..4934ce03ac533d9c60674632cdac6621e62f6b44 100644 +index ab14baf5a8a0fd7090702390197366b5a118cc6f..6e6f44d60b36d935cf004b856b6c1d6d1633f406 100644 --- a/net/minecraft/server/level/ServerLevel.java +++ b/net/minecraft/server/level/ServerLevel.java -@@ -908,9 +908,10 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -925,9 +925,10 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe // Paper start - optimise random ticking private final ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom simpleRandom = new ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom(net.minecraft.world.level.levelgen.RandomSupport.generateUniqueSeed()); @@ -20,7 +20,7 @@ index 386fdc23b35675a7db66d16bf2a8a6dd5b44059a..4934ce03ac533d9c60674632cdac6621 final ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom simpleRandom = this.simpleRandom; final boolean doubleTickFluids = !ca.spottedleaf.moonrise.common.PlatformHooks.get().configFixMC224294(); -@@ -919,42 +920,38 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -936,42 +937,38 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe final int offsetZ = cpos.z << 4; for (int sectionIndex = 0, sectionsLen = sections.length; sectionIndex < sectionsLen; sectionIndex++) { diff --git a/divinemc-server/minecraft-patches/features/0070-Optimize-Moonrise.patch b/divinemc-server/minecraft-patches/features/0070-Optimize-Moonrise.patch index 431d745..e17f3fb 100644 --- a/divinemc-server/minecraft-patches/features/0070-Optimize-Moonrise.patch +++ b/divinemc-server/minecraft-patches/features/0070-Optimize-Moonrise.patch @@ -465,10 +465,10 @@ index 50bc5b940812432bc472e5b272582efb8bbfc7a7..0bece4ed69b332174cbe37f82df1f7da } diff --git a/net/minecraft/server/level/ServerChunkCache.java b/net/minecraft/server/level/ServerChunkCache.java -index 45bf13dc23b886ea2d660c38c74becf0e3754963..b05fb5946564264771f998cff418513916eb6adb 100644 +index df84bd2a2e306776ba92da74168b7c6c06c94beb..2da02f04606be99bfb691207886b623c1a35af7d 100644 --- a/net/minecraft/server/level/ServerChunkCache.java +++ b/net/minecraft/server/level/ServerChunkCache.java -@@ -672,8 +672,7 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon +@@ -467,8 +467,7 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon public boolean isPositionTicking(long chunkPos) { // Paper start - rewrite chunk system @@ -479,7 +479,7 @@ index 45bf13dc23b886ea2d660c38c74becf0e3754963..b05fb5946564264771f998cff4185139 } diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java -index 4934ce03ac533d9c60674632cdac6621e62f6b44..b50afea7c2e4c61a3df196e74afd8f82b30aca8a 100644 +index 6e6f44d60b36d935cf004b856b6c1d6d1633f406..9387bf252af4dfe0eddd3ab9ce6fe7eb83b7e4ad 100644 --- a/net/minecraft/server/level/ServerLevel.java +++ b/net/minecraft/server/level/ServerLevel.java @@ -179,6 +179,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe @@ -490,7 +490,7 @@ index 4934ce03ac533d9c60674632cdac6621e62f6b44..b50afea7c2e4c61a3df196e74afd8f82 private int lastSpawnChunkRadius; final EntityTickList entityTickList = new EntityTickList(this); // DivineMC - Parallel world ticking private final ServerWaypointManager waypointManager; -@@ -691,6 +692,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -708,6 +709,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe // Paper start - rewrite chunk system this.moonrise$setEntityLookup(new ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server.ServerEntityLookup((ServerLevel)(Object)this, ((ServerLevel)(Object)this).new EntityCallbacks())); this.chunkTaskScheduler = new ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler((ServerLevel)(Object)this); @@ -498,7 +498,7 @@ index 4934ce03ac533d9c60674632cdac6621e62f6b44..b50afea7c2e4c61a3df196e74afd8f82 this.entityDataController = new ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.EntityDataController( new ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.EntityDataController.EntityRegionFileStorage( new RegionStorageInfo(levelStorageAccess.getLevelId(), dimension, "entities"), -@@ -846,8 +848,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -863,8 +865,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe @Override public boolean shouldTickBlocksAt(long chunkPos) { // Paper start - rewrite chunk system @@ -508,7 +508,7 @@ index 4934ce03ac533d9c60674632cdac6621e62f6b44..b50afea7c2e4c61a3df196e74afd8f82 // Paper end - rewrite chunk system } -@@ -2584,16 +2585,13 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -2601,16 +2602,13 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe public boolean isPositionTickingWithEntitiesLoaded(long chunkPos) { // Paper start - rewrite chunk system diff --git a/divinemc-server/minecraft-patches/features/0073-lithium-sleeping_block_entity.patch b/divinemc-server/minecraft-patches/features/0073-lithium-sleeping_block_entity.patch index 624745e..5b8e676 100644 --- a/divinemc-server/minecraft-patches/features/0073-lithium-sleeping_block_entity.patch +++ b/divinemc-server/minecraft-patches/features/0073-lithium-sleeping_block_entity.patch @@ -71,10 +71,10 @@ index 3092454bf7071deca75fecfc203072593fe5c7e7..098dd4647ae1500195729d6531e90c2b } } diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java -index b50afea7c2e4c61a3df196e74afd8f82b30aca8a..dc5889c97b4aa1fe9be83b1c10c7c855e5a96d45 100644 +index 9387bf252af4dfe0eddd3ab9ce6fe7eb83b7e4ad..d5a0c2d6344de8bd57d56cff0e264df804772e84 100644 --- a/net/minecraft/server/level/ServerLevel.java +++ b/net/minecraft/server/level/ServerLevel.java -@@ -2438,6 +2438,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -2455,6 +2455,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe for (TickingBlockEntity tickingBlockEntity : this.blockEntityTickers) { BlockPos pos = tickingBlockEntity.getPos(); diff --git a/divinemc-server/src/main/java/org/bxteam/divinemc/async/rct/RegionData.java b/divinemc-server/src/main/java/org/bxteam/divinemc/async/rct/RegionData.java new file mode 100644 index 0000000..146765f --- /dev/null +++ b/divinemc-server/src/main/java/org/bxteam/divinemc/async/rct/RegionData.java @@ -0,0 +1,11 @@ +package org.bxteam.divinemc.async.rct; + +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import net.minecraft.world.entity.Entity; +import java.util.Set; + +record RegionData(LongOpenHashSet chunks, Set entities) { + public boolean isEmpty() { + return chunks.isEmpty(); + } +} diff --git a/divinemc-server/src/main/java/org/bxteam/divinemc/async/rct/RegionizedChunkTicking.java b/divinemc-server/src/main/java/org/bxteam/divinemc/async/rct/RegionizedChunkTicking.java new file mode 100644 index 0000000..3eb3f03 --- /dev/null +++ b/divinemc-server/src/main/java/org/bxteam/divinemc/async/rct/RegionizedChunkTicking.java @@ -0,0 +1,245 @@ +package org.bxteam.divinemc.async.rct; + +import ca.spottedleaf.moonrise.common.list.IteratorSafeOrderedReferenceSet; +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.common.util.TickThread; +import ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistances; +import com.mojang.datafixers.DataFixer; +import com.mojang.logging.LogUtils; +import io.papermc.paper.entity.activation.ActivationRange; +import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongIterator; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.function.Supplier; + +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.progress.ChunkProgressListener; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.GameRules; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.entity.ChunkStatusUpdateListener; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; +import net.minecraft.world.level.storage.DimensionDataStorage; +import net.minecraft.world.level.storage.LevelStorageSource; +import org.bxteam.divinemc.config.DivineConfig; +import org.bxteam.divinemc.util.NamedAgnosticThreadFactory; +import org.slf4j.Logger; + + +public final class RegionizedChunkTicking extends ServerChunkCache { + private static final Logger LOGGER = LogUtils.getLogger(); + public static final Executor REGION_EXECUTOR = Executors.newFixedThreadPool(DivineConfig.AsyncCategory.regionizedChunkTickingExecutorThreadCount, + new NamedAgnosticThreadFactory<>("Region Ticking", TickThread::new, DivineConfig.AsyncCategory.regionizedChunkTickingExecutorThreadPriority) + ); + + public RegionizedChunkTicking(final ServerLevel level, final LevelStorageSource.LevelStorageAccess levelStorageAccess, final DataFixer fixerUpper, final StructureTemplateManager structureManager, final Executor dispatcher, final ChunkGenerator generator, final int viewDistance, final int simulationDistance, final boolean sync, final ChunkProgressListener progressListener, final ChunkStatusUpdateListener chunkStatusListener, final Supplier overworldDataStorage) { + super(level, levelStorageAccess, fixerUpper, structureManager, dispatcher, generator, viewDistance, simulationDistance, sync, progressListener, chunkStatusListener, overworldDataStorage); + } + + public void execute(CompletableFuture spawns, final LevelChunk[] raw) { + final TickPair tickPair = computePlayerRegionsParallel(); + final RegionData[] regions = tickPair.regions(); + final int regionCount = regions.length; + + ActivationRange.activateEntities(level); // Paper - EAR + + final int randomTickSpeed = level.getGameRules().getInt(GameRules.RULE_RANDOMTICKING); + ObjectArrayList> ticked = new ObjectArrayList<>(regionCount); + + for (final RegionData region : regions) { + if (region == null || region.isEmpty()) { + continue; + } + + ticked.add(tick(region, randomTickSpeed)); + } + + CompletableFuture.runAsync(() -> { + finishTicking(ticked, randomTickSpeed, raw, tickPair); + spawns.join(); + }, REGION_EXECUTOR).join(); + } + + private CompletableFuture tick(RegionData region, int randomTickSpeed) { + return CompletableFuture.supplyAsync(() -> { + ObjectArrayList regionChunks = new ObjectArrayList<>(region.chunks().size()); + LongOpenHashSet regionChunksIDs = new LongOpenHashSet(region.chunks().size()); + for (long key : region.chunks()) { + LevelChunk chunk = fullChunks.get(key); + if (chunk != null) { + regionChunks.add(chunk); + regionChunksIDs.add(key); + } + } + + for (LevelChunk chunk : regionChunks) { + level.tickChunk(chunk, randomTickSpeed); + } + for (Entity entity : region.entities()) { + tickEntity(entity); + } + + return regionChunksIDs; + }, REGION_EXECUTOR); + } + + private void finishTicking(final ObjectArrayList> ticked, final int randomTickSpeed, final LevelChunk[] raw, final TickPair tickPair) { + try { + CompletableFuture.allOf(ticked.toArray(new CompletableFuture[0])).join(); + } catch (CompletionException ex) { + LOGGER.error("Error during region chunk ticking", ex.getCause()); + } + + LongOpenHashSet tickedChunkKeys = new LongOpenHashSet(raw.length); + + for (CompletableFuture future : ticked) { + if (!future.isCompletedExceptionally()) { + try { + LongOpenHashSet regionChunks = future.join(); + tickedChunkKeys.addAll(regionChunks); + } catch (Exception e) { + LOGGER.error("Exception in region ticking future", e); + } + } + } + + for (LevelChunk chunk : raw) { + if (!tickedChunkKeys.contains(chunk.coordinateKey)) { + level.tickChunk(chunk, randomTickSpeed); + } + } + + for (Entity entity : tickPair.entities()) { + tickEntity(entity); + } + } + + private TickPair computePlayerRegionsParallel() { + int tickViewDistance = level.moonrise$getViewDistanceHolder().getViewDistances().tickViewDistance(); + List players = new ArrayList<>(level.players()); + int max = maxChunksForViewDistance(); + + List playerChunkSets = players.parallelStream() + .map(player -> { + ChunkPos playerChunk = player.chunkPosition(); + int px = playerChunk.x; + int pz = playerChunk.z; + LongOpenHashSet chunkKeys = new LongOpenHashSet(max); + for (int dx = -tickViewDistance; dx <= tickViewDistance; dx++) { + for (int dz = -tickViewDistance; dz <= tickViewDistance; dz++) { + long key = CoordinateUtils.getChunkKey(px + dx, pz + dz); + chunkKeys.add(key); + } + } + return chunkKeys; + }).toList(); + + List mergedRegions = new ArrayList<>(); + boolean[] merged = new boolean[playerChunkSets.size()]; + + for (int i = 0; i < playerChunkSets.size(); i++) { + if (merged[i]) continue; + + LongOpenHashSet region = new LongOpenHashSet(playerChunkSets.get(i)); + merged[i] = true; + + boolean madeChanges; + do { + madeChanges = false; + for (int j = i + 1; j < playerChunkSets.size(); j++) { + if (merged[j]) continue; + + LongOpenHashSet set = playerChunkSets.get(j); + + boolean hasIntersection = false; + LongIterator iter = set.iterator(); + while (iter.hasNext()) { + if (region.contains(iter.nextLong())) { + hasIntersection = true; + break; + } + } + + if (hasIntersection) { + region.addAll(set); + merged[j] = true; + madeChanges = true; + } + } + } while (madeChanges); + + mergedRegions.add(region); + } + + ObjectArrayList regions = new ObjectArrayList<>(); + Long2IntOpenHashMap chunkToRegion = new Long2IntOpenHashMap(max * mergedRegions.size()); + chunkToRegion.defaultReturnValue(-1); + for (int i = 0; i < mergedRegions.size(); i++) { + regions.add(new RegionData(mergedRegions.get(i), new ObjectOpenHashSet<>())); + for (long key : mergedRegions.get(i)) { + chunkToRegion.put(key, i); + } + } + + final Set firstTick = new ObjectOpenHashSet<>(); + + synchronized (getEntityTickList().entities) { + final IteratorSafeOrderedReferenceSet.Iterator iterator = getEntityTickList().entities.iterator(); + try { + while (iterator.hasNext()) { + Entity entity = iterator.next(); + long chunkKey = entity.chunkPosition().longKey; + int regionIndex = chunkToRegion.get(chunkKey); + if (regionIndex != -1) { + regions.get(regionIndex).entities().add(entity); + } else { + firstTick.add(entity); + } + } + } finally { + iterator.finishedIterating(); + } + } + + return new TickPair(regions.toArray(new RegionData[0]), firstTick); + } + + // Should be max safe estimate of ticking chunks in a region + private int maxChunksForViewDistance() { + ViewDistances distances = level.moonrise$getViewDistanceHolder().getViewDistances(); + int diameter = 2 * distances.tickViewDistance() + 1; + return diameter * diameter; + } + + private void tickEntity(Entity entity) { + if (!entity.isRemoved()) { + if (!level.tickRateManager().isEntityFrozen(entity)) { + entity.checkDespawn(); + // Paper - rewrite chunk system + Entity vehicle = entity.getVehicle(); + if (vehicle != null) { + if (!vehicle.isRemoved() && vehicle.hasPassenger(entity)) { + return; + } + + entity.stopRiding(); + } + + level.guardEntityTick(level::tickNonPassenger, entity); + } + } + } +} diff --git a/divinemc-server/src/main/java/org/bxteam/divinemc/async/rct/TickPair.java b/divinemc-server/src/main/java/org/bxteam/divinemc/async/rct/TickPair.java new file mode 100644 index 0000000..4f8ecfd --- /dev/null +++ b/divinemc-server/src/main/java/org/bxteam/divinemc/async/rct/TickPair.java @@ -0,0 +1,7 @@ +package org.bxteam.divinemc.async.rct; + +import net.minecraft.world.entity.Entity; +import java.util.Set; + +record TickPair(RegionData[] regions, Set entities) { +} diff --git a/divinemc-server/src/main/java/org/bxteam/divinemc/config/DivineConfig.java b/divinemc-server/src/main/java/org/bxteam/divinemc/config/DivineConfig.java index 4546be1..587cdc9 100644 --- a/divinemc-server/src/main/java/org/bxteam/divinemc/config/DivineConfig.java +++ b/divinemc-server/src/main/java/org/bxteam/divinemc/config/DivineConfig.java @@ -227,6 +227,7 @@ public class DivineConfig { // Async mob spawning settings public static boolean enableAsyncSpawning = true; + public static boolean asyncNaturalSpawn = true; public static void load() { parallelWorldTicking(); @@ -354,6 +355,8 @@ public class DivineConfig { private static void asyncMobSpawning() { enableAsyncSpawning = getBoolean(ConfigCategory.ASYNC.key("mob-spawning.enable"), enableAsyncSpawning, "Enables optimization that will offload much of the computational effort involved with spawning new mobs to a different thread."); + asyncNaturalSpawn = getBoolean(ConfigCategory.ASYNC.key("async-ticking-of-natural-spawns"), asyncNaturalSpawn, + "Enables offloading of natural spawning to a different thread"); } }