From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: MrHua269 Date: Sat, 27 Jan 2024 13:24:20 +0000 Subject: [PATCH] Optimize mob spawning(Async mob spawn state calc) diff --git a/src/main/java/com/destroystokyo/paper/util/misc/AreaMap.java b/src/main/java/com/destroystokyo/paper/util/misc/AreaMap.java index 41b9405d6759d865e0d14dd4f95163e9690e967d..091b1ae822e1c0517e59572e7a9bda11e998c0ee 100644 --- a/src/main/java/com/destroystokyo/paper/util/misc/AreaMap.java +++ b/src/main/java/com/destroystokyo/paper/util/misc/AreaMap.java @@ -26,7 +26,7 @@ public abstract class AreaMap { // we use linked for better iteration. // map of: coordinate to set of objects in coordinate - protected final Long2ObjectOpenHashMap> areaMap = new Long2ObjectOpenHashMap<>(1024, 0.7f); + protected Long2ObjectOpenHashMap> areaMap = new Long2ObjectOpenHashMap<>(1024, 0.7f); // Pufferfish - not actually final protected final PooledLinkedHashSets pooledHashSets; protected final ChangeCallback addCallback; @@ -160,7 +160,8 @@ public abstract class AreaMap { protected abstract PooledLinkedHashSets.PooledObjectLinkedOpenHashSet getEmptySetFor(final E object); // expensive op, only for debug - protected void validate(final E object, final int viewDistance) { + protected void validate0(final E object, final int viewDistance) { // Pufferfish - rename this thing just in case it gets used I'd rather a compile time error. + if (true) throw new UnsupportedOperationException(); // Pufferfish - not going to put in the effort to fix this if it doesn't ever get used. int entiesGot = 0; int expectedEntries = (2 * viewDistance + 1); expectedEntries *= expectedEntries; diff --git a/src/main/java/com/destroystokyo/paper/util/misc/PlayerAreaMap.java b/src/main/java/com/destroystokyo/paper/util/misc/PlayerAreaMap.java index 46954db7ecd35ac4018fdf476df7c8020d7ce6c8..1ad890a244bdf6df48a8db68cb43450e08c788a6 100644 --- a/src/main/java/com/destroystokyo/paper/util/misc/PlayerAreaMap.java +++ b/src/main/java/com/destroystokyo/paper/util/misc/PlayerAreaMap.java @@ -5,7 +5,7 @@ import net.minecraft.server.level.ServerPlayer; /** * @author Spottedleaf */ -public final class PlayerAreaMap extends AreaMap { +public class PlayerAreaMap extends AreaMap { // Pufferfish - not actually final public PlayerAreaMap() { super(); diff --git a/src/main/java/gg/pufferfish/pufferfish/util/AsyncPlayerAreaMap.java b/src/main/java/gg/pufferfish/pufferfish/util/AsyncPlayerAreaMap.java new file mode 100644 index 0000000000000000000000000000000000000000..fdcb62d12164024a5f354d60cc863821a18d1b2a --- /dev/null +++ b/src/main/java/gg/pufferfish/pufferfish/util/AsyncPlayerAreaMap.java @@ -0,0 +1,31 @@ +package gg.pufferfish.pufferfish.util; + +import com.destroystokyo.paper.util.misc.PlayerAreaMap; +import com.destroystokyo.paper.util.misc.PooledLinkedHashSets; +import java.util.concurrent.ConcurrentHashMap; +import net.minecraft.server.level.ServerPlayer; + +public final class AsyncPlayerAreaMap extends PlayerAreaMap { + + public AsyncPlayerAreaMap() { + super(); + this.areaMap = new Long2ObjectOpenHashMapWrapper<>(new ConcurrentHashMap<>(1024, 0.7f)); + } + + public AsyncPlayerAreaMap(final PooledLinkedHashSets pooledHashSets) { + super(pooledHashSets); + this.areaMap = new Long2ObjectOpenHashMapWrapper<>(new ConcurrentHashMap<>(1024, 0.7f)); + } + + public AsyncPlayerAreaMap(final PooledLinkedHashSets pooledHashSets, final ChangeCallback addCallback, + final ChangeCallback removeCallback) { + this(pooledHashSets, addCallback, removeCallback, null); + } + + public AsyncPlayerAreaMap(final PooledLinkedHashSets pooledHashSets, final ChangeCallback addCallback, + final ChangeCallback removeCallback, final ChangeSourceCallback changeSourceCallback) { + super(pooledHashSets, addCallback, removeCallback, changeSourceCallback); + this.areaMap = new Long2ObjectOpenHashMapWrapper<>(new ConcurrentHashMap<>(1024, 0.7f)); + } + +} diff --git a/src/main/java/gg/pufferfish/pufferfish/util/Long2ObjectOpenHashMapWrapper.java b/src/main/java/gg/pufferfish/pufferfish/util/Long2ObjectOpenHashMapWrapper.java new file mode 100644 index 0000000000000000000000000000000000000000..facd55463d44cb7e3d2ca6892982f5497b8dded1 --- /dev/null +++ b/src/main/java/gg/pufferfish/pufferfish/util/Long2ObjectOpenHashMapWrapper.java @@ -0,0 +1,40 @@ +package gg.pufferfish.pufferfish.util; + +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import java.util.Map; +import org.jetbrains.annotations.Nullable; + +public class Long2ObjectOpenHashMapWrapper extends Long2ObjectOpenHashMap { + + private final Map backingMap; + + public Long2ObjectOpenHashMapWrapper(Map map) { + backingMap = map; + } + + @Override + public V put(Long key, V value) { + return backingMap.put(key, value); + } + + @Override + public V get(Object key) { + return backingMap.get(key); + } + + @Override + public V remove(Object key) { + return backingMap.remove(key); + } + + @Nullable + @Override + public V putIfAbsent(Long key, V value) { + return backingMap.putIfAbsent(key, value); + } + + @Override + public int size() { + return backingMap.size(); + } +} diff --git a/src/main/java/io/papermc/paper/threadedregions/RegionizedWorldData.java b/src/main/java/io/papermc/paper/threadedregions/RegionizedWorldData.java index 7ca275826609bcf96f103a8c50beaa47c3b4068b..aae4b59a14fc514a30b133921669c855d1717787 100644 --- a/src/main/java/io/papermc/paper/threadedregions/RegionizedWorldData.java +++ b/src/main/java/io/papermc/paper/threadedregions/RegionizedWorldData.java @@ -4,6 +4,7 @@ import com.destroystokyo.paper.util.maplist.ReferenceList; import com.destroystokyo.paper.util.misc.PlayerAreaMap; import com.destroystokyo.paper.util.misc.PooledLinkedHashSets; import com.mojang.logging.LogUtils; +import gg.pufferfish.pufferfish.util.AsyncPlayerAreaMap; import io.papermc.paper.chunk.system.scheduling.ChunkHolderManager; import io.papermc.paper.util.CoordinateUtils; import io.papermc.paper.util.TickThread; @@ -14,6 +15,7 @@ import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet; import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +import me.earthme.luminol.LuminolConfig; import net.minecraft.CrashReport; import net.minecraft.ReportedException; import net.minecraft.core.BlockPos; @@ -58,8 +60,13 @@ import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.function.Predicate; +import java.util.stream.Collectors; public final class RegionizedWorldData { @@ -145,6 +152,10 @@ public final class RegionizedWorldData { into.wanderingTraderSpawnDelay = Math.max(from.wanderingTraderSpawnDelay, into.wanderingTraderSpawnDelay); into.wanderingTraderSpawnChance = Math.max(from.wanderingTraderSpawnChance, into.wanderingTraderSpawnChance); } + + //Luminol start - Async mob spawning + from.lastAsyncSpawnStateTask = null; //Discard the task currently processing + //Luminol end } @Override @@ -302,6 +313,10 @@ public final class RegionizedWorldData { regionizedWorldData.wanderingTraderSpawnDelay = from.wanderingTraderSpawnDelay; regionizedWorldData.villageSiegeState = new VillageSiegeState(); // just re set it, as the spawn pos will be invalid } + + //Luminol start - Async mob spawning + from.lastAsyncSpawnStateTask = null; //Reset the task + //Luminol end } }; @@ -398,6 +413,22 @@ public final class RegionizedWorldData { public java.util.ArrayDeque redstoneUpdateInfos; public final Long2IntOpenHashMap chunksBeingWorkedOn = new Long2IntOpenHashMap(); + //Luminol start - Asnc mob spawning + public volatile CompletableFuture lastAsyncSpawnStateTask = null; + public static ThreadPoolExecutor ASYNC_MOB_SPAWNING_EXECUTOR; + public static void initMobSpawningExecutor(){ + if (LuminolConfig.enableAsyncMobSpawning){ + ASYNC_MOB_SPAWNING_EXECUTOR = new ThreadPoolExecutor( + 1, + Integer.MAX_VALUE, + 1, + TimeUnit.MINUTES, + new LinkedBlockingQueue<>() + ); + } + } + //Luminol end + public static final class TempCollisionList { final UnsafeList list = new UnsafeList<>(64); boolean inUse; @@ -430,7 +461,7 @@ public final class RegionizedWorldData { // Mob spawning private final PooledLinkedHashSets pooledHashSets = new PooledLinkedHashSets<>(); - public final PlayerAreaMap mobSpawnMap = new PlayerAreaMap(this.pooledHashSets); + public final PlayerAreaMap mobSpawnMap = new AsyncPlayerAreaMap(this.pooledHashSets); //Luminol - Async mob spawning public int catSpawnerNextTick = 0; public int patrolSpawnerNextTick = 0; public int phantomSpawnerNextTick = 0; @@ -578,6 +609,12 @@ public final class RegionizedWorldData { return this.loadedEntities; } + //Luminol start - Async mob spawning + public Iterable getLoadedEntitiesCopy(){ + return Arrays.asList(this.loadedEntities.getRawData()).stream().map(o -> (Entity)o).collect(Collectors.toList()); + } + //Luminol end + public Entity[] takeTrackingUnloads() { final Entity[] ret = Arrays.copyOf(this.toProcessTrackingUnloading.getRawData(), this.toProcessTrackingUnloading.size(), Entity[].class); diff --git a/src/main/java/me/earthme/luminol/LuminolConfig.java b/src/main/java/me/earthme/luminol/LuminolConfig.java index ca54cc28282ac6441098215d6e1a3531c3f68b83..c79070eb6bb71d473fab758ae9d826276ae7eea6 100644 --- a/src/main/java/me/earthme/luminol/LuminolConfig.java +++ b/src/main/java/me/earthme/luminol/LuminolConfig.java @@ -2,6 +2,7 @@ package me.earthme.luminol; import dev.kaiijumc.kaiiju.region.RegionFileFormat; import com.electronwill.nightconfig.core.file.CommentedFileConfig; +import io.papermc.paper.threadedregions.RegionizedWorldData; import me.earthme.luminol.commands.TpsBarCommand; import me.earthme.luminol.functions.GlobalServerTpsBar; import net.minecraft.core.registries.BuiltInRegistries; @@ -61,6 +62,7 @@ public class LuminolConfig { public static boolean asyncPathProcessing = false; public static int asyncPathProcessingMaxThreads = 0; public static int asyncPathProcessingKeepalive = 60; + public static boolean enableAsyncMobSpawning = false; public static void init() throws IOException { PARENT_FOLDER.mkdir(); @@ -186,6 +188,8 @@ public class LuminolConfig { asyncPathProcessingMaxThreads = Math.max(Runtime.getRuntime().availableProcessors() / 4, 1); if (!asyncPathProcessing) asyncPathProcessingMaxThreads = 0; + enableAsyncMobSpawning = get("optimizations.enable_async_mob_spawning",enableAsyncMobSpawning); + RegionizedWorldData.initMobSpawningExecutor(); } public static T get(String key,T def){ diff --git a/src/main/java/me/earthme/luminol/utils/AsyncMobSpawnExecutor.java b/src/main/java/me/earthme/luminol/utils/AsyncMobSpawnExecutor.java new file mode 100644 index 0000000000000000000000000000000000000000..88d5b188ccfb17fe1ae4b08f32565f27569cad5c --- /dev/null +++ b/src/main/java/me/earthme/luminol/utils/AsyncMobSpawnExecutor.java @@ -0,0 +1,71 @@ +package me.earthme.luminol.utils; + +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +import net.minecraft.server.MinecraftServer; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.LockSupport; + +public class AsyncMobSpawnExecutor implements Runnable, Executor { + private final MultiThreadedQueue allTasks = new MultiThreadedQueue<>(); + private final Thread worker = new Thread(this); + private AtomicBoolean shouldRunNext = new AtomicBoolean(true); + private AtomicBoolean isRunning = new AtomicBoolean(false); + private AtomicBoolean isIdle = new AtomicBoolean(false); + + public boolean isRunning(){ + return this.isRunning.get(); + } + + public void startExecutor(){ + this.worker.setDaemon(true); + this.worker.setContextClassLoader(MinecraftServer.class.getClassLoader()); + this.worker.start(); + } + + public void forceTerminate(){ + this.shouldRunNext.set(false); + if (this.isRunning.get()){ + this.allTasks.clear(); + LockSupport.unpark(this.worker); + } + } + + public void dropAllTasks(){ + this.allTasks.clear(); + } + + @Override + public void run() { + this.isRunning.set(true); + try { + while (this.shouldRunNext.get()){ + final Runnable task = this.allTasks.poll(); + if (task != null){ + this.isIdle.set(false); + + try { + task.run(); + }catch (Exception e){ + e.printStackTrace(); //TODO - Exception processing? + } + + continue; + } + + this.isIdle.set(true); + LockSupport.park(); + } + }finally { + this.isRunning.set(false); + } + } + + @Override + public void execute(@NotNull Runnable command) { + this.allTasks.offer(command); + LockSupport.unpark(this.worker); //Notify + } +} diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java index 88db5ada13329a5fe0d0fb652d2c8a8d561649e8..9d5a1d1aa5c9fae20c4598a2da370fe2b021ec25 100644 --- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java +++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java @@ -17,6 +17,9 @@ import java.util.function.BooleanSupplier; import java.util.function.Consumer; import java.util.function.Supplier; import javax.annotation.Nullable; + +import io.papermc.paper.threadedregions.RegionizedWorldData; +import me.earthme.luminol.LuminolConfig; import net.minecraft.Util; import net.minecraft.core.BlockPos; import net.minecraft.core.SectionPos; @@ -486,32 +489,38 @@ public class ServerChunkCache extends ChunkSource { int k = this.distanceManager.getNaturalSpawnChunkCount(); // Paper start - per player mob spawning int naturalSpawnChunkCount = k; - NaturalSpawner.SpawnState spawnercreature_d; // moved down + NaturalSpawner.SpawnState spawnercreature_d = null; // moved down profiler.startTimer(ca.spottedleaf.leafprofiler.LProfilerRegistry.MOB_SPAWN_ENTITY_COUNT); try { // Folia - profiler - 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.getLocalPlayers()) { // Folia - region threading - // 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 (!LuminolConfig.enableAsyncMobSpawning) { //Luminol + 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.getLocalPlayers()) { // Folia - region threading + // 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 + spawnercreature_d = NaturalSpawner.createState(naturalSpawnChunkCount, regionizedWorldData.getLoadedEntities(), this::getFullChunk, null, true); // Folia - region threading - note: function only cares about loaded entities, doesn't need all + } else { + spawnercreature_d = 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 } - spawnercreature_d = NaturalSpawner.createState(naturalSpawnChunkCount, regionizedWorldData.getLoadedEntities(), this::getFullChunk, null, true); // Folia - region threading - note: function only cares about loaded entities, doesn't need all - } else { - spawnercreature_d = 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 + }//Luminol + // Luminol start - Async mob spawning + if (!LuminolConfig.enableAsyncMobSpawning){ + regionizedWorldData.lastAsyncSpawnStateTask = CompletableFuture.completedFuture(spawnercreature_d); } } finally { profiler.stopTimer(ca.spottedleaf.leafprofiler.LProfilerRegistry.MOB_SPAWN_ENTITY_COUNT); } // Folia - profiler // Paper end this.level.timings.countNaturalMobs.stopTiming(); // Paper - timings - regionizedWorldData.lastSpawnState = spawnercreature_d; // Folia - region threading + //regionizedWorldData.lastSpawnState = spawnercreature_d; // Folia - region threading //Luminol - Async mob spawning gameprofilerfiller.popPush("spawnAndTick"); boolean flag = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && !this.level.getLocalPlayers().isEmpty(); // CraftBukkit // Folia - region threadin @@ -606,7 +615,11 @@ public class ServerChunkCache extends ChunkSource { chunk1.incrementInhabitedTime(j); if (spawn && flag && (this.spawnEnemies || this.spawnFriendlies) && this.level.getWorldBorder().isWithinBounds(chunkcoordintpair)) { // Spigot // Paper - optimise chunk tick iteration ++spawnChunkCount; // Folia - profiler - NaturalSpawner.spawnForChunk(this.level, chunk1, spawnercreature_d, this.spawnFriendlies, this.spawnEnemies, flag1); + //Luminol start - Async mob spawning + if (regionizedWorldData.lastAsyncSpawnStateTask != null && regionizedWorldData.lastAsyncSpawnStateTask.isDone()){ + NaturalSpawner.spawnForChunk(this.level, chunk1, regionizedWorldData.lastAsyncSpawnStateTask.join(), this.spawnFriendlies, this.spawnEnemies, flag1); + } + //Luminol end } if (true || this.level.shouldTickBlocksAt(chunkcoordintpair.toLong())) { // Paper - optimise chunk tick iteration @@ -636,6 +649,37 @@ public class ServerChunkCache extends ChunkSource { } // Paper - timings } finally { profiler.stopTimer(ca.spottedleaf.leafprofiler.LProfilerRegistry.MISC_MOB_SPAWN_TICK); } // Folia - profiler } + + //Luminol start - Async mob spawning + if (LuminolConfig.enableAsyncMobSpawning){ + final Iterable cp = regionizedWorldData.getLoadedEntitiesCopy(); + //Luminol - Copied down + 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 : regionizedWorldData.getLocalPlayers()) { // Folia - region threading + // 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; + } + // Paper end - per player mob spawning backoff + } + + if (regionizedWorldData.lastAsyncSpawnStateTask == null || regionizedWorldData.lastAsyncSpawnStateTask.isDone()){ + regionizedWorldData.lastAsyncSpawnStateTask = CompletableFuture.supplyAsync(() -> NaturalSpawner.createState(naturalSpawnChunkCount,cp, this::getFullChunk, null, true),RegionizedWorldData.ASYNC_MOB_SPAWNING_EXECUTOR); + } + } else { + if (regionizedWorldData.lastAsyncSpawnStateTask == null || regionizedWorldData.lastAsyncSpawnStateTask.isDone()){ + regionizedWorldData.lastAsyncSpawnStateTask = CompletableFuture.supplyAsync(() -> NaturalSpawner.createState(naturalSpawnChunkCount,cp, this::getFullChunk, !this.level.paperConfig().entities.spawning.perPlayerMobSpawns ? new LocalMobCapCalculator(this.chunkMap) : null, false), RegionizedWorldData.ASYNC_MOB_SPAWNING_EXECUTOR); + } + } + } + //Luminol end } gameprofilerfiller.popPush("broadcast"); @@ -806,7 +850,7 @@ public class ServerChunkCache extends ChunkSource { @VisibleForDebug public NaturalSpawner.SpawnState getLastSpawnState() { io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.level.getCurrentWorldData(); // Folia - region threading - return worldData == null ? null : worldData.lastSpawnState; // Folia - region threading + return worldData.lastAsyncSpawnStateTask != null && worldData.lastAsyncSpawnStateTask.isDone() ? worldData.lastAsyncSpawnStateTask.join() : null; // Folia - region threading //Luminol - Async mob spawning } public void removeTicketsOnClosing() { diff --git a/src/main/java/net/minecraft/world/level/NaturalSpawner.java b/src/main/java/net/minecraft/world/level/NaturalSpawner.java index 1d082e977442790504e3ca70a90dabd69b522622..6fdfeb6920a6db79eb2d374ad120482a5103ccaf 100644 --- a/src/main/java/net/minecraft/world/level/NaturalSpawner.java +++ b/src/main/java/net/minecraft/world/level/NaturalSpawner.java @@ -1,6 +1,9 @@ package net.minecraft.world.level; +import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; import com.mojang.logging.LogUtils; +import io.papermc.paper.threadedregions.RegionizedServer; +import io.papermc.paper.util.TickThread; import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2IntMaps; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; @@ -117,6 +120,15 @@ public final class NaturalSpawner { object2intopenhashmap.addTo(enumcreaturetype, 1); // Paper start if (countMobs) { + //Luminol start - Async mob spawning + if (!TickThread.isTickThread()){ + RegionizedServer.getInstance().taskQueue.queueTickTaskQueue(chunk.level,chunk.locX,chunk.locZ,()->{ + chunk.level.getChunkSource().chunkMap.updatePlayerMobTypeMap(entity); + }); + return; + } + //Luminol end + chunk.level.getChunkSource().chunkMap.updatePlayerMobTypeMap(entity); } // Paper end