diff --git a/README.md b/README.md
index 2ecd018e..3d225e39 100644
--- a/README.md
+++ b/README.md
@@ -114,6 +114,7 @@ If these excellent projects hadn't appeared, Leaf wouldn't have become great.
• Polpot
• Matter
• Luminol
+ • Nitori
diff --git a/patches/removed/server/0082-Tracking-Optimize-Use-thread-safe-Collection.patch b/patches/removed/server/0082-Tracking-Optimize-Use-thread-safe-Collection.patch
deleted file mode 100644
index 1af17d90..00000000
--- a/patches/removed/server/0082-Tracking-Optimize-Use-thread-safe-Collection.patch
+++ /dev/null
@@ -1,63 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Dreeam <61569423+Dreeam-qwq@users.noreply.github.com>
-Date: Thu, 23 May 2024 21:28:02 +0800
-Subject: [PATCH] Tracking Optimize: Use thread-safe Collection
-
-Removed since 1.21, not good, useless
-
-diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
-index a8821f031d85ca66f96aece7c179c40ee96fb90f..29780ed86e938ea1f9e6405e5dee84b09abe3ac0 100644
---- a/src/main/java/net/minecraft/server/level/ChunkMap.java
-+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
-@@ -244,7 +244,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- public final io.papermc.paper.util.player.NearbyPlayers nearbyPlayers;
- // Paper end
- // Paper start - optimise chunk tick iteration
-- public final it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet needsChangeBroadcasting = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>();
-+ public final Set needsChangeBroadcasting = Sets.newConcurrentHashSet(); // Leaf - Use thread-safe collection
- public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerMobSpawnMap = new gg.pufferfish.pufferfish.util.AsyncPlayerAreaMap(this.pooledLinkedPlayerHashSets); // Pufferfish
- // Paper end - optimise chunk tick iteration
-
-diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
-index 31d789e6559bc72c699f1be5457230fb640be045..700b8713e41982aa9e1f1ae54eb850a31d3ace52 100644
---- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
-+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
-@@ -644,7 +644,7 @@ public class ServerChunkCache extends ChunkSource {
- // Paper - optimise chunk tick iteration
- // Paper start - optimise chunk tick iteration
- if (!this.chunkMap.needsChangeBroadcasting.isEmpty()) {
-- it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet copy = this.chunkMap.needsChangeBroadcasting.clone();
-+ List copy = new java.util.ArrayList<>(this.chunkMap.needsChangeBroadcasting); // Leaf - Use thread-safe collection
- this.chunkMap.needsChangeBroadcasting.clear();
- for (ChunkHolder holder : copy) {
- holder.broadcastChanges(holder.getFullChunkNowUnchecked()); // LevelChunks are NEVER unloaded
-diff --git a/src/main/java/net/minecraft/server/level/ServerEntity.java b/src/main/java/net/minecraft/server/level/ServerEntity.java
-index 5a17ccd4a620f17c6434f457244ae2b790e2f34e..d57c6b9d9b37d94829c01f63977ad2caca110dfd 100644
---- a/src/main/java/net/minecraft/server/level/ServerEntity.java
-+++ b/src/main/java/net/minecraft/server/level/ServerEntity.java
-@@ -436,7 +436,7 @@ public class ServerEntity {
-
- if (!set.isEmpty()) {
- // Leaf start - petal - sync
-- final Set copy = new it.unimi.dsi.fastutil.objects.ObjectOpenHashSet<>(set);
-+ final Set copy = com.google.common.collect.Sets.newConcurrentHashSet(set);
- ((ServerLevel) this.entity.level()).chunkSource.chunkMap.runOnTrackerMainThread(() -> {
- // CraftBukkit start - Send scaled max health
- if (this.entity instanceof ServerPlayer) {
-diff --git a/src/main/java/net/minecraft/world/entity/ai/attributes/AttributeMap.java b/src/main/java/net/minecraft/world/entity/ai/attributes/AttributeMap.java
-index 894082d9a8e2aa05f86948bfd335090f37a4ba07..9d16bcfb64885d8f23df8effd44f35bb3a0fa2e4 100644
---- a/src/main/java/net/minecraft/world/entity/ai/attributes/AttributeMap.java
-+++ b/src/main/java/net/minecraft/world/entity/ai/attributes/AttributeMap.java
-@@ -19,8 +19,10 @@ import org.slf4j.Logger;
-
- public class AttributeMap {
- private static final Logger LOGGER = LogUtils.getLogger();
-- private final Map, AttributeInstance> attributes = new Object2ObjectOpenHashMap<>();
-- private final Set dirtyAttributes = new ObjectOpenHashSet<>();
-+ // Leaf start - Use thread-safe collection
-+ private final Map, AttributeInstance> attributes = com.google.common.collect.Maps.newConcurrentMap();
-+ private final Set dirtyAttributes = com.google.common.collect.Sets.newConcurrentHashSet();
-+ // Leaf end - Use thread-safe collection
- private final AttributeSupplier supplier;
- private final java.util.function.Function, AttributeInstance> createInstance; // Gale - Airplane - reduce entity allocations
- private final net.minecraft.world.entity.LivingEntity entity; // Purpur
diff --git a/patches/removed/server/0084-Tracking-Optimize-reduce-ArmorStand.patch b/patches/removed/server/0084-Tracking-Optimize-reduce-ArmorStand.patch
deleted file mode 100644
index 280f670e..00000000
--- a/patches/removed/server/0084-Tracking-Optimize-reduce-ArmorStand.patch
+++ /dev/null
@@ -1,79 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Dreeam <61569423+Dreeam-qwq@users.noreply.github.com>
-Date: Thu, 23 May 2024 23:03:10 +0800
-Subject: [PATCH] Tracking Optimize: reduce ArmorStand
-
-Removed since 1.21, not good, useless
-
-diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
-index dced0a22e924838b13edd0c24a7d3fb3de9242d6..05d94bdc6f9c8baafa392fa3e8f2221df999277c 100644
---- a/src/main/java/net/minecraft/server/level/ServerLevel.java
-+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
-@@ -1437,7 +1437,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
- return;
- }*/ // Paper - comment out EAR 2
- // Spigot end
-- final boolean isActive = org.spigotmc.ActivationRange.checkIfActive(entity);
-+ boolean isActive = shouldActive(entity); // Leaf - Reduce entity tick
- entity.setOldPosAndRot();
-
- ++entity.tickCount;
-@@ -1465,7 +1465,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
- if (!passenger.isRemoved() && passenger.getVehicle() == vehicle) {
- if (passenger instanceof Player || this.entityTickList.contains(passenger)) {
- // Paper - EAR 2
-- final boolean isActive = org.spigotmc.ActivationRange.checkIfActive(passenger);
-+ final boolean isActive = shouldActive(passenger); // Leaf - Reduce entity tick
- passenger.setOldPosAndRot();
- ++passenger.tickCount;
- // Paper start - EAR 2
-@@ -1492,6 +1492,12 @@ public class ServerLevel extends Level implements WorldGenLevel {
- }
- }
-
-+ // Leaf start - Reduce entity tick
-+ private boolean shouldActive(Entity entity) {
-+ return entity instanceof net.minecraft.world.entity.decoration.ArmorStand ? entity.level().paperConfig().entities.armorStands.tick : org.spigotmc.ActivationRange.checkIfActive(entity);
-+ }
-+ // Leaf end - Reduce entity tick
-+
- @Override
- public boolean mayInteract(Player player, BlockPos pos) {
- return !this.server.isUnderSpawnProtection(this, pos, player) && this.getWorldBorder().isWithinBounds(pos);
-diff --git a/src/main/java/org/spigotmc/ActivationRange.java b/src/main/java/org/spigotmc/ActivationRange.java
-index 37e35606e3f47325db4da5deeff02aad9d541e87..b3fa485dbf96a5b691f423d004bbe0ff7bde4f9a 100644
---- a/src/main/java/org/spigotmc/ActivationRange.java
-+++ b/src/main/java/org/spigotmc/ActivationRange.java
-@@ -147,6 +147,13 @@ public class ActivationRange
- */
- public static boolean initializeEntityActivationState(Entity entity, SpigotWorldConfig config)
- {
-+ // Leaf start - Reduce ArmorStand tick
-+ if (entity instanceof net.minecraft.world.entity.decoration.ArmorStand
-+ && !entity.level().paperConfig().entities.armorStands.tick) {
-+ return false;
-+ }
-+ // Leaf end - Reduce ArmorStand tick
-+
- if ( ( entity.activationType == ActivationType.MISC && config.miscActivationRange <= 0 )
- || ( entity.activationType == ActivationType.RAIDER && config.raiderActivationRange <= 0 )
- || ( entity.activationType == ActivationType.ANIMAL && config.animalActivationRange <= 0 )
-@@ -233,12 +240,18 @@ public class ActivationRange
- // Paper start
- java.util.List entities = world.getEntities((Entity)null, ActivationRange.maxBB, null);
- boolean tickMarkers = world.paperConfig().entities.markers.tick; // Paper - Configurable marker ticking
-+ boolean tickArmorStands = world.paperConfig().entities.armorStands.tick; // Leaf - Reduce entity tick
- for (Entity entity : entities) {
- // Paper start - Configurable marker ticking
- if (!tickMarkers && entity instanceof net.minecraft.world.entity.Marker) {
- continue;
- }
- // Paper end - Configurable marker ticking
-+ // Leaf start - Reduce entity tick
-+ if (!tickArmorStands && entity instanceof net.minecraft.world.entity.decoration.ArmorStand) {
-+ continue;
-+ }
-+ // Leaf end - Reduce entity tick
- ActivationRange.activateEntity(entity);
-
- // Pufferfish start
diff --git a/patches/removed/server/0085-Tracking-Optimize-Skip-redundant-useless-packets.patch b/patches/removed/server/0085-Tracking-Optimize-Skip-redundant-useless-packets.patch
deleted file mode 100644
index 5231441f..00000000
--- a/patches/removed/server/0085-Tracking-Optimize-Skip-redundant-useless-packets.patch
+++ /dev/null
@@ -1,56 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Dreeam <61569423+Dreeam-qwq@users.noreply.github.com>
-Date: Fri, 24 May 2024 19:03:02 +0800
-Subject: [PATCH] Tracking Optimize: Skip redundant useless packets
-
-Removed since 1.21, not good, useless
-
-diff --git a/src/main/java/net/minecraft/server/level/ServerEntity.java b/src/main/java/net/minecraft/server/level/ServerEntity.java
-index c2db18cbb1230588e6b1783dc51c69ed3ccd2e34..71a7b36f7ab1d0743591495a45d8a153cee138e8 100644
---- a/src/main/java/net/minecraft/server/level/ServerEntity.java
-+++ b/src/main/java/net/minecraft/server/level/ServerEntity.java
-@@ -418,23 +418,34 @@ public class ServerEntity {
- // Paper end
- }
-
-+ // Leaf start - Tracking Optimize - Skip redundant useless packets
-+ private List> lastSendDirtyData = new ArrayList<>();
-+ private Set lastSendDirtyAttributes = com.google.common.collect.Sets.newConcurrentHashSet();
-+ // Leaf end - Tracking Optimize - Skip redundant useless packets
- private void sendDirtyEntityData() {
- SynchedEntityData datawatcher = this.entity.getEntityData();
- List> list = datawatcher.packDirty();
-
- if (list != null) {
- this.trackedDataValues = datawatcher.getNonDefaultValues();
-+ // Leaf start - Tracking Optimize - Skip redundant useless packets
-+ if (!lastSendDirtyData.equals(list)) {
- // Leaf start - petal - sync
- ((ServerLevel) this.entity.level()).chunkSource.chunkMap.runOnTrackerMainThread(() ->
- this.broadcastAndSend(new ClientboundSetEntityDataPacket(this.entity.getId(), list))
- );
- // Leaf end - petal
-+ lastSendDirtyData = new ArrayList<>(list);
-+ }
-+ // Leaf end - Tracking Optimize - Skip redundant useless packets
- }
-
- if (this.entity instanceof LivingEntity) {
- Set set = ((LivingEntity) this.entity).getAttributes().getDirtyAttributes();
-
- if (!set.isEmpty()) {
-+ // Leaf start - Tracking Optimize - Skip redundant useless packets
-+ if (!lastSendDirtyAttributes.equals(set)) {
- // Leaf start - petal - sync
- final Set copy = com.google.common.collect.Sets.newConcurrentHashSet(set);
- ((ServerLevel) this.entity.level()).chunkSource.chunkMap.runOnTrackerMainThread(() -> {
-@@ -447,6 +458,9 @@ public class ServerEntity {
-
- });
- // Leaf end - petal
-+ lastSendDirtyAttributes = com.google.common.collect.Sets.newConcurrentHashSet(set);
-+ }
-+ // Leaf end - Tracking Optimize - Skip redundant useless packets
- }
-
- ((LivingEntity) this.entity).getAttributes().clearDirtyAttributes(); // Leaf
diff --git a/patches/server/0012-Fix-Pufferfish-and-Purpur-patches.patch b/patches/server/0012-Fix-Pufferfish-and-Purpur-patches.patch
index 256c302b..d96f9dd7 100644
--- a/patches/server/0012-Fix-Pufferfish-and-Purpur-patches.patch
+++ b/patches/server/0012-Fix-Pufferfish-and-Purpur-patches.patch
@@ -28,7 +28,7 @@ index 23609a71a993fc91271578ee0a541a9c6ec7354f..34f7f74b374d319d0d578e498db7663d
@Override
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
-index 9d082b3c06281b0cafe455959d6ef20b8891110e..5071828f4ec318ca457d7c29d5ffaa3de2f43e48 100644
+index 9d082b3c06281b0cafe455959d6ef20b8891110e..c842bd59cb612a98d261d31a026d4ae8ce4e455b 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
@@ -300,7 +300,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop S spin(Function serverFactory) {
AtomicReference atomicreference = new AtomicReference();
diff --git a/patches/server/0039-Petal-Async-Pathfinding.patch b/patches/server/0039-Petal-Async-Pathfinding.patch
index bfdcad64..4eb5ac5c 100644
--- a/patches/server/0039-Petal-Async-Pathfinding.patch
+++ b/patches/server/0039-Petal-Async-Pathfinding.patch
@@ -1095,7 +1095,7 @@ index 0000000000000000000000000000000000000000..2f5aff1f0e2aca0a8bfeab27c4ab027f
+}
diff --git a/src/main/java/org/dreeam/leaf/async/path/AsyncPathProcessor.java b/src/main/java/org/dreeam/leaf/async/path/AsyncPathProcessor.java
new file mode 100644
-index 0000000000000000000000000000000000000000..ccee3723679d3740d34bd13c04018bafafa5da62
+index 0000000000000000000000000000000000000000..3eb86fc2e0ea28be18e23dd2c060e043f1fede93
--- /dev/null
+++ b/src/main/java/org/dreeam/leaf/async/path/AsyncPathProcessor.java
@@ -0,0 +1,52 @@
@@ -1123,7 +1123,7 @@ index 0000000000000000000000000000000000000000..ccee3723679d3740d34bd13c04018baf
+ org.dreeam.leaf.config.modules.async.AsyncPathfinding.asyncPathfindingKeepalive, TimeUnit.SECONDS,
+ new LinkedBlockingQueue<>(),
+ new ThreadFactoryBuilder()
-+ .setNameFormat("petal-async-pathfinding-thread-%d")
++ .setNameFormat("Leaf Async Pathfinding Thread - %d")
+ .setPriority(Thread.NORM_PRIORITY - 2)
+ .build()
+ );
diff --git a/patches/server/0055-SparklyPaper-Skip-EntityScheduler-s-executeTick-chec.patch b/patches/server/0055-SparklyPaper-Skip-EntityScheduler-s-executeTick-chec.patch
index 45a3da07..3867f6d2 100644
--- a/patches/server/0055-SparklyPaper-Skip-EntityScheduler-s-executeTick-chec.patch
+++ b/patches/server/0055-SparklyPaper-Skip-EntityScheduler-s-executeTick-chec.patch
@@ -75,13 +75,13 @@ index c03608fec96b51e1867f43d8f42e5aefb1520e46..15b21fa3907db1b77ed5b5d1050a37f4
throw new IllegalStateException("Ticking retired scheduler");
}
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
-index efd29f9d2417c9483b0aa2c521109a9e20f6990c..7308c3ae41397338092ab3f7f95baca6b2a515c3 100644
+index 55466ff58d724a6254fd6e53c72adb592f2d433a..0255d5a23e2f5a1a67af3ff1588b608521e4c133 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
@@ -312,6 +312,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop entitiesWithScheduledTasks = java.util.concurrent.ConcurrentHashMap.newKeySet(); // SparklyPaper - skip EntityScheduler's executeTick checks if there isn't any tasks to be run (concurrent because plugins may schedule tasks async)
public static S spin(Function serverFactory) {
diff --git a/patches/server/0100-Multithreaded-Tracker.patch b/patches/server/0100-Multithreaded-Tracker.patch
new file mode 100644
index 00000000..3d37253c
--- /dev/null
+++ b/patches/server/0100-Multithreaded-Tracker.patch
@@ -0,0 +1,537 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: peaches94
+Date: Sat, 2 Jul 2022 00:35:56 -0500
+Subject: [PATCH] Multithreaded Tracker
+
+Original license: GPL v3
+Original project: https://github.com/Bloom-host/Petal
+
+Original license: GPL v3
+Original project: https://github.com/TECHNOVE/Airplane-Experimental
+
+Co-authored-by: Paul Sauve
+Co-authored-by: Kevin Raneri
+
+This patch refactored from original multithreaded tracker (Petal version),
+and is derived from the Airplane fork by Paul Sauve, the tree is like:
+Airplane -> Pufferfish? -> Petal -> Leaf
+
+We made much of tracking logic asynchronously, and fixed visible issue
+for the case of some NPC plugins which using real entity type, e.g. Citizens.
+
+But it is still recommending to use those packet based, virtual entity
+based NPC plugins, e.g. ZNPC Plus, Adyeshach, Fancy NPC, etc.
+
+diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java b/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java
+index 31bb5c3058233c98cbdd919e4803dd2f2266d39d..2622a82b6e34cb636eaad239d8e6e30dc8cce589 100644
+--- a/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java
++++ b/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java
+@@ -42,6 +42,12 @@ class PaperEventManager {
+ if (event.isAsynchronous() && this.server.isPrimaryThread()) {
+ throw new IllegalStateException(event.getEventName() + " may only be triggered asynchronously.");
+ } else if (!event.isAsynchronous() && !this.server.isPrimaryThread() && !this.server.isStopping()) {
++ // Leaf start - petal - Multithreaded tracker
++ if (org.dreeam.leaf.config.modules.async.MultithreadedTracker.enabled) {
++ net.minecraft.server.MinecraftServer.getServer().scheduleOnMain(event::callEvent);
++ return;
++ }
++ // Leaf end - petal - Multithreaded tracker
+ throw new IllegalStateException(event.getEventName() + " may only be triggered synchronously.");
+ }
+
+diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
+index 200a08d09904c6d5ea85b9e2c0228e6184f3aed1..cfd9545384c0b74605115e47c390c876a61dbdd3 100644
+--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
++++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
+@@ -906,6 +906,21 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity)entity).moonrise$setTrackedEntity(null); // Paper - optimise entity tracker
+ }
+
++ // Leaf start - petal - Multithreaded tracker
++ private final java.util.concurrent.ConcurrentLinkedQueue trackerMainThreadTasks = new java.util.concurrent.ConcurrentLinkedQueue<>();
++ private boolean tracking = false;
++
++ public void runOnTrackerMainThread(final Runnable runnable) {
++ //final boolean isOnMain = ca.spottedleaf.moonrise.common.util.TickThread.isTickThread();
++ //System.out.println(isOnMain);
++ if (false && this.tracking) { // TODO: check here
++ this.trackerMainThreadTasks.add(runnable);
++ } else {
++ runnable.run();
++ }
++ }
++ // Leaf end - petal - Multithreaded tracker
++
+ // Paper start - optimise entity tracker
+ private void newTrackerTick() {
+ final ca.spottedleaf.moonrise.common.misc.NearbyPlayers nearbyPlayers = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getNearbyPlayers();
+@@ -939,6 +954,13 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ // Paper end - optimise entity tracker
+
+ protected void tick() {
++ // Leaf start - petal - Multithreaded tracker
++ if (org.dreeam.leaf.config.modules.async.MultithreadedTracker.enabled) {
++ final ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel level = this.level;
++ org.dreeam.leaf.async.tracker.MultithreadedTracker.tick(level);
++ return;
++ }
++ // Leaf end - petal - Multithreaded tracker
+ // Paper start - optimise entity tracker
+ if (true) {
+ this.newTrackerTick();
+@@ -1088,7 +1110,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ final Entity entity;
+ private final int range;
+ SectionPos lastSectionPos;
+- public final Set seenBy = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>(); // Paper - Perf: optimise map impl
++ public final Set seenBy = org.dreeam.leaf.config.modules.async.MultithreadedTracker.enabled
++ ? Sets.newConcurrentHashSet()
++ : new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>(); // Paper - Perf: optimise map impl // Leaf - petal - Multithreaded tracker
+
+ // Paper start - optimise entity tracker
+ private long lastChunkUpdate = -1L;
+@@ -1116,6 +1140,37 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+
+ final ServerPlayer[] playersRaw = players.getRawDataUnchecked();
+
++ // Leaf start - Multithreaded tracker
++ if (org.dreeam.leaf.config.modules.async.MultithreadedTracker.enabled && org.dreeam.leaf.config.modules.async.MultithreadedTracker.compatModeEnabled) {
++ final boolean isServerPlayer = this.entity instanceof ServerPlayer;
++ final boolean isRealPlayer = isServerPlayer && ((ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer) this.entity).moonrise$isRealPlayer();
++ Runnable updatePlayerTasks = () -> {
++ for (int i = 0, len = players.size(); i < len; ++i) {
++ final ServerPlayer player = playersRaw[i];
++ this.updatePlayer(player);
++ }
++
++ if (lastChunkUpdate != currChunkUpdate || lastTrackedChunk != chunk) {
++ // need to purge any players possible not in the chunk list
++ for (final ServerPlayerConnection conn : new java.util.ArrayList<>(this.seenBy)) {
++ final ServerPlayer player = conn.getPlayer();
++ if (!players.contains(player)) {
++ this.removePlayer(player);
++ }
++ }
++ }
++ };
++
++ // Only update asynchronously for real player, and sync update for fake players
++ // This can fix compatibility issue with NPC plugins using real entity type, like Citizens
++ // To prevent visible issue with player type NPCs
++ // btw, still recommend to use packet based NPC plugins, like ZNPC Plus, Adyeshach, Fancy NPC, etc.
++ if (isRealPlayer || !isServerPlayer) {
++ org.dreeam.leaf.async.tracker.MultithreadedTracker.getTrackerExecutor().execute(updatePlayerTasks);
++ } else {
++ updatePlayerTasks.run();
++ }
++ } else {
+ for (int i = 0, len = players.size(); i < len; ++i) {
+ final ServerPlayer player = playersRaw[i];
+ this.updatePlayer(player);
+@@ -1130,6 +1185,8 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ }
+ }
+ }
++ }
++ // Leaf end - Multithreaded tracker
+ }
+
+ @Override
+@@ -1184,14 +1241,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ }
+
+ public void broadcast(Packet> packet) {
+- Iterator iterator = this.seenBy.iterator();
+-
+- while (iterator.hasNext()) {
+- ServerPlayerConnection serverplayerconnection = (ServerPlayerConnection) iterator.next();
+-
++ // Leaf start - petal - Multithreaded tracker
++ for (ServerPlayerConnection serverplayerconnection : this.seenBy.toArray(new ServerPlayerConnection[0])) {
+ serverplayerconnection.send(packet);
+ }
+-
++ // Leaf end - petal - Multithreaded tracker
+ }
+
+ public void broadcastAndSend(Packet> packet) {
+@@ -1203,18 +1257,15 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ }
+
+ public void broadcastRemoved() {
+- Iterator iterator = this.seenBy.iterator();
+-
+- while (iterator.hasNext()) {
+- ServerPlayerConnection serverplayerconnection = (ServerPlayerConnection) iterator.next();
+-
++ // Leaf start - petal - Multithreaded tracker
++ for (ServerPlayerConnection serverplayerconnection : this.seenBy.toArray(new ServerPlayerConnection[0])) {
+ this.serverEntity.removePairing(serverplayerconnection.getPlayer());
+ }
+-
++ // Leaf end - petal - Multithreaded tracker
+ }
+
+ public void removePlayer(ServerPlayer player) {
+- org.spigotmc.AsyncCatcher.catchOp("player tracker clear"); // Spigot
++ //org.spigotmc.AsyncCatcher.catchOp("player tracker clear"); // Spigot // Leaf - petal - Multithreaded tracker - We can remove async too
+ if (this.seenBy.remove(player.connection)) {
+ this.serverEntity.removePairing(player);
+ }
+@@ -1222,7 +1273,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ }
+
+ public void updatePlayer(ServerPlayer player) {
+- org.spigotmc.AsyncCatcher.catchOp("player tracker update"); // Spigot
++ //org.spigotmc.AsyncCatcher.catchOp("player tracker update"); // Spigot // Leaf - petal - Multithreaded tracker - We can update async
+ if (player != this.entity) {
+ // Paper start - remove allocation of Vec3D here
+ // Vec3 vec3d = player.position().subtract(this.entity.position());
+diff --git a/src/main/java/net/minecraft/server/level/ServerBossEvent.java b/src/main/java/net/minecraft/server/level/ServerBossEvent.java
+index 4f91107f9ae42f96c060c310596db9aa869a8dbc..f9889f593ed144ee8f1f5bd380e631c659b0c2b8 100644
+--- a/src/main/java/net/minecraft/server/level/ServerBossEvent.java
++++ b/src/main/java/net/minecraft/server/level/ServerBossEvent.java
+@@ -13,7 +13,9 @@ import net.minecraft.util.Mth;
+ import net.minecraft.world.BossEvent;
+
+ public class ServerBossEvent extends BossEvent {
+- private final Set players = Sets.newHashSet();
++ private final Set players = org.dreeam.leaf.config.modules.async.MultithreadedTracker.enabled
++ ? Sets.newConcurrentHashSet()
++ : Sets.newHashSet(); // Leaf - petal - Multithreaded tracker - players can be removed in async tracking
+ private final Set unmodifiablePlayers = Collections.unmodifiableSet(this.players);
+ public boolean visible = true;
+
+diff --git a/src/main/java/net/minecraft/server/level/ServerEntity.java b/src/main/java/net/minecraft/server/level/ServerEntity.java
+index 05125144ce0cb50fa6ac769fa025cda010c93f14..3b40fc420ec1a8aca4c66a77f54cf628a39aa6f2 100644
+--- a/src/main/java/net/minecraft/server/level/ServerEntity.java
++++ b/src/main/java/net/minecraft/server/level/ServerEntity.java
+@@ -336,7 +336,11 @@ public class ServerEntity {
+
+ public void removePairing(ServerPlayer player) {
+ this.entity.stopSeenByPlayer(player);
+- player.connection.send(new ClientboundRemoveEntitiesPacket(new int[]{this.entity.getId()}));
++ // Leaf start - petal - Multithreaded tracker - send in main thread
++ ((ServerLevel) this.entity.level()).chunkSource.chunkMap.runOnTrackerMainThread(() ->
++ player.connection.send(new ClientboundRemoveEntitiesPacket(this.entity.getId()))
++ );
++ // Leaf end - petal - Multithreaded tracker - send in main thread
+ }
+
+ public void addPairing(ServerPlayer player) {
+@@ -344,7 +348,11 @@ public class ServerEntity {
+
+ Objects.requireNonNull(list);
+ this.sendPairingData(player, list::add);
+- player.connection.send(new ClientboundBundlePacket(list));
++ // Leaf start - petal - Multithreaded tracker - send in main thread
++ ((ServerLevel) this.entity.level()).chunkSource.chunkMap.runOnTrackerMainThread(() ->
++ player.connection.send(new ClientboundBundlePacket(list))
++ );
++ // Leaf end - petal - Multithreaded tracker - send in main thread
+ this.entity.startSeenByPlayer(player);
+ }
+
+@@ -464,19 +472,28 @@ public class ServerEntity {
+
+ if (list != null) {
+ this.trackedDataValues = datawatcher.getNonDefaultValues();
+- this.broadcastAndSend(new ClientboundSetEntityDataPacket(this.entity.getId(), list));
++ // Leaf start - petal - Multithreaded tracker - send in main thread
++ ((ServerLevel) this.entity.level()).chunkSource.chunkMap.runOnTrackerMainThread(() ->
++ this.broadcastAndSend(new ClientboundSetEntityDataPacket(this.entity.getId(), list))
++ );
++ // Leaf end - petal - Multithreaded tracker - send in main thread
+ }
+
+ if (this.entity instanceof LivingEntity) {
+ Set set = ((LivingEntity) this.entity).getAttributes().getAttributesToSync();
+
+ if (!set.isEmpty()) {
++ // Leaf end - petal - Multithreaded tracker - send in main thread
++ final Set copy = new it.unimi.dsi.fastutil.objects.ObjectOpenHashSet<>(set);
++ ((ServerLevel) this.entity.level()).chunkSource.chunkMap.runOnTrackerMainThread(() -> {
+ // CraftBukkit start - Send scaled max health
+ if (this.entity instanceof ServerPlayer) {
+- ((ServerPlayer) this.entity).getBukkitEntity().injectScaledMaxHealth(set, false);
++ ((ServerPlayer) this.entity).getBukkitEntity().injectScaledMaxHealth(copy, false);
+ }
+ // CraftBukkit end
+- this.broadcastAndSend(new ClientboundUpdateAttributesPacket(this.entity.getId(), set));
++ this.broadcastAndSend(new ClientboundUpdateAttributesPacket(this.entity.getId(), copy));
++ });
++ // Leaf end - petal - Multithreaded tracker - send in main thread
+ }
+
+ set.clear();
+diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
+index dab20d348d7542a985a2510b37029ff97e8be1f6..44f128f7d6741d47f9a4bbd92e147b4011447a50 100644
+--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
+@@ -2399,7 +2399,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf.
+
+ @Override
+ public LevelEntityGetter getEntities() {
+- org.spigotmc.AsyncCatcher.catchOp("Chunk getEntities call"); // Spigot
++ //org.spigotmc.AsyncCatcher.catchOp("Chunk getEntities call"); // Spigot // Leaf - petal - Multithreaded tracker
+ return this.moonrise$getEntityLookup(); // Paper - rewrite chunk system
+ }
+
+diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
+index 5ee48b2347b4d588206d4c4aabd47a3918046973..2f0184997c55f35d081bcaed29c763455684b621 100644
+--- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
++++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
+@@ -1816,7 +1816,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
+ }
+
+ public void internalTeleport(double d0, double d1, double d2, float f, float f1, Set set) { // Paper
+- org.spigotmc.AsyncCatcher.catchOp("teleport"); // Paper
++ //org.spigotmc.AsyncCatcher.catchOp("teleport"); // Paper // Leaf - petal - Multithreaded tracker
+ // Paper start - Prevent teleporting dead entities
+ if (player.isRemoved()) {
+ LOGGER.info("Attempt to teleport removed player {} restricted", player.getScoreboardName());
+diff --git a/src/main/java/net/minecraft/world/entity/ai/attributes/AttributeMap.java b/src/main/java/net/minecraft/world/entity/ai/attributes/AttributeMap.java
+index 14ceb3308474e76220bd64b0254df3f2925d4206..5fc03bf452082d13c577e2fcf49288c5c20c2d19 100644
+--- a/src/main/java/net/minecraft/world/entity/ai/attributes/AttributeMap.java
++++ b/src/main/java/net/minecraft/world/entity/ai/attributes/AttributeMap.java
+@@ -19,11 +19,20 @@ import org.slf4j.Logger;
+
+ public class AttributeMap {
+ private static final Logger LOGGER = LogUtils.getLogger();
++ // Leaf start - petal - Multithreaded tracker
++ private final boolean multiThreadedTrackingEnabled = org.dreeam.leaf.config.modules.async.MultithreadedTracker.enabled;
+ // Gale start - Lithium - replace AI attributes with optimized collections
+- private final Map, AttributeInstance> attributes = new it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap<>(0);
+- private final Set attributesToSync = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>(0);
+- private final Set attributesToUpdate = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>(0);
++ private final Map, AttributeInstance> attributes = multiThreadedTrackingEnabled
++ ? new java.util.concurrent.ConcurrentHashMap<>()
++ : new it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap<>(0);
++ private final Set attributesToSync = multiThreadedTrackingEnabled
++ ? com.google.common.collect.Sets.newConcurrentHashSet()
++ : new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>(0);
++ private final Set attributesToUpdate = multiThreadedTrackingEnabled
++ ? com.google.common.collect.Sets.newConcurrentHashSet()
++ : new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>(0);
+ // Gale end - Lithium - replace AI attributes with optimized collections
++ // Leaf end - petal - Multithreaded tracker
+ private final AttributeSupplier supplier;
+ private final java.util.function.Function, AttributeInstance> createInstance; // Gale - Airplane - reduce entity allocations
+ private final net.minecraft.world.entity.LivingEntity entity; // Purpur
+diff --git a/src/main/java/org/dreeam/leaf/async/tracker/MultithreadedTracker.java b/src/main/java/org/dreeam/leaf/async/tracker/MultithreadedTracker.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..16c59bdeeaa7f114c912e4b3d6409272e0f682bf
+--- /dev/null
++++ b/src/main/java/org/dreeam/leaf/async/tracker/MultithreadedTracker.java
+@@ -0,0 +1,162 @@
++package org.dreeam.leaf.async.tracker;
++
++import ca.spottedleaf.moonrise.common.list.ReferenceList;
++import ca.spottedleaf.moonrise.common.misc.NearbyPlayers;
++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
++import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server.ServerEntityLookup;
++import ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity;
++import ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerTrackedEntity;
++import com.google.common.util.concurrent.ThreadFactoryBuilder;
++import net.minecraft.server.level.ChunkMap;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.world.entity.Entity;
++
++import java.util.Arrays;
++import java.util.concurrent.Executor;
++import java.util.concurrent.LinkedBlockingQueue;
++import java.util.concurrent.ThreadPoolExecutor;
++import java.util.concurrent.TimeUnit;
++
++public class MultithreadedTracker {
++
++ private static final Executor trackerExecutor = new ThreadPoolExecutor(
++ 1,
++ org.dreeam.leaf.config.modules.async.MultithreadedTracker.asyncEntityTrackerMaxThreads,
++ org.dreeam.leaf.config.modules.async.MultithreadedTracker.asyncEntityTrackerKeepalive, TimeUnit.SECONDS,
++ new LinkedBlockingQueue<>(),
++ new ThreadFactoryBuilder()
++ .setNameFormat("Leaf Async Tracker Thread - %d")
++ .setPriority(Thread.NORM_PRIORITY - 2)
++ .build());
++
++ private MultithreadedTracker() {
++ }
++
++ public static Executor getTrackerExecutor() {
++ return trackerExecutor;
++ }
++
++ public static void tick(ChunkSystemServerLevel level) {
++ if (!org.dreeam.leaf.config.modules.async.MultithreadedTracker.compatModeEnabled) {
++ tickAsync(level);
++ } else {
++ tickAsyncWithCompatMode(level);
++ }
++ }
++
++ private static void tickAsync(ChunkSystemServerLevel level) {
++ final NearbyPlayers nearbyPlayers = level.moonrise$getNearbyPlayers();
++ final ServerEntityLookup entityLookup = (ServerEntityLookup) level.moonrise$getEntityLookup();
++
++ final ReferenceList trackerEntities = entityLookup.trackerEntities;
++ final Entity[] trackerEntitiesRaw = trackerEntities.getRawDataUnchecked();
++
++ // Move tracking to off-main
++ trackerExecutor.execute(() -> {
++ for (final Entity entity : trackerEntitiesRaw) {
++ if (entity == null) continue;
++
++ final ChunkMap.TrackedEntity tracker = ((EntityTrackerEntity) entity).moonrise$getTrackedEntity();
++
++ if (tracker == null) continue;
++
++ ((EntityTrackerTrackedEntity) tracker).moonrise$tick(nearbyPlayers.getChunk(entity.chunkPosition()));
++ tracker.serverEntity.sendChanges();
++ }
++ });
++
++ // process unloads
++ final ReferenceList unloadedEntities = entityLookup.trackerUnloadedEntities;
++ final Entity[] unloadedEntitiesRaw = Arrays.copyOf(unloadedEntities.getRawDataUnchecked(), unloadedEntities.size());
++ unloadedEntities.clear();
++
++ // Move player unload to off-main
++ trackerExecutor.execute(() -> {
++ for (final Entity entity : unloadedEntitiesRaw) {
++ final ChunkMap.TrackedEntity tracker = ((EntityTrackerEntity) entity).moonrise$getTrackedEntity();
++
++ if (tracker == null) continue;
++
++ ((EntityTrackerTrackedEntity) tracker).moonrise$clearPlayers();
++ }
++ });
++ }
++
++ private static void tickAsyncWithCompatMode(ChunkSystemServerLevel level) {
++ final NearbyPlayers nearbyPlayers = level.moonrise$getNearbyPlayers();
++ final ServerEntityLookup entityLookup = (ServerEntityLookup) level.moonrise$getEntityLookup();
++
++ final ReferenceList trackerEntities = entityLookup.trackerEntities;
++ final Entity[] trackerEntitiesRaw = trackerEntities.getRawDataUnchecked();
++ final Runnable[] sendChangesTasks = new Runnable[trackerEntitiesRaw.length];
++ int index = 0;
++
++ for (final Entity entity : trackerEntitiesRaw) {
++ if (entity == null) continue;
++
++ final ChunkMap.TrackedEntity tracker = ((EntityTrackerEntity) entity).moonrise$getTrackedEntity();
++
++ if (tracker == null) continue;
++
++ ((EntityTrackerTrackedEntity) tracker).moonrise$tick(nearbyPlayers.getChunk(entity.chunkPosition()));
++ sendChangesTasks[index++] = () -> tracker.serverEntity.sendChanges(); // Collect send changes to task array
++ }
++
++ // batch submit tasks
++ trackerExecutor.execute(() -> {
++ for (final Runnable sendChanges : sendChangesTasks) {
++ if (sendChanges == null) continue;
++
++ sendChanges.run();
++ }
++ });
++
++ // process unloads
++ final ReferenceList unloadedEntities = entityLookup.trackerUnloadedEntities;
++ final Entity[] unloadedEntitiesRaw = Arrays.copyOf(unloadedEntities.getRawDataUnchecked(), unloadedEntities.size());
++ unloadedEntities.clear();
++
++ trackerExecutor.execute(() -> {
++ for (final Entity entity : unloadedEntitiesRaw) {
++ final ChunkMap.TrackedEntity tracker = ((EntityTrackerEntity) entity).moonrise$getTrackedEntity();
++
++ if (tracker == null) continue;
++
++ ((EntityTrackerTrackedEntity) tracker).moonrise$clearPlayers();
++ }
++ });
++ }
++
++ // Original ChunkMap#newTrackerTick of Paper
++ // Just for diff usage for future update
++ private static void tickOriginal(ServerLevel level) {
++ final ca.spottedleaf.moonrise.common.misc.NearbyPlayers nearbyPlayers = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel) level).moonrise$getNearbyPlayers();
++ final ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server.ServerEntityLookup entityLookup = (ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server.ServerEntityLookup) ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel) level).moonrise$getEntityLookup();
++ ;
++
++ final ca.spottedleaf.moonrise.common.list.ReferenceList trackerEntities = entityLookup.trackerEntities;
++ final Entity[] trackerEntitiesRaw = trackerEntities.getRawDataUnchecked();
++ for (int i = 0, len = trackerEntities.size(); i < len; ++i) {
++ final Entity entity = trackerEntitiesRaw[i];
++ final ChunkMap.TrackedEntity tracker = ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity) entity).moonrise$getTrackedEntity();
++ if (tracker == null) {
++ continue;
++ }
++ ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerTrackedEntity) tracker).moonrise$tick(nearbyPlayers.getChunk(entity.chunkPosition()));
++ tracker.serverEntity.sendChanges();
++ }
++
++ // process unloads
++ final ca.spottedleaf.moonrise.common.list.ReferenceList unloadedEntities = entityLookup.trackerUnloadedEntities;
++ final Entity[] unloadedEntitiesRaw = java.util.Arrays.copyOf(unloadedEntities.getRawDataUnchecked(), unloadedEntities.size());
++ unloadedEntities.clear();
++
++ for (final Entity entity : unloadedEntitiesRaw) {
++ final ChunkMap.TrackedEntity tracker = ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity) entity).moonrise$getTrackedEntity();
++ if (tracker == null) {
++ continue;
++ }
++ ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerTrackedEntity) tracker).moonrise$clearPlayers();
++ }
++ }
++}
+diff --git a/src/main/java/org/dreeam/leaf/config/modules/async/MultithreadedTracker.java b/src/main/java/org/dreeam/leaf/config/modules/async/MultithreadedTracker.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..33da9733f0cc0e414189152afa7c757f4ffd1900
+--- /dev/null
++++ b/src/main/java/org/dreeam/leaf/config/modules/async/MultithreadedTracker.java
+@@ -0,0 +1,43 @@
++package org.dreeam.leaf.config.modules.async;
++
++import org.dreeam.leaf.config.ConfigModules;
++import org.dreeam.leaf.config.EnumConfigCategory;
++import org.dreeam.leaf.config.LeafConfig;
++
++public class MultithreadedTracker extends ConfigModules {
++
++ public String getBasePath() {
++ return EnumConfigCategory.ASYNC.getBaseKeyName() + ".async-entity-tracker";
++ }
++
++ public static boolean enabled = true;
++ public static boolean compatModeEnabled = false;
++ public static int asyncEntityTrackerMaxThreads = 0;
++ public static int asyncEntityTrackerKeepalive = 60;
++
++ @Override
++ public void onLoaded() {
++ config.addComment(getBasePath(), """
++ Make entity tracking saving asynchronously, can improve performance significantly,
++ especially in some massive entities in small area situations.
++ """);
++
++ enabled = config().getBoolean(getBasePath() + ".enabled", enabled);
++ compatModeEnabled = config.getBoolean(getBasePath() + ".compat-mode", compatModeEnabled, """
++ Enable compat mode ONLY if Citizens or NPC plugins using real entity has installed,
++ Compat mode fixed visible issue with player type NPCs of Citizens,
++ But still recommend to use packet based / virtual entity NPC plugin, e.g. ZNPC Plus, Adyeshach, Fancy NPC or else.""");
++ asyncEntityTrackerMaxThreads = config.getInt(getBasePath() + ".max-threads", asyncEntityTrackerMaxThreads);
++ asyncEntityTrackerKeepalive = config.getInt(getBasePath() + ".keepalive", asyncEntityTrackerKeepalive);
++
++ if (asyncEntityTrackerMaxThreads < 0)
++ asyncEntityTrackerMaxThreads = Math.max(Runtime.getRuntime().availableProcessors() + asyncEntityTrackerMaxThreads, 1);
++ else if (asyncEntityTrackerMaxThreads == 0)
++ asyncEntityTrackerMaxThreads = Math.max(Runtime.getRuntime().availableProcessors() / 4, 1);
++
++ if (!enabled)
++ asyncEntityTrackerMaxThreads = 0;
++ else
++ LeafConfig.LOGGER.info("Using {} threads for Async Entity Tracker", asyncEntityTrackerMaxThreads);
++ }
++}
diff --git a/patches/server/0101-Nitori-Async-playerdata-Save.patch b/patches/server/0101-Nitori-Async-playerdata-Save.patch
new file mode 100644
index 00000000..f6eaa542
--- /dev/null
+++ b/patches/server/0101-Nitori-Async-playerdata-Save.patch
@@ -0,0 +1,111 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Dreeam <61569423+Dreeam-qwq@users.noreply.github.com>
+Date: Fri, 23 Aug 2024 22:04:20 -0400
+Subject: [PATCH] Nitori: Async playerdata Save
+
+Original license: GPL v3
+Original project: https://github.com/Gensokyo-Reimagined/Nitori
+
+diff --git a/src/main/java/net/minecraft/world/level/storage/LevelStorageSource.java b/src/main/java/net/minecraft/world/level/storage/LevelStorageSource.java
+index 85ba843ce7e1f62971e736fa2cc028c47b274ce4..7d018095f9cafbe727be41655742875bee2c028b 100644
+--- a/src/main/java/net/minecraft/world/level/storage/LevelStorageSource.java
++++ b/src/main/java/net/minecraft/world/level/storage/LevelStorageSource.java
+@@ -605,7 +605,11 @@ public class LevelStorageSource {
+ CompoundTag nbttagcompound2 = new CompoundTag();
+
+ nbttagcompound2.put("Data", nbttagcompound1);
+- this.saveLevelData(nbttagcompound2);
++
++ // Leaf start - Nitori - Async playerdata save
++ Runnable runnable = () -> this.saveLevelData(nbttagcompound2);
++ org.dreeam.leaf.async.AsyncPlayerDataSaving.saveAsync(runnable);
++ // Leaf end - Nitori - Async playerdata save
+ }
+
+ private void saveLevelData(CompoundTag nbt) {
+@@ -702,7 +706,11 @@ public class LevelStorageSource {
+ CompoundTag nbttagcompound = LevelStorageSource.readLevelDataTagRaw(this.levelDirectory.dataFile());
+
+ nbtProcessor.accept(nbttagcompound.getCompound("Data"));
+- this.saveLevelData(nbttagcompound);
++
++ // Leaf start - Nitori - Async playerdata save
++ Runnable runnable = () -> this.saveLevelData(nbttagcompound);
++ org.dreeam.leaf.async.AsyncPlayerDataSaving.saveAsync(runnable);
++ // Leaf end - Nitori - Async playerdata save
+ }
+
+ public long makeWorldBackup() throws IOException {
+diff --git a/src/main/java/net/minecraft/world/level/storage/PlayerDataStorage.java b/src/main/java/net/minecraft/world/level/storage/PlayerDataStorage.java
+index b148cf247acdd36f856d0495cde4cc5ad32b5a2f..e825d9e573a38531f5a3b3f9cdccc24570953015 100644
+--- a/src/main/java/net/minecraft/world/level/storage/PlayerDataStorage.java
++++ b/src/main/java/net/minecraft/world/level/storage/PlayerDataStorage.java
+@@ -36,6 +36,13 @@ public class PlayerDataStorage {
+
+ public void save(Player player) {
+ if (org.spigotmc.SpigotConfig.disablePlayerDataSaving) return; // Spigot
++
++ // Leaf start - Nitori - Async playerdata save
++ Runnable runnable = () -> save0(player);
++ org.dreeam.leaf.async.AsyncPlayerDataSaving.saveAsync(runnable);
++ }
++ private void save0(Player player) {
++ // Leaf end - Nitori - Async playerdata save
+ try {
+ CompoundTag nbttagcompound = player.saveWithoutId(new CompoundTag());
+ Path path = this.playerDir.toPath();
+diff --git a/src/main/java/org/dreeam/leaf/async/AsyncPlayerDataSaving.java b/src/main/java/org/dreeam/leaf/async/AsyncPlayerDataSaving.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..6f74ca2f5bdae24434255976ec24f28c4980ac17
+--- /dev/null
++++ b/src/main/java/org/dreeam/leaf/async/AsyncPlayerDataSaving.java
+@@ -0,0 +1,23 @@
++package org.dreeam.leaf.async;
++
++import net.minecraft.Util;
++import org.dreeam.leaf.config.modules.async.AsyncPlayerDataSave;
++
++import java.util.concurrent.CompletableFuture;
++import java.util.concurrent.ExecutorService;
++
++public class AsyncPlayerDataSaving {
++
++ private AsyncPlayerDataSaving() {
++ }
++
++ public static void saveAsync(Runnable runnable) {
++ if (!AsyncPlayerDataSave.enabled) {
++ runnable.run();
++ return;
++ }
++
++ ExecutorService ioExecutor = Util.backgroundExecutor();
++ CompletableFuture.runAsync(runnable, ioExecutor);
++ }
++}
+diff --git a/src/main/java/org/dreeam/leaf/config/modules/async/AsyncPlayerDataSave.java b/src/main/java/org/dreeam/leaf/config/modules/async/AsyncPlayerDataSave.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..f1d715723a641dc042a7f645398169e6f483700c
+--- /dev/null
++++ b/src/main/java/org/dreeam/leaf/config/modules/async/AsyncPlayerDataSave.java
+@@ -0,0 +1,20 @@
++package org.dreeam.leaf.config.modules.async;
++
++import org.dreeam.leaf.config.ConfigModules;
++import org.dreeam.leaf.config.EnumConfigCategory;
++
++public class AsyncPlayerDataSave extends ConfigModules {
++
++ public String getBasePath() {
++ return EnumConfigCategory.ASYNC.getBaseKeyName() + ".async-playerdata-save";
++ }
++
++ public static boolean enabled = true;
++
++ @Override
++ public void onLoaded() {
++ config.addComment(getBasePath(), "Make PlayerData saving asynchronously.");
++
++ enabled = config().getBoolean(getBasePath() + ".enabled", enabled);
++ }
++}