From ea00b11d11053980bde678a563169f72671fe381 Mon Sep 17 00:00:00 2001 From: hayanesuru Date: Wed, 16 Jul 2025 02:42:37 +0900 Subject: [PATCH] Refactor async entity tracker (#390) * prevents async entity tracker update equipment * fix seenBy updated check * skip submit empty * fix invertedVisibilityEntities data race * strict thread check * set max-threads to 1 by default * use fixed thread count * increase thread priority * Revert "use fixed thread count" This reverts commit 6746bc25a8e920ac858f9a1d7341284eb58dcb92. * Revert "set max-threads to 1 by default" This reverts commit 5295b6d3e1b6479d23597223ceac912120dc373f. * update entity tracker * cleanup * [ci skip] fix phrasing * cleanup * cleanup * support Citizens * optimize update if chunk player no change * configurable threads * configurable no blocking * fix pos y and z * optimize no blocking * cleanup * cleanup * add handle during waitUntilNextTick * fix entity disappear * cleanup * disable nonblocking by default * [ci skip] add entity slice * impl fork-join * fix async locator diff * optimize queue * inline iterator * [ci skip] Update patch header * cleanup * improve compatibility * add license header * optimize spin wait * remove queue-size option * dynamic adjust subtasks --------- Co-authored-by: Taiyou06 Co-authored-by: Dreeam <61569423+Dreeam-qwq@users.noreply.github.com> --- .../features/0169-Multithreaded-Tracker.patch | 1093 ++++++++++++++--- .../features/0186-Cache-chunk-key.patch | 10 +- ...checking-nearby-players-for-spawning.patch | 6 +- .../features/0201-optimize-mob-despawn.patch | 8 +- .../features/0209-Async-chunk-sending.patch | 4 +- ...-SparklyPaper-Parallel-world-ticking.patch | 30 +- ...2-SparklyPaper-Track-each-world-MSPT.patch | 6 +- .../0216-Use-BFS-on-getSlopeDistance.patch | 4 +- ...-Micro-optimizations-for-random-tick.patch | 6 +- .../features/0230-Async-target-finding.patch | 12 +- .../features/0237-Protocol-Core.patch | 6 +- .../features/0257-optimize-mob-spawning.patch | 8 +- .../features/0264-optimize-random-tick.patch | 4 +- .../features/0271-Paw-optimization.patch | 6 +- .../features/0042-Multithreaded-Tracker.patch | 60 +- .../features/0043-Asynchronous-locator.patch | 10 +- .../leaf/async/FixedThreadExecutor.java | 85 ++ .../dreeam/leaf/async/ShutdownExecutors.java | 10 +- .../leaf/async/tracker/AsyncTracker.java | 91 ++ .../async/tracker/MultithreadedTracker.java | 211 ---- .../dreeam/leaf/async/tracker/TrackerCtx.java | 274 +++++ .../leaf/async/tracker/TrackerTask.java | 56 + .../modules/async/MultithreadedTracker.java | 53 +- .../org/dreeam/leaf/util/EntitySlice.java | 108 ++ .../org/dreeam/leaf/util/queue/MpmcQueue.java | 231 ++++ 25 files changed, 1887 insertions(+), 505 deletions(-) create mode 100644 leaf-server/src/main/java/org/dreeam/leaf/async/FixedThreadExecutor.java create mode 100644 leaf-server/src/main/java/org/dreeam/leaf/async/tracker/AsyncTracker.java delete mode 100644 leaf-server/src/main/java/org/dreeam/leaf/async/tracker/MultithreadedTracker.java create mode 100644 leaf-server/src/main/java/org/dreeam/leaf/async/tracker/TrackerCtx.java create mode 100644 leaf-server/src/main/java/org/dreeam/leaf/async/tracker/TrackerTask.java create mode 100644 leaf-server/src/main/java/org/dreeam/leaf/util/EntitySlice.java create mode 100644 leaf-server/src/main/java/org/dreeam/leaf/util/queue/MpmcQueue.java diff --git a/leaf-server/minecraft-patches/features/0169-Multithreaded-Tracker.patch b/leaf-server/minecraft-patches/features/0169-Multithreaded-Tracker.patch index 8126aed5..f8b7f287 100644 --- a/leaf-server/minecraft-patches/features/0169-Multithreaded-Tracker.patch +++ b/leaf-server/minecraft-patches/features/0169-Multithreaded-Tracker.patch @@ -12,19 +12,22 @@ Original project: https://github.com/TECHNOVE/Airplane-Experimental Co-authored-by: Paul Sauve Co-authored-by: Kevin Raneri Co-authored-by: HaHaWTH <102713261+HaHaWTH@users.noreply.github.com> +Co-authored-by: hayanesuru 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 +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. +The core logic has beed reworked compared to the old one. +Current impl includes many improvements and fixes we made, such as +plugin compat issues with some NPC plugins 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. +However we still recommend to use those packet based NPC plugins, +e.g. ZNPC Plus, Adyeshach, Fancy NPC, etc. diff --git a/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java b/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java -index 1b8193587814225c2ef2c5d9e667436eb50ff6c5..4200d22606c6a3dbdf282792a4007a51df66963b 100644 +index 1b8193587814225c2ef2c5d9e667436eb50ff6c5..2a2626e90836ae52a8a686b2040843c6644b6914 100644 --- a/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java +++ b/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java @@ -60,7 +60,16 @@ public final class NearbyPlayers { @@ -45,8 +48,56 @@ index 1b8193587814225c2ef2c5d9e667436eb50ff6c5..4200d22606c6a3dbdf282792a4007a51 private final Long2ReferenceOpenHashMap>[] directByChunk = new Long2ReferenceOpenHashMap[TOTAL_MAP_TYPES]; { for (int i = 0; i < this.directByChunk.length; ++i) { +@@ -164,6 +173,8 @@ public final class NearbyPlayers { + private int nonEmptyLists; + private long updateCount; + ++ public final it.unimi.dsi.fastutil.objects.ReferenceSet playersTracking = org.dreeam.leaf.config.modules.async.MultithreadedTracker.enabled ? it.unimi.dsi.fastutil.objects.ReferenceSets.synchronize(new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>(1)) : null; // Leaf - Multithreaded tracker ++ public final java.util.concurrent.atomic.AtomicLong trackingUpdateCountAtomic = org.dreeam.leaf.config.modules.async.MultithreadedTracker.enabled ? new java.util.concurrent.atomic.AtomicLong(0) : null; // Leaf - Multithreaded tracker + public TrackedChunk(final long chunkKey, final NearbyPlayers nearbyPlayers) { + this.chunkKey = chunkKey; + this.nearbyPlayers = nearbyPlayers; +@@ -177,6 +188,12 @@ public final class NearbyPlayers { + return this.updateCount; + } + ++ // Leaf start - Multithreaded tracker ++ public long getAtomicUpdateCount() { ++ return this.trackingUpdateCountAtomic.get() | 0x1000000000000000L; ++ } ++ // Leaf end - Multithreaded tracker ++ + public ReferenceList getPlayers(final NearbyMapType type) { + return this.players[type.ordinal()]; + } +@@ -185,6 +202,12 @@ public final class NearbyPlayers { + ++this.updateCount; + + final int idx = type.ordinal(); ++ // Leaf start - Multithreaded tracker ++ if (org.dreeam.leaf.config.modules.async.MultithreadedTracker.enabled && idx == NearbyMapType.VIEW_DISTANCE.ordinal()) { ++ this.trackingUpdateCountAtomic.getAndIncrement(); ++ this.playersTracking.add(player); ++ } ++ // Leaf end - Multithreaded tracker + final ReferenceList list = this.players[idx]; + if (list == null) { + ++this.nonEmptyLists; +@@ -203,6 +226,12 @@ public final class NearbyPlayers { + ++this.updateCount; + + final int idx = type.ordinal(); ++ // Leaf start - Multithreaded tracker ++ if (org.dreeam.leaf.config.modules.async.MultithreadedTracker.enabled && type == NearbyMapType.VIEW_DISTANCE) { ++ this.trackingUpdateCountAtomic.getAndIncrement(); ++ this.playersTracking.remove(player); ++ } ++ // Leaf end - Multithreaded tracker + final ReferenceList list = this.players[idx]; + if (list == null) { + throw new IllegalStateException("Does not contain player " + player); diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java b/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java -index bdc1200ef5317fdaf58973bf580b0a672aee800f..0bca3843e8568b37cda6ae312bdf4f423a0891a9 100644 +index bdc1200ef5317fdaf58973bf580b0a672aee800f..20b1186f53c267f69ed7852f5cf3dd2460f8200d 100644 --- a/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java +++ b/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java @@ -344,7 +344,7 @@ public final class RegionizedPlayerChunkLoader { @@ -54,10 +105,25 @@ index bdc1200ef5317fdaf58973bf580b0a672aee800f..0bca3843e8568b37cda6ae312bdf4f42 private final ArrayDeque> delayedTicketOps = new ArrayDeque<>(); - private final LongOpenHashSet sentChunks = new LongOpenHashSet(); -+ private final LongOpenHashSet sentChunks = org.dreeam.leaf.config.modules.async.MultithreadedTracker.enabled && !org.dreeam.leaf.config.modules.async.MultithreadedTracker.compatModeEnabled ? new org.dreeam.leaf.util.map.ConcurrentLongHashSet() : new LongOpenHashSet(); // Leaf - Multithreaded tracker ++ private final LongOpenHashSet sentChunks = org.dreeam.leaf.config.modules.async.MultithreadedTracker.enabled ? new org.dreeam.leaf.util.map.ConcurrentLongHashSet() : new LongOpenHashSet(); // Leaf - Multithreaded tracker private static final byte CHUNK_TICKET_STAGE_NONE = 0; private static final byte CHUNK_TICKET_STAGE_LOADING = 1; +@@ -422,6 +422,14 @@ public final class RegionizedPlayerChunkLoader { + + PlatformHooks.get().onChunkWatch(this.world, chunk, this.player); + PlayerChunkSender.sendChunk(this.player.connection, this.world, chunk); ++ // Leaf start - Multithreaded tracker ++ if (org.dreeam.leaf.config.modules.async.MultithreadedTracker.enabled) { ++ ca.spottedleaf.moonrise.common.misc.NearbyPlayers.TrackedChunk trackedChunk = this.world.moonrise$getNearbyPlayers().getChunk(chunkX, chunkZ); ++ if (trackedChunk != null) { ++ trackedChunk.trackingUpdateCountAtomic.getAndIncrement(); ++ } ++ } ++ // Leaf end - Multithreaded tracker + return; + } + throw new IllegalStateException(); diff --git a/net/minecraft/network/protocol/game/ClientboundUpdateAttributesPacket.java b/net/minecraft/network/protocol/game/ClientboundUpdateAttributesPacket.java index 9c0c99b936b4a82ebfe924866e53ec71f7bbe9ad..2ccff968cb2065d34fad4d27573f9e3081edb2f2 100644 --- a/net/minecraft/network/protocol/game/ClientboundUpdateAttributesPacket.java @@ -70,68 +136,88 @@ index 9c0c99b936b4a82ebfe924866e53ec71f7bbe9ad..2ccff968cb2065d34fad4d27573f9e30 this.attributes .add( new ClientboundUpdateAttributesPacket.AttributeSnapshot( +diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java +index 07943aa3be4222ab7a63b09a6625f7a003da8725..6c1a8925b1e13a0ebddc1b45a980fe8bdd8676cf 100644 +--- a/net/minecraft/server/MinecraftServer.java ++++ b/net/minecraft/server/MinecraftServer.java +@@ -1732,6 +1732,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop, ServerLevel> oldLevels = this.levels; + Map, ServerLevel> newLevels = Maps.newLinkedHashMap(oldLevels); + newLevels.remove(level.dimension()); ++ // Leaf start - Multithreaded tracker ++ if (org.dreeam.leaf.config.modules.async.MultithreadedTracker.enabled) { ++ level.trackerTask = null; ++ } ++ // Leaf end - Multithreaded tracker + this.levels = Collections.unmodifiableMap(newLevels); + } + // CraftBukkit end diff --git a/net/minecraft/server/level/ChunkMap.java b/net/minecraft/server/level/ChunkMap.java -index 74d11e8983f12f6f33fe2eb3016730507e1031d4..f621cfea59b6d2f2fb29333e50860584b7992c26 100644 +index 74d11e8983f12f6f33fe2eb3016730507e1031d4..42555d9fc1b37696b8da4c85f9a809819bcc6dde 100644 --- a/net/minecraft/server/level/ChunkMap.java +++ b/net/minecraft/server/level/ChunkMap.java -@@ -255,6 +255,15 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - } - - final ServerPlayer[] backingSet = inRange.getRawDataUnchecked(); -+ // Leaf start - Multithreaded tracker -+ if (org.dreeam.leaf.config.modules.async.MultithreadedTracker.enabled) -+ for (int i = 0, len = inRange.size(); i < len; i++) { -+ final ServerPlayer player = backingSet[i]; -+ if (player == null) continue; -+ ++(player.mobCounts[index]); -+ } -+ else -+ // Leaf end - Multithreaded tracker - for (int i = 0, len = inRange.size(); i < len; i++) { - ++(backingSet[i].mobCounts[index]); - } -@@ -1013,6 +1022,13 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider +@@ -1013,6 +1013,13 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider // Paper end - optimise entity tracker protected void tick() { + // Leaf start - Multithreaded tracker + if (org.dreeam.leaf.config.modules.async.MultithreadedTracker.enabled) { + final ServerLevel level = this.level; -+ org.dreeam.leaf.async.tracker.MultithreadedTracker.tick(level); ++ org.dreeam.leaf.async.tracker.AsyncTracker.tick(level); + return; + } + // Leaf end - Multithreaded tracker // Paper start - optimise entity tracker if (true) { this.newTrackerTick(); -@@ -1135,7 +1151,42 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider +@@ -1135,12 +1142,60 @@ 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 + // Leaf start - Multithreaded tracker + public static final ServerPlayerConnection[] EMPTY_OBJECT_ARRAY = new ServerPlayerConnection[0]; -+ private final it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet nonSyncSeenBy = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>() { ++ ++ private class SeenBySet extends it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet { + @Override + public boolean add(ServerPlayerConnection serverPlayerConnection) { -+ seenByUpdated = true; -+ return super.add(serverPlayerConnection); ++ if (super.add(serverPlayerConnection)) { ++ // for plugin compatibility ++ TrackedEntity.this.seenByUpdated = true; ++ return true; ++ } else { ++ return false; ++ } + } + + @Override + public boolean remove(Object k) { -+ seenByUpdated = true; -+ return super.remove(k); ++ if (super.remove(k)) { ++ // for plugin compatibility ++ TrackedEntity.this.seenByUpdated = true; ++ return true; ++ } else { ++ return false; ++ } + } + + @Override + public void clear() { -+ seenByUpdated = true; ++ TrackedEntity.this.seenByUpdated = true; + super.clear(); + } -+ }; -+ public final Set seenBy = org.dreeam.leaf.config.modules.async.MultithreadedTracker.enabled ? it.unimi.dsi.fastutil.objects.ReferenceSets.synchronize(nonSyncSeenBy) : nonSyncSeenBy; // Paper - Perf: optimise map impl -+ private volatile boolean seenByUpdated = true; ++ } ++ ++ public final Set seenBy = org.dreeam.leaf.config.modules.async.MultithreadedTracker.enabled ? it.unimi.dsi.fastutil.objects.ReferenceSets.synchronize(new SeenBySet()) : new SeenBySet(); // Paper - Perf: optimise map impl ++ private volatile boolean seenByUpdated = false; + private volatile ServerPlayerConnection[] seenByArray = EMPTY_OBJECT_ARRAY; + public ServerPlayerConnection[] seenBy() { + if (!seenByUpdated) { @@ -148,17 +234,36 @@ index 74d11e8983f12f6f33fe2eb3016730507e1031d4..f621cfea59b6d2f2fb29333e50860584 // Paper start - optimise entity tracker private long lastChunkUpdate = -1L; -@@ -1162,27 +1213,95 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + private ca.spottedleaf.moonrise.common.misc.NearbyPlayers.TrackedChunk lastTrackedChunk; + ++ // Leaf - Multithreaded tracker - diff on change + @Override + public final void moonrise$tick(final ca.spottedleaf.moonrise.common.misc.NearbyPlayers.TrackedChunk chunk) { + if (chunk == null) { +@@ -1150,6 +1205,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + + final ca.spottedleaf.moonrise.common.list.ReferenceList players = chunk.getPlayers(ca.spottedleaf.moonrise.common.misc.NearbyPlayers.NearbyMapType.VIEW_DISTANCE); + ++ // Leaf - Multithreaded tracker - diff on change + if (players == null) { + this.moonrise$clearPlayers(); + return; +@@ -1162,27 +1218,120 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider this.lastTrackedChunk = chunk; final ServerPlayer[] playersRaw = players.getRawDataUnchecked(); - - for (int i = 0, len = players.size(); i < len; ++i) { -+ final int playersLength = Math.min(playersRaw.length, players.size()); // Leaf - Multithreaded tracker -+ for (int i = 0; i < playersLength; ++i) { // Leaf - Multithreaded tracker ++ // Leaf start - Multithreaded tracker ++ final int playersLength = Math.min(playersRaw.length, players.size()); ++ boolean updated = false; ++ for (int i = 0; i < playersLength; ++i) { final ServerPlayer player = playersRaw[i]; - this.updatePlayer(player); +- this.updatePlayer(player); ++ updated |= this.updatePlayerMulti(player); } ++ if (updated) seenByUpdated(); ++ // Leaf end - Multithreaded tracker if (lastChunkUpdate != currChunkUpdate || lastTrackedChunk != chunk) { // need to purge any players possible not in the chunk list @@ -170,77 +275,98 @@ index 74d11e8983f12f6f33fe2eb3016730507e1031d4..f621cfea59b6d2f2fb29333e50860584 if (!players.contains(player)) { - this.removePlayer(player); + removed |= this.removePlayerMulti(player); + } + } ++ if (removed) this.seenByUpdated(); ++ // Leaf end - Multithreaded tracker + } + } + ++ // Leaf start - Multithreaded tracker ++ public final void leafTick(final org.dreeam.leaf.async.tracker.TrackerCtx ctx, final ca.spottedleaf.moonrise.common.misc.NearbyPlayers.TrackedChunk chunk) { ++ if (chunk == null || chunk.playersTracking.isEmpty()) { ++ this.lastChunkUpdate = -1L; ++ this.lastTrackedChunk = null; ++ if (!this.seenBy.isEmpty()) { ++ for (final ServerPlayerConnection conn : this.seenBy()) { ++ if (this.seenBy.remove(conn)) { ++ ctx.stopSeenByPlayer(conn, this.entity); ++ ctx.send(conn, new net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket(this.entity.getId())); ++ } + } -+ } -+ if (removed) { + this.seenByUpdated(); + } ++ return; ++ } ++ ++ final it.unimi.dsi.fastutil.objects.ReferenceSet players = chunk.playersTracking; ++ final long currChunkUpdate = chunk.getAtomicUpdateCount(); ++ final boolean chunkStateChanged = this.lastChunkUpdate != currChunkUpdate || this.lastTrackedChunk != chunk; ++ this.lastChunkUpdate = currChunkUpdate; ++ this.lastTrackedChunk = chunk; ++ ++ if (!(chunkStateChanged || ((entity.tickCount + entity.getId()) & 15) == 15)) { ++ return; ++ } ++ ++ boolean updated = false; ++ final double ex = this.entity.getX(); ++ final double ey = this.entity.getY(); ++ final double ez = this.entity.getZ(); ++ final int eChunkX = this.entity.chunkPosition().x; ++ final int eChunkZ = this.entity.chunkPosition().z; ++ final double effectiveRange = this.getEffectiveRange(); ++ final double rangeSqr = effectiveRange * effectiveRange; ++ final double rangeY = level.paperConfig().entities.trackingRangeY.enabled ? level.paperConfig().entities.trackingRangeY.get(this.entity, -1) : -1; ++ final double rangeYSqr = (rangeY > 0.0) ? (rangeY * rangeY) : 0.0; ++ synchronized (players) { ++ for (ServerPlayer player : players) { ++ if (player == this.entity) { ++ continue; ++ } ++ final double dx = player.getX() - ex; ++ final double dz = player.getZ() - ez; ++ final double dy = player.getY() - ey; ++ final double playerViewDistance = ChunkMap.this.getPlayerViewDistance(player); ++ final boolean flag = ((dx * dx + dz * dz) <= Math.min(rangeSqr, playerViewDistance * playerViewDistance * 256.0)) ++ && ((rangeYSqr == 0.0) || ((dy * dy) <= rangeYSqr)) ++ && this.entity.broadcastToPlayer(player) ++ && ChunkMap.this.isChunkTracked(player, eChunkX, eChunkZ) ++ && player.getBukkitEntity().canSeeChunkMapUpdatePlayer(this.entity.getBukkitEntity()); ++ if (flag) { ++ if (this.seenBy.add(player.connection)) { ++ this.serverEntity.leafAddPairing(ctx, player); ++ this.serverEntity.onPlayerAdd(); ++ updated = true; ++ } ++ } else if (this.seenBy.remove(player.connection)) { ++ ctx.stopSeenByPlayer(player.connection, this.entity); ++ ctx.send(player.connection, new net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket(this.entity.getId())); ++ updated = true; ++ } ++ } ++ } ++ if (updated) { ++ this.seenByUpdated(); ++ } ++ if (!chunkStateChanged) { ++ return; ++ } ++ updated = false; ++ for (final ServerPlayerConnection conn : this.seenBy()) { ++ final ServerPlayer player = conn.getPlayer(); ++ if (!players.contains(player) && this.seenBy.remove(conn)) { ++ ctx.stopSeenByPlayer(conn, this.entity); ++ ctx.send(conn, new net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket(this.entity.getId())); ++ updated = true; ++ } ++ } ++ if (updated) { ++ this.seenByUpdated(); + } + } + // Leaf end - Multithreaded tracker + -+ // Leaf start - Multithreaded tracker -+ public final @Nullable Runnable leafTickCompact(final ca.spottedleaf.moonrise.common.misc.NearbyPlayers.TrackedChunk chunk) { -+ if (chunk == null) { -+ this.moonrise$clearPlayers(); -+ return null; -+ } -+ -+ final ca.spottedleaf.moonrise.common.list.ReferenceList players = chunk.getPlayers(ca.spottedleaf.moonrise.common.misc.NearbyPlayers.NearbyMapType.VIEW_DISTANCE); -+ -+ if (players == null) { -+ this.moonrise$clearPlayers(); -+ return null; -+ } -+ -+ final long lastChunkUpdate = this.lastChunkUpdate; -+ final long currChunkUpdate = chunk.getUpdateCount(); -+ final ca.spottedleaf.moonrise.common.misc.NearbyPlayers.TrackedChunk lastTrackedChunk = this.lastTrackedChunk; -+ this.lastChunkUpdate = currChunkUpdate; -+ this.lastTrackedChunk = chunk; -+ -+ final ServerPlayer[] playersRaw = players.getRawDataUnchecked(); -+ final int playersLen = players.size(); // Ensure length won't change in the future tasks -+ -+ if (!org.dreeam.leaf.config.modules.async.MultithreadedTracker.enabled || !org.dreeam.leaf.config.modules.async.MultithreadedTracker.compatModeEnabled) { -+ throw new IllegalStateException(); -+ } -+ 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; i < playersLen; ++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 -+ boolean removed = false; -+ for (final ServerPlayerConnection conn : this.seenBy()) { -+ final ServerPlayer player = conn.getPlayer(); -+ if (!players.contains(player)) { -+ removed |= this.removePlayerMulti(player); -+ } -+ } -+ if (removed) { -+ this.seenByUpdated(); - } - } -+ }; -+ -+ // 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) { -+ return updatePlayerTasks; -+ } else { -+ updatePlayerTasks.run(); -+ return null; - } - } -+ // Leaf end - Multithreaded tracker - @Override public final void moonrise$removeNonTickThreadPlayers() { boolean foundToRemove = false; @@ -249,7 +375,7 @@ index 74d11e8983f12f6f33fe2eb3016730507e1031d4..f621cfea59b6d2f2fb29333e50860584 if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(conn.getPlayer())) { foundToRemove = true; break; -@@ -1193,12 +1312,13 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider +@@ -1193,12 +1342,13 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider return; } @@ -265,7 +391,7 @@ index 74d11e8983f12f6f33fe2eb3016730507e1031d4..f621cfea59b6d2f2fb29333e50860584 } @Override -@@ -1208,10 +1328,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider +@@ -1208,10 +1358,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider if (this.seenBy.isEmpty()) { return; } @@ -279,16 +405,56 @@ index 74d11e8983f12f6f33fe2eb3016730507e1031d4..f621cfea59b6d2f2fb29333e50860584 } @Override -@@ -1238,7 +1359,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider +@@ -1238,20 +1389,46 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } public void broadcast(Packet packet) { - for (ServerPlayerConnection serverPlayerConnection : this.seenBy) { ++ // Leaf - Multithreaded tracker - diff on change + for (ServerPlayerConnection serverPlayerConnection : this.seenBy()) { // Leaf - petal - Multithreaded tracker serverPlayerConnection.send(packet); } } -@@ -1259,21 +1380,34 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + + public void broadcastIgnorePlayers(Packet packet, List ignoredPlayers) { +- for (ServerPlayerConnection serverPlayerConnection : this.seenBy) { ++ // Leaf - Multithreaded tracker - diff on change ++ for (ServerPlayerConnection serverPlayerConnection : this.seenBy()) { + if (!ignoredPlayers.contains(serverPlayerConnection.getPlayer().getUUID())) { + serverPlayerConnection.send(packet); + } + } + } + ++ // Leaf start - Multithreaded tracker ++ public void leafBroadcast(org.dreeam.leaf.async.tracker.TrackerCtx ctx, Packet packet) { ++ for (ServerPlayerConnection serverPlayerConnection : this.seenBy()) { ++ ctx.send(serverPlayerConnection, packet); ++ } ++ } ++ ++ public void leafBroadcastIgnorePlayers(org.dreeam.leaf.async.tracker.TrackerCtx ctx, Packet packet, List ignoredPlayers) { ++ for (ServerPlayerConnection serverPlayerConnection : this.seenBy()) { ++ if (!ignoredPlayers.contains(serverPlayerConnection.getPlayer().getUUID())) { ++ ctx.send(serverPlayerConnection, packet); ++ } ++ } ++ } ++ ++ public void leafBroadcastAndSend(org.dreeam.leaf.async.tracker.TrackerCtx ctx, Packet packet) { ++ leafBroadcast(ctx, packet); ++ if (this.entity instanceof ServerPlayer serverPlayer) { ++ ctx.send(serverPlayer.connection, packet); ++ } ++ } ++ // Leaf end - Multithreaded tracker ++ + public void broadcastAndSend(Packet packet) { ++ // Leaf - Multithreaded tracker - diff on change + this.broadcast(packet); + if (this.entity instanceof ServerPlayer) { + ((ServerPlayer)this.entity).connection.send(packet); +@@ -1259,21 +1436,34 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } public void broadcastRemoved() { @@ -314,8 +480,8 @@ index 74d11e8983f12f6f33fe2eb3016730507e1031d4..f621cfea59b6d2f2fb29333e50860584 + //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); ++ this.seenByUpdated(); // Leaf - Multithreaded tracker } -+ this.seenByUpdated(); // Leaf - Multithreaded tracker } public void updatePlayer(ServerPlayer player) { @@ -326,7 +492,7 @@ index 74d11e8983f12f6f33fe2eb3016730507e1031d4..f621cfea59b6d2f2fb29333e50860584 // Paper start - remove allocation of Vec3D here // Vec3 vec3 = player.position().subtract(this.entity.position()); double vec3_dx = player.getX() - this.entity.getX(); -@@ -1301,6 +1435,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider +@@ -1301,6 +1491,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider // CraftBukkit end if (flag) { if (this.seenBy.add(player.connection)) { @@ -334,7 +500,7 @@ index 74d11e8983f12f6f33fe2eb3016730507e1031d4..f621cfea59b6d2f2fb29333e50860584 // Paper start - entity tracking events if (io.papermc.paper.event.player.PlayerTrackEntityEvent.getHandlerList().getRegisteredListeners().length == 0 || new io.papermc.paper.event.player.PlayerTrackEntityEvent(player.getBukkitEntity(), this.entity.getBukkitEntity()).callEvent()) { this.serverEntity.addPairing(player); -@@ -1309,6 +1444,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider +@@ -1309,11 +1500,60 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider this.serverEntity.onPlayerAdd(); // Paper - fix desync when a player is added to the tracker } } else if (this.seenBy.remove(player.connection)) { @@ -342,46 +508,693 @@ index 74d11e8983f12f6f33fe2eb3016730507e1031d4..f621cfea59b6d2f2fb29333e50860584 this.serverEntity.removePairing(player); } } + } + ++ // Leaf start - Multithreaded tracker ++ private boolean updatePlayerMulti(ServerPlayer player) { ++ //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 vec3 = player.position().subtract(this.entity.position()); ++ double vec3_dx = player.getX() - this.entity.getX(); ++ double vec3_dz = player.getZ() - this.entity.getZ(); ++ // Paper end - remove allocation of Vec3D here ++ int playerViewDistance = ChunkMap.this.getPlayerViewDistance(player); ++ double d = Math.min(this.getEffectiveRange(), playerViewDistance * 16); ++ double d1 = vec3_dx * vec3_dx + vec3_dz * vec3_dz; // Paper ++ double d2 = d * d; ++ // Paper start - Configurable entity tracking range by Y ++ boolean flag = d1 <= d2; ++ if (flag && level.paperConfig().entities.trackingRangeY.enabled) { ++ double rangeY = level.paperConfig().entities.trackingRangeY.get(this.entity, -1); ++ if (rangeY != -1) { ++ double vec3_dy = player.getY() - this.entity.getY(); ++ flag = vec3_dy * vec3_dy <= rangeY * rangeY; ++ } ++ } ++ flag = flag && this.entity.broadcastToPlayer(player) && ChunkMap.this.isChunkTracked(player, this.entity.chunkPosition().x, this.entity.chunkPosition().z); ++ // Paper end - Configurable entity tracking range by Y ++ // CraftBukkit start - respect vanish API ++ if (flag && !player.getBukkitEntity().canSeeChunkMapUpdatePlayer(this.entity.getBukkitEntity())) { // Paper - only consider hits // SparklyPaper - optimize canSee checks ++ flag = false; ++ } ++ // CraftBukkit end ++ if (flag) { ++ if (this.seenBy.add(player.connection)) { ++ // Paper start - entity tracking events ++ if (io.papermc.paper.event.player.PlayerTrackEntityEvent.getHandlerList().getRegisteredListeners().length == 0 || new io.papermc.paper.event.player.PlayerTrackEntityEvent(player.getBukkitEntity(), this.entity.getBukkitEntity()).callEvent()) { ++ this.serverEntity.addPairing(player); ++ } ++ // Paper end - entity tracking events ++ this.serverEntity.onPlayerAdd(); // Paper - fix desync when a player is added to the tracker ++ return true; ++ } ++ } else if (this.seenBy.remove(player.connection)) { ++ this.serverEntity.removePairing(player); ++ return true; ++ } ++ } ++ return false; ++ } ++ // Leaf end - Multithreaded tracker ++ + private int scaledRange(int trackingDistance) { + return ChunkMap.this.level.getServer().getScaledTrackingDistance(trackingDistance); + } +@@ -1339,10 +1579,14 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + // Paper end - optimise entity tracker + } + ++ // Leaf start - Multithreaded tracker + public void updatePlayers(List playersList) { ++ boolean updated = false; + for (ServerPlayer serverPlayer : playersList) { +- this.updatePlayer(serverPlayer); ++ updated |= this.updatePlayerMulti(serverPlayer); + } ++ if (updated) seenByUpdated(); + } ++ // Leaf end - Multithreaded tracker + } + } diff --git a/net/minecraft/server/level/ServerBossEvent.java b/net/minecraft/server/level/ServerBossEvent.java -index f106373ef3ac4a8685c2939c9e8361688a285913..51ae390c68e7a3aa193329cc3bc47ca675930ff2 100644 +index f106373ef3ac4a8685c2939c9e8361688a285913..7df467924d029d6e42b1d0b039cb9272bb068c4d 100644 --- a/net/minecraft/server/level/ServerBossEvent.java +++ b/net/minecraft/server/level/ServerBossEvent.java -@@ -13,7 +13,7 @@ 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; +@@ -105,6 +105,20 @@ public class ServerBossEvent extends BossEvent { + } + } ++ // Leaf start - Multithreaded tracker ++ public void leafAddPlayer(org.dreeam.leaf.async.tracker.TrackerCtx ctx, ServerPlayer player) { ++ if (this.players.add(player) && this.visible) { ++ ctx.send(player.connection, ClientboundBossEventPacket.createAddPacket(this)); ++ } ++ } ++ ++ public void leafRemovePlayer(org.dreeam.leaf.async.tracker.TrackerCtx ctx, ServerPlayer player) { ++ if (this.players.remove(player) && this.visible) { ++ ctx.send(player.connection, ClientboundBossEventPacket.createRemovePacket(this.getId())); ++ } ++ } ++ // Leaf end - Multithreaded tracker ++ + public void removeAllPlayers() { + if (!this.players.isEmpty()) { + for (ServerPlayer serverPlayer : Lists.newArrayList(this.players)) { diff --git a/net/minecraft/server/level/ServerEntity.java b/net/minecraft/server/level/ServerEntity.java -index add696ec1835eb161d6fc94509a2a77febd23d69..2b55ec55b0a5dacc0c658ae5359040ff529418c6 100644 +index add696ec1835eb161d6fc94509a2a77febd23d69..ca948c0f4f48b567b66e4b992eef99d3a9210d45 100644 --- a/net/minecraft/server/level/ServerEntity.java +++ b/net/minecraft/server/level/ServerEntity.java -@@ -146,7 +146,7 @@ public class ServerEntity { +@@ -57,11 +57,13 @@ public class ServerEntity { + public static final int FORCED_POS_UPDATE_PERIOD = 60; + private static final int FORCED_TELEPORT_PERIOD = 400; + private final ServerLevel level; ++ // Leaf - Multithreaded tracker - diff on change + private final Entity entity; + private final int updateInterval; + private final boolean trackDelta; + private final Consumer> broadcast; + private final BiConsumer, List> broadcastWithIgnore; ++ // Leaf - Multithreaded tracker - diff on change + private final VecDeltaCodec positionCodec = new VecDeltaCodec(); + private byte lastSentYRot; + private byte lastSentXRot; +@@ -94,6 +96,7 @@ public class ServerEntity { + this.updateInterval = updateInterval; + this.trackDelta = trackDelta; + this.broadcastWithIgnore = broadcastWithIgnore; ++ // Leaf - Multithreaded tracker - diff on change + this.positionCodec.setBase(entity.trackingPosition()); + this.lastSentMovement = entity.getDeltaMovement(); + this.lastSentYRot = Mth.packDegrees(entity.getYRot()); +@@ -105,204 +108,413 @@ public class ServerEntity { + + // Paper start - fix desync when a player is added to the tracker + private boolean forceStateResync; ++ // Leaf - Multithreaded tracker - diff on change + public void onPlayerAdd() { + this.forceStateResync = true; + } + // Paper end - fix desync when a player is added to the tracker + ++ // Leaf - Multithreaded tracker - diff on change + public void sendChanges() { + // Paper start - optimise collisions + if (((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity)this.entity).moonrise$isHardColliding()) { + this.teleportDelay = 9999; + } ++ // Leaf - Multithreaded tracker - diff on change + // Paper end - optimise collisions + List passengers = this.entity.getPassengers(); ++ // Leaf - Multithreaded tracker - diff on change + if (!passengers.equals(this.lastPassengers)) { + // Leaf start - Remove stream in entity mountedOrDismounted changes update + List list = new ArrayList<>(); + for (Entity entity : this.lastPassengers) { ++ // Leaf - Multithreaded tracker - diff on change + if (!passengers.contains(entity)) { + list.add(entity.getUUID()); + } + } ++ // Leaf - Multithreaded tracker - diff on change + for (Entity entity : passengers) { + if (!this.lastPassengers.contains(entity)) { + list.add(entity.getUUID()); + } ++ // Leaf - Multithreaded tracker - diff on change + } + // Leaf end - Remove stream in entity mountedOrDismounted changes update + this.broadcastWithIgnore.accept(new ClientboundSetPassengersPacket(this.entity), list); + // Paper start - Allow riding players ++ // Leaf - Multithreaded tracker - diff on change + if (this.entity instanceof ServerPlayer player) { + player.connection.send(new ClientboundSetPassengersPacket(this.entity)); + } + // Paper end - Allow riding players + this.lastPassengers = passengers; ++ // Leaf - Multithreaded tracker - diff on change + } + ++ // Leaf - Multithreaded tracker - diff on change + if (!this.trackedPlayers.isEmpty() && this.entity instanceof ItemFrame itemFrame /*&& this.tickCount % 10 == 0*/) { // CraftBukkit - moved tickCount below // Paper - Perf: Only tick item frames if players can see it + ItemStack item = itemFrame.getItem(); + if (this.level.paperConfig().maps.itemFrameCursorUpdateInterval > 0 && this.tickCount % this.level.paperConfig().maps.itemFrameCursorUpdateInterval == 0 && item.getItem() instanceof MapItem) { // CraftBukkit - Moved this.tickCounter % 10 logic here so item frames do not enter the other blocks // Paper - Make item frame map cursor update interval configurable MapId mapId = itemFrame.cachedMapId; // Paper - Perf: Cache map ids on item frames MapItemSavedData savedData = MapItem.getSavedData(mapId, this.level); ++ // Leaf - Multithreaded tracker - diff on change if (savedData != null) { -- for (final net.minecraft.server.network.ServerPlayerConnection connection : this.trackedPlayers) { // Paper -+ for (final net.minecraft.server.network.ServerPlayerConnection connection : this.trackedPlayers.toArray(ChunkMap.TrackedEntity.EMPTY_OBJECT_ARRAY)) { // Paper // Leaf - Multithreaded tracker + for (final net.minecraft.server.network.ServerPlayerConnection connection : this.trackedPlayers) { // Paper final ServerPlayer serverPlayer = connection.getPlayer(); // Paper savedData.tickCarriedBy(serverPlayer, item); Packet updatePacket = savedData.getUpdatePacket(mapId, serverPlayer); -@@ -462,7 +462,7 @@ public class ServerEntity { ++ // Leaf - Multithreaded tracker - diff on change + if (updatePacket != null) { + serverPlayer.connection.send(updatePacket); + } + } + } ++ // Leaf - Multithreaded tracker - diff on change + } + ++ // Leaf - Multithreaded tracker - diff on change + this.sendDirtyEntityData(); + } + ++ // Leaf - Multithreaded tracker - diff on change + if (this.forceStateResync || this.tickCount % this.updateInterval == 0 || this.entity.hasImpulse || this.entity.getEntityData().isDirty()) { // Paper - fix desync when a player is added to the tracker + byte b = Mth.packDegrees(this.entity.getYRot()); + byte b1 = Mth.packDegrees(this.entity.getXRot()); + boolean flag = Math.abs(b - this.lastSentYRot) >= 1 || Math.abs(b1 - this.lastSentXRot) >= 1; ++ // Leaf - Multithreaded tracker - diff on change + if (this.entity.isPassenger()) { + if (flag) { ++ // Leaf - Multithreaded tracker - diff on change + this.broadcast.accept(new ClientboundMoveEntityPacket.Rot(this.entity.getId(), b, b1, this.entity.onGround())); + this.lastSentYRot = b; + this.lastSentXRot = b1; + } ++ // Leaf - Multithreaded tracker - diff on change + ++ // Leaf - Multithreaded tracker - diff on change + this.positionCodec.setBase(this.entity.trackingPosition()); + this.sendDirtyEntityData(); + this.wasRiding = true; + } else if (this.entity instanceof AbstractMinecart abstractMinecart + && abstractMinecart.getBehavior() instanceof NewMinecartBehavior newMinecartBehavior) { ++ // Leaf - Multithreaded tracker - diff on change + this.handleMinecartPosRot(newMinecartBehavior, b, b1, flag); + } else { + this.teleportDelay++; + Vec3 vec3 = this.entity.trackingPosition(); ++ // Leaf - Multithreaded tracker - diff on change + // Paper start - reduce allocation of Vec3D here + Vec3 base = this.positionCodec.base; + double vec3_dx = vec3.x - base.x; + double vec3_dy = vec3.y - base.y; + double vec3_dz = vec3.z - base.z; ++ // Leaf - Multithreaded tracker - diff on change + boolean flag1 = (vec3_dx * vec3_dx + vec3_dy * vec3_dy + vec3_dz * vec3_dz) >= 7.62939453125E-6D; + // Paper end - reduce allocation of Vec3D here + Packet packet = null; ++ // Leaf - Multithreaded tracker - diff on change + boolean flag2 = flag1 || this.tickCount % 60 == 0; + boolean flag3 = false; + boolean flag4 = false; + long l = this.positionCodec.encodeX(vec3); + long l1 = this.positionCodec.encodeY(vec3); + long l2 = this.positionCodec.encodeZ(vec3); ++ // Leaf - Multithreaded tracker - diff on change + boolean flag5 = l < -32768L || l > 32767L || l1 < -32768L || l1 > 32767L || l2 < -32768L || l2 > 32767L; + if (this.forceStateResync || this.entity.getRequiresPrecisePosition() // Paper - fix desync when a player is added to the tracker + || flag5 + || this.teleportDelay > 400 + || this.wasRiding + || this.wasOnGround != this.entity.onGround()) { ++ // Leaf - Multithreaded tracker - diff on change + this.wasOnGround = this.entity.onGround(); + this.teleportDelay = 0; + packet = ClientboundEntityPositionSyncPacket.of(this.entity); + flag3 = true; + flag4 = true; ++ // Leaf - Multithreaded tracker - diff on change + // Gale start - Airplane - better checking for useless move packets + } else { ++ // Leaf - Multithreaded tracker - diff on change + if (flag2 || flag || this.entity instanceof AbstractArrow) { + if ((!flag2 || !flag) && !(this.entity instanceof AbstractArrow)) { ++ // Leaf - Multithreaded tracker - diff on change + if (flag2) { + packet = new ClientboundMoveEntityPacket.Pos(this.entity.getId(), (short) l, (short) l1, (short) l2, this.entity.onGround()); + flag3 = true; + } else if (flag) { ++ // Leaf - Multithreaded tracker - diff on change + packet = new ClientboundMoveEntityPacket.Rot(this.entity.getId(), b, b1, this.entity.onGround()); + flag4 = true; + } + } else { ++ // Leaf - Multithreaded tracker - diff on change + packet = new ClientboundMoveEntityPacket.PosRot(this.entity.getId(), (short) l, (short) l1, (short) l2, b, b1, this.entity.onGround()); + flag3 = true; + flag4 = true; + } + } + } ++ // Leaf - Multithreaded tracker - diff on change + // Gale end - Airplane - better checking for useless move packets + + if (org.dreeam.leaf.config.modules.opt.ReduceUselessPackets.reduceUselessEntityMovePackets && isUselessMoveEntityPacket(packet)) packet = null; // Purpur + ++ // Leaf - Multithreaded tracker - diff on change + if (this.entity.hasImpulse || this.trackDelta || this.entity instanceof LivingEntity && ((LivingEntity)this.entity).isFallFlying()) { + Vec3 deltaMovement = this.entity.getDeltaMovement(); + if (deltaMovement != this.lastSentMovement) { // SparklyPaper start - skip distanceToSqr call in ServerEntity#sendChanges if the delta movement hasn't changed + double d = deltaMovement.distanceToSqr(this.lastSentMovement); + if (d > 1.0E-7 || d > 0.0 && deltaMovement.lengthSqr() == 0.0) { + this.lastSentMovement = deltaMovement; ++ // Leaf - Multithreaded tracker - diff on change + if (this.entity instanceof AbstractHurtingProjectile abstractHurtingProjectile) { ++ // Leaf - Multithreaded tracker - diff on change + this.broadcast + .accept( + new ClientboundBundlePacket( + List.of( ++ // Leaf - Multithreaded tracker - diff on change + new ClientboundSetEntityMotionPacket(this.entity.getId(), this.lastSentMovement), + new ClientboundProjectilePowerPacket(abstractHurtingProjectile.getId(), abstractHurtingProjectile.accelerationPower) + ) + ) + ); ++ // Leaf - Multithreaded tracker - diff on change + } else { ++ // Leaf - Multithreaded tracker - diff on change + this.broadcast.accept(new ClientboundSetEntityMotionPacket(this.entity.getId(), this.lastSentMovement)); + } + } + } // SparklyPaper end + } + ++ // Leaf - Multithreaded tracker - diff on change + if (packet != null) { + this.broadcast.accept(packet); + } + + this.sendDirtyEntityData(); ++ // Leaf - Multithreaded tracker - diff on change + if (flag3) { + this.positionCodec.setBase(vec3); + } + + if (flag4) { ++ // Leaf - Multithreaded tracker - diff on change + this.lastSentYRot = b; + this.lastSentXRot = b1; + } + ++ // Leaf - Multithreaded tracker - diff on change + this.wasRiding = false; + } + ++ // Leaf - Multithreaded tracker - diff on change + byte b2 = Mth.packDegrees(this.entity.getYHeadRot()); + if (Math.abs(b2 - this.lastSentYHeadRot) >= 1) { + this.broadcast.accept(new ClientboundRotateHeadPacket(this.entity, b2)); + this.lastSentYHeadRot = b2; + } + ++ // Leaf - Multithreaded tracker - diff on change + this.entity.hasImpulse = false; + this.forceStateResync = false; // Paper - fix desync when a player is added to the tracker + } + ++ // Leaf - Multithreaded tracker - diff on change + this.tickCount++; + if (this.entity.hurtMarked) { + // CraftBukkit start - Create PlayerVelocity event + boolean cancelled = false; + ++ // Leaf - Multithreaded tracker - diff on change + if (this.entity instanceof ServerPlayer) { + org.bukkit.entity.Player player = (org.bukkit.entity.Player) this.entity.getBukkitEntity(); + org.bukkit.util.Vector velocity = player.getVelocity(); + ++ // Leaf - Multithreaded tracker - diff on change + org.bukkit.event.player.PlayerVelocityEvent event = new org.bukkit.event.player.PlayerVelocityEvent(player, velocity.clone()); + if (!event.callEvent()) { + cancelled = true; ++ // Leaf - Multithreaded tracker - diff on change + } else if (!velocity.equals(event.getVelocity())) { + player.setVelocity(event.getVelocity()); + } + } ++ // Leaf - Multithreaded tracker - diff on change + + if (cancelled) { + return; + } ++ // Leaf - Multithreaded tracker - diff on change + // CraftBukkit end + this.entity.hurtMarked = false; + this.broadcastAndSend(new ClientboundSetEntityMotionPacket(this.entity)); ++ // Leaf - Multithreaded tracker - diff on change + } + } + ++ // Leaf start - Multithreaded tracker ++ public void leafSendChanges(org.dreeam.leaf.async.tracker.TrackerCtx ctx, ChunkMap.TrackedEntity trackedEntity) { ++ // Paper start - optimise collisions ++ if (this.entity.moonrise$isHardColliding()) { ++ this.teleportDelay = 9999; ++ } ++ // Paper end - optimise collisions ++ List passengers = this.entity.getPassengers(); ++ if (!passengers.equals(this.lastPassengers)) { ++ // Leaf start - Remove stream in entity mountedOrDismounted changes update ++ List list = new ArrayList<>(); ++ for (Entity entity : this.lastPassengers) { ++ if (!passengers.contains(entity)) { ++ list.add(entity.getUUID()); ++ } ++ } ++ for (Entity entity : passengers) { ++ if (!this.lastPassengers.contains(entity)) { ++ list.add(entity.getUUID()); ++ } ++ } ++ // Leaf end - Remove stream in entity mountedOrDismounted changes update ++ trackedEntity.leafBroadcastIgnorePlayers(ctx, new ClientboundSetPassengersPacket(this.entity), list); ++ // Paper start - Allow riding players ++ if (this.entity instanceof ServerPlayer player) { ++ ctx.send(player.connection, new ClientboundSetPassengersPacket(this.entity)); ++ } ++ // Paper end - Allow riding players ++ this.lastPassengers = passengers; ++ } ++ if (!trackedEntity.seenBy.isEmpty() && this.entity instanceof ItemFrame itemFrame /*&& this.tickCount % 10 == 0*/) { // CraftBukkit - moved tickCount below // Paper - Perf: Only tick item frames if players can see it ++ if (this.level.paperConfig().maps.itemFrameCursorUpdateInterval > 0 && this.tickCount % this.level.paperConfig().maps.itemFrameCursorUpdateInterval == 0 && itemFrame.getItem().getItem() instanceof MapItem) { // CraftBukkit - Moved this.tickCounter % 10 logic here so item frames do not enter the other blocks // Paper - Make item frame map cursor update interval configurable ++ ctx.updateItemFrame(itemFrame); ++ } ++ this.leafSendDirtyEntityData(ctx, trackedEntity); ++ } ++ ++ if (this.forceStateResync || this.tickCount % this.updateInterval == 0 || this.entity.hasImpulse || this.entity.getEntityData().isDirty()) { // Paper - fix desync when a player is added to the tracker ++ byte b = Mth.packDegrees(this.entity.getYRot()); ++ byte b1 = Mth.packDegrees(this.entity.getXRot()); ++ boolean flag = Math.abs(b - this.lastSentYRot) >= 1 || Math.abs(b1 - this.lastSentXRot) >= 1; ++ if (this.entity.isPassenger()) { ++ if (flag) { ++ trackedEntity.leafBroadcast(ctx, new ClientboundMoveEntityPacket.Rot(this.entity.getId(), b, b1, this.entity.onGround())); ++ this.lastSentYRot = b; ++ this.lastSentXRot = b1; ++ } ++ ++ this.positionCodec.setBase(this.entity.trackingPosition()); ++ this.leafSendDirtyEntityData(ctx, trackedEntity); ++ this.wasRiding = true; ++ } else if (this.entity instanceof AbstractMinecart abstractMinecart ++ && abstractMinecart.getBehavior() instanceof NewMinecartBehavior newMinecartBehavior) { ++ this.leafHandleMinecartPosRot(ctx, trackedEntity, newMinecartBehavior, b, b1, flag); ++ } else { ++ this.teleportDelay++; ++ Vec3 vec3 = this.entity.trackingPosition(); ++ // Paper start - reduce allocation of Vec3D here ++ Vec3 base = this.positionCodec.base; ++ double vec3_dx = vec3.x - base.x; ++ double vec3_dy = vec3.y - base.y; ++ double vec3_dz = vec3.z - base.z; ++ boolean flag1 = (vec3_dx * vec3_dx + vec3_dy * vec3_dy + vec3_dz * vec3_dz) >= 7.62939453125E-6D; ++ // Paper end - reduce allocation of Vec3D here ++ Packet packet = null; ++ boolean flag2 = flag1 || this.tickCount % 60 == 0; ++ boolean flag3 = false; ++ boolean flag4 = false; ++ long l = this.positionCodec.encodeX(vec3); ++ long l1 = this.positionCodec.encodeY(vec3); ++ long l2 = this.positionCodec.encodeZ(vec3); ++ boolean flag5 = l < -32768L || l > 32767L || l1 < -32768L || l1 > 32767L || l2 < -32768L || l2 > 32767L; ++ if (this.forceStateResync || this.entity.getRequiresPrecisePosition() // Paper - fix desync when a player is added to the tracker ++ || flag5 ++ || this.teleportDelay > 400 ++ || this.wasRiding ++ || this.wasOnGround != this.entity.onGround()) { ++ this.wasOnGround = this.entity.onGround(); ++ this.teleportDelay = 0; ++ packet = ClientboundEntityPositionSyncPacket.of(this.entity); ++ flag3 = true; ++ flag4 = true; ++ // Gale start - Airplane - better checking for useless move packets ++ } else { ++ if (flag2 || flag || this.entity instanceof AbstractArrow) { ++ if ((!flag2 || !flag) && !(this.entity instanceof AbstractArrow)) { ++ if (flag2) { ++ packet = new ClientboundMoveEntityPacket.Pos(this.entity.getId(), (short) l, (short) l1, (short) l2, this.entity.onGround()); ++ flag3 = true; ++ } else if (flag) { ++ packet = new ClientboundMoveEntityPacket.Rot(this.entity.getId(), b, b1, this.entity.onGround()); ++ flag4 = true; ++ } ++ } else { ++ packet = new ClientboundMoveEntityPacket.PosRot(this.entity.getId(), (short) l, (short) l1, (short) l2, b, b1, this.entity.onGround()); ++ flag3 = true; ++ flag4 = true; ++ } ++ } ++ } ++ // Gale end - Airplane - better checking for useless move packets ++ ++ if (org.dreeam.leaf.config.modules.opt.ReduceUselessPackets.reduceUselessEntityMovePackets && isUselessMoveEntityPacket(packet)) packet = null; // Purpur ++ ++ if (this.entity.hasImpulse || this.trackDelta || this.entity instanceof LivingEntity && ((LivingEntity)this.entity).isFallFlying()) { ++ Vec3 deltaMovement = this.entity.getDeltaMovement(); ++ if (deltaMovement != this.lastSentMovement) { // SparklyPaper start - skip distanceToSqr call in ServerEntity#sendChanges if the delta movement hasn't changed ++ double d = deltaMovement.distanceToSqr(this.lastSentMovement); ++ if (d > 1.0E-7 || d > 0.0 && deltaMovement.lengthSqr() == 0.0) { ++ this.lastSentMovement = deltaMovement; ++ if (this.entity instanceof AbstractHurtingProjectile abstractHurtingProjectile) { ++ trackedEntity.leafBroadcast(ctx, new ClientboundSetEntityMotionPacket(this.entity.getId(), this.lastSentMovement)); ++ trackedEntity.leafBroadcast(ctx, new ClientboundProjectilePowerPacket(abstractHurtingProjectile.getId(), abstractHurtingProjectile.accelerationPower)); ++ } else { ++ trackedEntity.leafBroadcast(ctx, new ClientboundSetEntityMotionPacket(this.entity.getId(), this.lastSentMovement)); ++ } ++ } ++ } // SparklyPaper end ++ } ++ ++ if (packet != null) { ++ trackedEntity.leafBroadcast(ctx, packet); ++ } ++ ++ this.leafSendDirtyEntityData(ctx, trackedEntity); ++ if (flag3) { ++ this.positionCodec.setBase(vec3); ++ } ++ ++ if (flag4) { ++ this.lastSentYRot = b; ++ this.lastSentXRot = b1; ++ } ++ ++ this.wasRiding = false; ++ } ++ ++ byte b2 = Mth.packDegrees(this.entity.getYHeadRot()); ++ if (Math.abs(b2 - this.lastSentYHeadRot) >= 1) { ++ trackedEntity.leafBroadcast(ctx, new ClientboundRotateHeadPacket(this.entity, b2)); ++ this.lastSentYHeadRot = b2; ++ } ++ ++ this.entity.hasImpulse = false; ++ this.forceStateResync = false; // Paper - fix desync when a player is added to the tracker ++ } ++ ++ this.tickCount++; ++ if (this.entity.hurtMarked) { ++ if (this.entity instanceof ServerPlayer serverPlayer) { ++ ctx.playerVelocity(serverPlayer); ++ } else { ++ this.entity.hurtMarked = false; ++ trackedEntity.leafBroadcastAndSend(ctx, new ClientboundSetEntityMotionPacket(this.entity)); ++ } ++ } ++ } ++ // Leaf end - Multithreaded tracker ++ + private Stream mountedOrDismounted(List entities) { + return Streams.concat( + this.lastPassengers.stream().filter(entity -> !entities.contains(entity)), +@@ -356,6 +568,39 @@ public class ServerEntity { + this.positionCodec.setBase(this.entity.position()); + } + ++ // Leaf start - Multithreaded tracker ++ private void leafHandleMinecartPosRot(org.dreeam.leaf.async.tracker.TrackerCtx ctx, ChunkMap.TrackedEntity trackedEntity, NewMinecartBehavior behavior, byte yRot, byte xRot, boolean dirty) { ++ this.leafSendDirtyEntityData(ctx, trackedEntity); ++ if (behavior.lerpSteps.isEmpty()) { ++ Vec3 deltaMovement = this.entity.getDeltaMovement(); ++ double d = deltaMovement.distanceToSqr(this.lastSentMovement); ++ Vec3 vec3 = this.entity.trackingPosition(); ++ boolean flag = this.positionCodec.delta(vec3).lengthSqr() >= 7.6293945E-6F; ++ boolean flag1 = flag || this.tickCount % 60 == 0; ++ if (flag1 || dirty || d > 1.0E-7) { ++ trackedEntity ++ .leafBroadcast(ctx, ++ new ClientboundMoveMinecartPacket( ++ this.entity.getId(), ++ List.of( ++ new NewMinecartBehavior.MinecartStep( ++ this.entity.position(), this.entity.getDeltaMovement(), this.entity.getYRot(), this.entity.getXRot(), 1.0F ++ ) ++ ) ++ ) ++ ); ++ } ++ } else { ++ trackedEntity.leafBroadcast(ctx, new ClientboundMoveMinecartPacket(this.entity.getId(), List.copyOf(behavior.lerpSteps))); ++ behavior.lerpSteps.clear(); ++ } ++ ++ this.lastSentYRot = yRot; ++ this.lastSentXRot = xRot; ++ this.positionCodec.setBase(this.entity.position()); ++ } ++ // Leaf end - Multithreaded tracker ++ + public void removePairing(ServerPlayer player) { + this.entity.stopSeenByPlayer(player); + player.connection.send(new ClientboundRemoveEntitiesPacket(this.entity.getId())); +@@ -368,8 +613,21 @@ public class ServerEntity { + this.entity.startSeenByPlayer(player); + } + ++ // Leaf start - Multithreaded tracker ++ public void leafAddPairing(org.dreeam.leaf.async.tracker.TrackerCtx ctx, ServerPlayer player) { ++ final net.minecraft.server.network.ServerGamePacketListenerImpl connection = player.connection; ++ this.sendPairingData0(player, p -> ctx.send(connection, p), true); ++ ctx.startSeenByPlayer(connection, this.entity); ++ } ++ + public void sendPairingData(ServerPlayer player, Consumer> consumer) { ++ sendPairingData0(player, consumer, false); ++ } ++ ++ private void sendPairingData0(ServerPlayer player, Consumer> consumer, boolean async) { + if (this.entity.isRemoved()) { ++ // Leaf end - Multithreaded tracker ++ + // CraftBukkit start - Remove useless error spam, just return + // LOGGER.warn("Fetching packet for removed entity {}", this.entity); + return; +@@ -407,7 +665,7 @@ public class ServerEntity { + if (!list.isEmpty()) { + consumer.accept(new ClientboundSetEquipmentPacket(this.entity.getId(), list, true)); // Paper - data sanitization + } +- ((LivingEntity) this.entity).detectEquipmentUpdates(); // CraftBukkit - SPIGOT-3789: sync again immediately after sending ++ if (!async) { ((LivingEntity) this.entity).detectEquipmentUpdates(); } // CraftBukkit - SPIGOT-3789: sync again immediately after sending // Leaf - Multithreaded tracker + } + + if (!this.entity.getPassengers().isEmpty()) { +@@ -443,6 +701,7 @@ public class ServerEntity { + return Mth.unpackDegrees(this.lastSentYHeadRot); + } + ++ // Leaf - Multithreaded tracker - diff on change + public void sendDirtyEntityData() { + SynchedEntityData entityData = this.entity.getEntityData(); + List> list = entityData.packDirty(); +@@ -450,10 +709,12 @@ public class ServerEntity { + this.trackedDataValues = entityData.getNonDefaultValues(); + this.broadcastAndSend(new ClientboundSetEntityDataPacket(this.entity.getId(), list)); + } ++ // Leaf - Multithreaded tracker - diff on change + + if (this.entity instanceof LivingEntity) { + Set attributesToSync = ((LivingEntity)this.entity).getAttributes().getAttributesToSync(); + if (!attributesToSync.isEmpty()) { ++ // Leaf - Multithreaded tracker - diff on change + // CraftBukkit start - Send scaled max health + if (this.entity instanceof ServerPlayer serverPlayer) { + serverPlayer.getBukkitEntity().injectScaledMaxHealth(attributesToSync, false); +@@ -462,11 +723,38 @@ public class ServerEntity { this.broadcastAndSend(new ClientboundUpdateAttributesPacket(this.entity.getId(), attributesToSync)); } - attributesToSync.clear(); ++ // attributesToSync.clear(); // Leaf - Multithreaded tracker ++ } ++ } ++ ++ // Leaf start - Multithreaded tracker ++ public void leafSendDirtyEntityData(org.dreeam.leaf.async.tracker.TrackerCtx ctx, ChunkMap.TrackedEntity trackedEntity) { ++ SynchedEntityData entityData = this.entity.getEntityData(); ++ List> list = entityData.packDirty(); ++ if (list != null) { ++ this.trackedDataValues = entityData.getNonDefaultValues(); ++ ClientboundSetEntityDataPacket packet = new ClientboundSetEntityDataPacket(this.entity.getId(), list); ++ trackedEntity.leafBroadcastAndSend(ctx, packet); ++ } ++ ++ if (this.entity instanceof LivingEntity) { ++ Set attributesToSync = ((LivingEntity)this.entity).getAttributes().getAttributesToSync(); ++ if (!attributesToSync.isEmpty()) { ++ // CraftBukkit start - Send scaled max health ++ if (this.entity instanceof ServerPlayer serverPlayer) { ++ serverPlayer.getBukkitEntity().injectScaledMaxHealth(attributesToSync, false); ++ } ++ // CraftBukkit end ++ trackedEntity.leafBroadcastAndSend(ctx, new ClientboundUpdateAttributesPacket(this.entity.getId(), attributesToSync)); ++ } ++ + // attributesToSync.clear(); // Leaf - Multithreaded tracker } } ++ // Leaf end - Multithreaded tracker + private void broadcastAndSend(Packet packet) { ++ // Leaf - Multithreaded tracker - diff on change + this.broadcast.accept(packet); + if (this.entity instanceof ServerPlayer) { + ((ServerPlayer)this.entity).connection.send(packet); diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java -index 08d12a1acc3a672a77daa15f82392cd603c30283..c5949a0e852ca6de84e8dd12e3d4ed8527b60e25 100644 +index 08d12a1acc3a672a77daa15f82392cd603c30283..b4ee1682a851eb8f1fae999ba0913ae300a71a89 100644 --- a/net/minecraft/server/level/ServerLevel.java +++ b/net/minecraft/server/level/ServerLevel.java -@@ -2527,7 +2527,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -216,6 +216,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + private final alternate.current.wire.WireHandler wireHandler = new alternate.current.wire.WireHandler(this); // Paper - optimize redstone (Alternate Current) + public boolean hasRidableMoveEvent = false; // Purpur - Ridables + final List realPlayers; // Leaves - skip ++ public @Nullable java.util.concurrent.Future[] trackerTask; // Leaf - Multithreaded tracker + + @Override + public @Nullable LevelChunk getChunkIfLoaded(int x, int z) { +@@ -834,6 +835,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + this.tickBlockEntities(); + } + // Paper - rewrite chunk system ++ if (org.dreeam.leaf.config.modules.async.MultithreadedTracker.enabled) { org.dreeam.leaf.async.tracker.AsyncTracker.onEntitiesTickEnd(this); } // Leaf - Multithreaded tracker + } + + @Override +@@ -2527,7 +2529,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe @Override public LevelEntityGetter getEntities() { @@ -390,7 +1203,7 @@ index 08d12a1acc3a672a77daa15f82392cd603c30283..c5949a0e852ca6de84e8dd12e3d4ed85 return this.moonrise$getEntityLookup(); // Paper - rewrite chunk system } -@@ -2799,7 +2799,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -2799,7 +2801,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe } map.carriedByPlayers.remove(player); @@ -400,7 +1213,7 @@ index 08d12a1acc3a672a77daa15f82392cd603c30283..c5949a0e852ca6de84e8dd12e3d4ed85 } } diff --git a/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/net/minecraft/server/network/ServerGamePacketListenerImpl.java -index 3f8b0c9f0d2172b1ffaee6c1065a91ff34b35953..608324d13514063b48ae250d3aaf11c433438ce6 100644 +index f98617c5d55876ac91377f3014d6f0438305d7b3..25871b2db4888d8729f2dd64fdac16dc6eecc5e9 100644 --- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java +++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java @@ -1928,7 +1928,7 @@ public class ServerGamePacketListenerImpl @@ -413,7 +1226,7 @@ index 3f8b0c9f0d2172b1ffaee6c1065a91ff34b35953..608324d13514063b48ae250d3aaf11c4 if (this.player.isRemoved()) { LOGGER.info("Attempt to teleport removed player {} restricted", player.getScoreboardName()); diff --git a/net/minecraft/world/entity/LivingEntity.java b/net/minecraft/world/entity/LivingEntity.java -index d81d1303fc3d23b35dbc177dd6a4c7f489eb5381..78c212fb8a8a8ffefe6fc860f1e06d16ba09bb40 100644 +index f63d0af0f6dc8a634f56f329d256affeaeac17da..80d80bc0c6c22ceda190fd238ca63875d47fc599 100644 --- a/net/minecraft/world/entity/LivingEntity.java +++ b/net/minecraft/world/entity/LivingEntity.java @@ -1342,13 +1342,13 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin @@ -436,7 +1249,7 @@ index d81d1303fc3d23b35dbc177dd6a4c7f489eb5381..78c212fb8a8a8ffefe6fc860f1e06d16 protected void onAttributeUpdated(Holder attribute) { diff --git a/net/minecraft/world/entity/ai/attributes/Attribute.java b/net/minecraft/world/entity/ai/attributes/Attribute.java -index f8419dde44ebc7324e783f8bee42132d5ec973c3..406767c60ec1a324faaf5d3658b161647497f99b 100644 +index f8419dde44ebc7324e783f8bee42132d5ec973c3..d7eef96e12c738cd06a8e16efe48b7673f2a8608 100644 --- a/net/minecraft/world/entity/ai/attributes/Attribute.java +++ b/net/minecraft/world/entity/ai/attributes/Attribute.java @@ -16,10 +16,15 @@ public class Attribute { @@ -445,13 +1258,13 @@ index f8419dde44ebc7324e783f8bee42132d5ec973c3..406767c60ec1a324faaf5d3658b16164 private Attribute.Sentiment sentiment = Attribute.Sentiment.POSITIVE; + // Leaf start - Optimize AttributeMap + public final int uid; -+ private static final java.util.concurrent.atomic.AtomicInteger SIZE = new java.util.concurrent.atomic.AtomicInteger(); ++ private static int SIZE = 0; + // Leaf end - Optimize AttributeMap protected Attribute(String descriptionId, double defaultValue) { this.defaultValue = defaultValue; this.descriptionId = descriptionId; -+ this.uid = SIZE.getAndAdd(1); // Leaf - Optimize AttributeMap ++ this.uid = SIZE++; // Leaf - Optimize AttributeMap } public double getDefaultValue() { @@ -657,15 +1470,3 @@ index 325ec57df2885f5e81b8a6b61e3a9fed9484b30f..abc5c097861d0decf49d0d3970ab48f1 } @Override -diff --git a/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java b/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java -index bf01c9d54248ceb8f97cf1e1c0e4234a338cb8ce..d049af4f129f6ac2d53f10c7a811c989d1f3edc0 100644 ---- a/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java -+++ b/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java -@@ -211,6 +211,7 @@ public class MapItemSavedData extends SavedData { - - for (int i = 0; i < this.carriedBy.size(); i++) { - MapItemSavedData.HoldingPlayer holdingPlayer1 = this.carriedBy.get(i); -+ if (holdingPlayer1 == null) continue; // Leaf - Multithreaded tracker - Player player1 = holdingPlayer1.player; - String string = player1.getName().getString(); - if (!player1.isRemoved() && (player1.getInventory().contains(predicate) || mapStack.isFramed())) { diff --git a/leaf-server/minecraft-patches/features/0186-Cache-chunk-key.patch b/leaf-server/minecraft-patches/features/0186-Cache-chunk-key.patch index f422f348..e48bbe09 100644 --- a/leaf-server/minecraft-patches/features/0186-Cache-chunk-key.patch +++ b/leaf-server/minecraft-patches/features/0186-Cache-chunk-key.patch @@ -9,7 +9,7 @@ This patch didn't cahce SectionPos or BlockPos to chunkKey, since it needs to co TODO: Cache block pos and section pos, whether need? diff --git a/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java b/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java -index 4200d22606c6a3dbdf282792a4007a51df66963b..4b258f048c73107d0d050a9aa4b4a39788145b17 100644 +index 2a2626e90836ae52a8a686b2040843c6644b6914..283e02b32cae0bc1b0cd71602795aa48987903db 100644 --- a/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java +++ b/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java @@ -136,7 +136,7 @@ public final class NearbyPlayers { @@ -97,10 +97,10 @@ index fd3d0f6cb53bc8b6186f0d86575f21007b2c20ed..cddeeab73e7b981701a42c5aad6b4777 // Paper end - rewrite chunk system } diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java -index c5949a0e852ca6de84e8dd12e3d4ed8527b60e25..0f311e603c8df175576a33d5d20369cbcda2be55 100644 +index b4ee1682a851eb8f1fae999ba0913ae300a71a89..97408ce24313ff26347ff434ab6460e9971c3598 100644 --- a/net/minecraft/server/level/ServerLevel.java +++ b/net/minecraft/server/level/ServerLevel.java -@@ -507,7 +507,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -508,7 +508,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe @Override public final void moonrise$markChunkForPlayerTicking(final LevelChunk chunk) { final ChunkPos pos = chunk.getPos(); @@ -109,7 +109,7 @@ index c5949a0e852ca6de84e8dd12e3d4ed8527b60e25..0f311e603c8df175576a33d5d20369cb return; } -@@ -2610,7 +2610,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -2612,7 +2612,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe public boolean areEntitiesActuallyLoadedAndTicking(ChunkPos chunkPos) { // Paper start - rewrite chunk system @@ -118,7 +118,7 @@ index c5949a0e852ca6de84e8dd12e3d4ed8527b60e25..0f311e603c8df175576a33d5d20369cb return chunkHolder != null && chunkHolder.isEntityTickingReady(); // Paper end - rewrite chunk system } -@@ -2625,7 +2625,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -2627,7 +2627,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe public boolean canSpawnEntitiesInChunk(ChunkPos chunkPos) { // Paper start - rewrite chunk system diff --git a/leaf-server/minecraft-patches/features/0194-Optimize-checking-nearby-players-for-spawning.patch b/leaf-server/minecraft-patches/features/0194-Optimize-checking-nearby-players-for-spawning.patch index c6fa9a34..46adfa91 100644 --- a/leaf-server/minecraft-patches/features/0194-Optimize-checking-nearby-players-for-spawning.patch +++ b/leaf-server/minecraft-patches/features/0194-Optimize-checking-nearby-players-for-spawning.patch @@ -5,10 +5,10 @@ Subject: [PATCH] Optimize checking nearby players for spawning diff --git a/net/minecraft/server/level/ChunkMap.java b/net/minecraft/server/level/ChunkMap.java -index f621cfea59b6d2f2fb29333e50860584b7992c26..79674f4bd7a12c42dec19a4175012d7a2dc88b84 100644 +index 634d37f6401f19cc28f9c131dbfbbfefb6c0895a..3c80a42a4d99460d75ab18afae43cfe29e4be798 100644 --- a/net/minecraft/server/level/ChunkMap.java +++ b/net/minecraft/server/level/ChunkMap.java -@@ -773,7 +773,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider +@@ -764,7 +764,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } private boolean anyPlayerCloseEnoughForSpawningInternal(ChunkPos chunkPos, boolean reducedRange) { @@ -17,7 +17,7 @@ index f621cfea59b6d2f2fb29333e50860584b7992c26..79674f4bd7a12c42dec19a4175012d7a // Spigot end // Paper start - chunk tick iteration optimisation final ca.spottedleaf.moonrise.common.list.ReferenceList players = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getNearbyPlayers().getPlayers( -@@ -785,23 +785,35 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider +@@ -776,23 +776,35 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider final ServerPlayer[] raw = players.getRawDataUnchecked(); final int len = players.size(); diff --git a/leaf-server/minecraft-patches/features/0201-optimize-mob-despawn.patch b/leaf-server/minecraft-patches/features/0201-optimize-mob-despawn.patch index d4e68d44..3d6e0560 100644 --- a/leaf-server/minecraft-patches/features/0201-optimize-mob-despawn.patch +++ b/leaf-server/minecraft-patches/features/0201-optimize-mob-despawn.patch @@ -5,10 +5,10 @@ Subject: [PATCH] optimize mob despawn diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java -index 0f311e603c8df175576a33d5d20369cbcda2be55..498b1ab5013030c4b9fe0eca57215d93965c43b6 100644 +index 97408ce24313ff26347ff434ab6460e9971c3598..af190cb959478cde3fa9011df85032dbab60ce56 100644 --- a/net/minecraft/server/level/ServerLevel.java +++ b/net/minecraft/server/level/ServerLevel.java -@@ -796,6 +796,8 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -797,6 +797,8 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe } io.papermc.paper.entity.activation.ActivationRange.activateEntities(this); // Paper - EAR @@ -17,7 +17,7 @@ index 0f311e603c8df175576a33d5d20369cbcda2be55..498b1ab5013030c4b9fe0eca57215d93 this.entityTickList .forEach( entity -> { -@@ -831,6 +833,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -832,6 +834,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe } } ); @@ -25,7 +25,7 @@ index 0f311e603c8df175576a33d5d20369cbcda2be55..498b1ab5013030c4b9fe0eca57215d93 this.tickBlockEntities(); } // Paper - rewrite chunk system -@@ -943,6 +946,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -945,6 +948,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe private int currentIceAndSnowTick = 0; protected void resetIceAndSnowTick() { this.currentIceAndSnowTick = this.simpleRandom.nextInt(16); } // Gale - Airplane - optimize random calls in chunk ticking diff --git a/leaf-server/minecraft-patches/features/0209-Async-chunk-sending.patch b/leaf-server/minecraft-patches/features/0209-Async-chunk-sending.patch index b4d3f210..17f0f248 100644 --- a/leaf-server/minecraft-patches/features/0209-Async-chunk-sending.patch +++ b/leaf-server/minecraft-patches/features/0209-Async-chunk-sending.patch @@ -5,10 +5,10 @@ Subject: [PATCH] Async chunk sending diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java b/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java -index 0bca3843e8568b37cda6ae312bdf4f423a0891a9..98054ab2be3fecc5f6a111a11cfe94f1a10419c1 100644 +index 20b1186f53c267f69ed7852f5cf3dd2460f8200d..19b5e4856d471ebfa49335ed19ced767e57df771 100644 --- a/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java +++ b/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java -@@ -440,7 +440,15 @@ public final class RegionizedPlayerChunkLoader { +@@ -448,7 +448,15 @@ public final class RegionizedPlayerChunkLoader { // Note: drop isAlive() check so that chunks properly unload client-side when the player dies ((ChunkSystemChunkHolder)((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager .getChunkHolder(chunkX, chunkZ).vanillaChunkHolder).moonrise$removeReceivedChunk(this.player); diff --git a/leaf-server/minecraft-patches/features/0211-SparklyPaper-Parallel-world-ticking.patch b/leaf-server/minecraft-patches/features/0211-SparklyPaper-Parallel-world-ticking.patch index 54d21a58..3e4b94a1 100644 --- a/leaf-server/minecraft-patches/features/0211-SparklyPaper-Parallel-world-ticking.patch +++ b/leaf-server/minecraft-patches/features/0211-SparklyPaper-Parallel-world-ticking.patch @@ -95,7 +95,7 @@ index 582e012222123e5001c34153f2ee1ab1d08935fd..c0bce2293d07ca58cc5bc9e036ab8dca List states = new java.util.ArrayList<>(level.capturedBlockStates.values()); level.capturedBlockStates.clear(); diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java -index 3c45e6eac0403c9cb13409c8e0e9c1653fd531ba..15b01ff019e48ce9434b1f538d712601c3fe65c8 100644 +index 169d4c5e317af201a2d5ad0d82d39805376c2e9e..c8265f25ead389b025aef6ebe4cc44972132f143 100644 --- a/net/minecraft/server/MinecraftServer.java +++ b/net/minecraft/server/MinecraftServer.java @@ -290,6 +290,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop, ServerLevel> oldLevels = this.levels; - Map, ServerLevel> newLevels = Maps.newLinkedHashMap(oldLevels); - newLevels.remove(level.dimension()); + if (org.dreeam.leaf.config.modules.async.MultithreadedTracker.enabled) { org.dreeam.leaf.async.tracker.AsyncTracker.onTickEnd(this); } // Leaf - Multithreaded tracker +@@ -1816,6 +1869,8 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop this.getDataStorage().computeIfAbsent(RandomSequences.TYPE)); @@ -455,7 +455,7 @@ index 498b1ab5013030c4b9fe0eca57215d93965c43b6..397ac1603c742b82e74cfb5d3e579935 // 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); -@@ -697,8 +700,26 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -698,8 +701,26 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe this.getCraftServer().addWorld(this.getWorld()); // CraftBukkit this.preciseTime = this.serverLevelData.getDayTime(); // Purpur - Configurable daylight cycle this.realPlayers = Lists.newArrayList(); // Leaves - skip @@ -482,7 +482,7 @@ index 498b1ab5013030c4b9fe0eca57215d93965c43b6..397ac1603c742b82e74cfb5d3e579935 // Paper start @Override public boolean hasChunk(int chunkX, int chunkZ) { -@@ -729,8 +750,112 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -730,8 +751,112 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe return this.structureManager; } @@ -595,7 +595,7 @@ index 498b1ab5013030c4b9fe0eca57215d93965c43b6..397ac1603c742b82e74cfb5d3e579935 TickRateManager tickRateManager = this.tickRateManager(); boolean runsNormally = tickRateManager.runsNormally(); if (runsNormally) { -@@ -738,6 +863,14 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -739,6 +864,14 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe this.advanceWeatherCycle(); } @@ -610,7 +610,7 @@ index 498b1ab5013030c4b9fe0eca57215d93965c43b6..397ac1603c742b82e74cfb5d3e579935 int _int = this.getGameRules().getInt(GameRules.RULE_PLAYERS_SLEEPING_PERCENTAGE); if (this.purpurConfig.playersSkipNight && this.sleepStatus.areEnoughSleeping(_int) && this.sleepStatus.areEnoughDeepSleeping(_int, this.players)) { // Purpur - Config for skipping night // Paper start - create time skip event - move up calculations -@@ -1306,9 +1439,12 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -1308,9 +1441,12 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe fluidState.tick(this, pos, blockState); } // Paper start - rewrite chunk system @@ -625,7 +625,7 @@ index 498b1ab5013030c4b9fe0eca57215d93965c43b6..397ac1603c742b82e74cfb5d3e579935 // Paper end - rewrite chunk system } -@@ -1319,9 +1455,12 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -1321,9 +1457,12 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe blockState.tick(this, pos, this.random); } // Paper start - rewrite chunk system @@ -640,7 +640,7 @@ index 498b1ab5013030c4b9fe0eca57215d93965c43b6..397ac1603c742b82e74cfb5d3e579935 // Paper end - rewrite chunk system } -@@ -1586,6 +1725,8 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -1588,6 +1727,8 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe } private void addPlayer(ServerPlayer player) { @@ -649,7 +649,7 @@ index 498b1ab5013030c4b9fe0eca57215d93965c43b6..397ac1603c742b82e74cfb5d3e579935 Entity entity = this.getEntity(player.getUUID()); if (entity != null) { LOGGER.warn("Force-added player with duplicate UUID {}", player.getUUID()); -@@ -1598,7 +1739,12 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -1600,7 +1741,12 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe // CraftBukkit start private boolean addEntity(Entity entity, @Nullable org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason spawnReason) { diff --git a/leaf-server/minecraft-patches/features/0212-SparklyPaper-Track-each-world-MSPT.patch b/leaf-server/minecraft-patches/features/0212-SparklyPaper-Track-each-world-MSPT.patch index 306a0200..ef5139a6 100644 --- a/leaf-server/minecraft-patches/features/0212-SparklyPaper-Track-each-world-MSPT.patch +++ b/leaf-server/minecraft-patches/features/0212-SparklyPaper-Track-each-world-MSPT.patch @@ -6,7 +6,7 @@ Subject: [PATCH] SparklyPaper: Track each world MSPT Original project: https://github.com/SparklyPower/SparklyPaper diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java -index 15b01ff019e48ce9434b1f538d712601c3fe65c8..f7aa2b84c20791fe6b626a7e1393145b2d0bf206 100644 +index c8265f25ead389b025aef6ebe4cc44972132f143..5ad00ebb9e0acab73a8366f0caf06979cfcccca0 100644 --- a/net/minecraft/server/MinecraftServer.java +++ b/net/minecraft/server/MinecraftServer.java @@ -1686,7 +1686,16 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop realPlayers; // Leaves - skip + public @Nullable java.util.concurrent.Future[] trackerTask; // Leaf - Multithreaded tracker + public final @Nullable org.dreeam.leaf.async.ai.AsyncGoalExecutor asyncGoalExecutor; // Leaf - Async target finding @Override public @Nullable LevelChunk getChunkIfLoaded(int x, int z) { -@@ -707,6 +717,13 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -708,6 +718,13 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe this.preciseTime = this.serverLevelData.getDayTime(); // Purpur - Configurable daylight cycle this.realPlayers = Lists.newArrayList(); // Leaves - skip this.tickExecutor = java.util.concurrent.Executors.newSingleThreadExecutor(new org.dreeam.leaf.async.world.SparklyPaperServerLevelTickExecutorThreadFactory(getWorld().getName())); // SparklyPaper - parallel world ticking @@ -178,7 +178,7 @@ index a9f33e705b3c0abfa3dcc01647a57c06bb614e1c..02c2b9c1978959e1ee0be5c72a5f7b98 // Leaf start - SparklyPaper - parallel world ticking - Shutdown handling for async reads diff --git a/net/minecraft/world/entity/LivingEntity.java b/net/minecraft/world/entity/LivingEntity.java -index 1d78cacbf2c63f35fdf0552d50834385ab51aaf0..aadba595ca9ad6ba403b16b3c741421005f0414a 100644 +index 6d9d8f85bf6936eee76c3d790dd676aa309a1d46..b19d2066b921d27f03b2c0af06fbf76fcb8d87b5 100644 --- a/net/minecraft/world/entity/LivingEntity.java +++ b/net/minecraft/world/entity/LivingEntity.java @@ -750,6 +750,12 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin diff --git a/leaf-server/minecraft-patches/features/0237-Protocol-Core.patch b/leaf-server/minecraft-patches/features/0237-Protocol-Core.patch index 4e09f207..dd213097 100644 --- a/leaf-server/minecraft-patches/features/0237-Protocol-Core.patch +++ b/leaf-server/minecraft-patches/features/0237-Protocol-Core.patch @@ -22,10 +22,10 @@ index 56fd1ed7ccaf96e7eedea60fbdbf7f934939d563..d2f522ea6d0a209496848af073c9af1c } diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java -index a73969d68b51a6996ae59073360051f447ce42a8..9eb35364757b5748f7228c1557e9350edc805440 100644 +index 9a68042d1efb0da915fc2a302641c9ea6d92f582..225d1604baa2a2c974de5c3ae45d3afd3c7d31df 100644 --- a/net/minecraft/server/MinecraftServer.java +++ b/net/minecraft/server/MinecraftServer.java -@@ -1803,6 +1803,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop Co-authored-by: Kevin Raneri Co-authored-by: HaHaWTH <102713261+HaHaWTH@users.noreply.github.com> +Co-authored-by: hayanesuru This patch refactored from original multithreaded tracker (Petal version), and is derived from the Airplane fork by Paul Sauve, the tree is like: @@ -23,23 +24,6 @@ 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 379c2dc1853e45a96dda9b13bf28b7e08f65658a..361f4de9cdf0f7505628a2fed2a3f5366031e04b 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 - Multithreaded tracker -+ if (org.dreeam.leaf.config.modules.async.MultithreadedTracker.enabled) { -+ net.minecraft.server.MinecraftServer.getServer().scheduleOnMain(event::callEvent); -+ return; -+ } -+ // Leaf end - Multithreaded tracker - throw new IllegalStateException(event.getEventName() + " may only be triggered synchronously."); - } - // Leaves start - skip photographer diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java index beae8a57a0ce9b8e7d81619efe4c39d908869319..6b1926080eddf61ff9c0156a6846f7f0bcff1c2d 100644 --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java @@ -72,9 +56,18 @@ index beae8a57a0ce9b8e7d81619efe4c39d908869319..6b1926080eddf61ff9c0156a6846f7f0 } return set; diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -index 1e08ac054947b1a0a6cce2f886be1ed83c74a642..d4b7b7747dda687fe3d12b85b7eccc96a0b825f6 100644 +index 1e08ac054947b1a0a6cce2f886be1ed83c74a642..7850bc5432bc88ec060ee64e45613b6f9c5c4fac 100644 --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +@@ -217,7 +217,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player, PluginMessa + private static final WeakHashMap> pluginWeakReferences = new WeakHashMap<>(); + private static final net.kyori.adventure.text.Component DEFAULT_KICK_COMPONENT = net.kyori.adventure.text.Component.translatable("multiplayer.disconnect.kicked"); + private final ConversationTracker conversationTracker = new ConversationTracker(); +- private final Map>> invertedVisibilityEntities = new it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap<>(); // SparklyPaper - optimize canSee checks ++ private final Map>> invertedVisibilityEntities = org.dreeam.leaf.config.modules.async.MultithreadedTracker.enabled ? it.unimi.dsi.fastutil.objects.Object2ObjectMaps.synchronize(new it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap<>()) : new it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap<>(); // SparklyPaper - optimize canSee checks // Leaf - Multithreaded tracker + private final Set unlistedEntities = new HashSet<>(); // Paper - Add Listing API for Player + public org.bukkit.event.player.PlayerResourcePackStatusEvent.Status resourcePackStatus; // Paper - more resource pack API + private long firstPlayed = 0; @@ -2962,7 +2962,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player, PluginMessa Iterator iterator = collection.iterator(); while (iterator.hasNext()) { @@ -84,34 +77,3 @@ index 1e08ac054947b1a0a6cce2f886be1ed83c74a642..d4b7b7747dda687fe3d12b85b7eccc96 iterator.remove(); break; } -diff --git a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java -index a162440a583801671787163d998d6b9546ef7e61..5e73db484a5526f4c39c7cc2de5ddc3ff037d2e4 100644 ---- a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java -+++ b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java -@@ -1808,6 +1808,26 @@ public class CraftEventFactory { - } - - public static boolean handleBlockFormEvent(Level world, BlockPos pos, net.minecraft.world.level.block.state.BlockState state, int flags, @Nullable Entity entity, boolean checkSetResult) { -+ // Leaf start - Multithreaded tracker -+ if (org.dreeam.leaf.config.modules.async.MultithreadedTracker.enabled && Thread.currentThread() instanceof org.dreeam.leaf.async.tracker.MultithreadedTracker.MultithreadedTrackerThread) { -+ java.util.concurrent.CompletableFuture future = new java.util.concurrent.CompletableFuture<>(); -+ net.minecraft.server.MinecraftServer.getServer().scheduleOnMain(() -> { -+ boolean resultFlag = false; -+ CraftBlockState snapshot = CraftBlockStates.getBlockState(world, pos); -+ snapshot.setData(state); -+ -+ BlockFormEvent event = (entity == null) ? new BlockFormEvent(snapshot.getBlock(), snapshot) : new EntityBlockFormEvent(entity.getBukkitEntity(), snapshot.getBlock(), snapshot); -+ if (event.callEvent()) { -+ boolean result = snapshot.place(flags); -+ resultFlag = !checkSetResult || result; -+ } -+ -+ future.complete(resultFlag); -+ }); -+ -+ return future.join(); -+ } -+ // Leaf end - Multithreaded tracker - CraftBlockState snapshot = CraftBlockStates.getBlockState(world, pos); - snapshot.setData(state); - diff --git a/leaf-server/paper-patches/features/0043-Asynchronous-locator.patch b/leaf-server/paper-patches/features/0043-Asynchronous-locator.patch index cd3b7ec8..cfbf1934 100644 --- a/leaf-server/paper-patches/features/0043-Asynchronous-locator.patch +++ b/leaf-server/paper-patches/features/0043-Asynchronous-locator.patch @@ -24,13 +24,13 @@ index 69cdd304d255d52c9b7dc9b6a33ffdb630b79abe..a4aa2615823d77920ff55b8aa0bcc27a this(group, run, name, ID_GENERATOR.incrementAndGet()); } 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 361f4de9cdf0f7505628a2fed2a3f5366031e04b..548fcd9646dee0c40b6ba9b3dafb9ca157dfe324 100644 +index 379c2dc1853e45a96dda9b13bf28b7e08f65658a..b9c159537c7db0ff85ebb0ff6b510e5b5061f5f9 100644 --- a/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java +++ b/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java -@@ -48,6 +48,12 @@ class PaperEventManager { - return; - } - // Leaf end - Multithreaded tracker +@@ -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 - Asynchronous locator + if (org.dreeam.leaf.config.modules.async.AsyncLocator.enabled) { + net.minecraft.server.MinecraftServer.getServer().scheduleOnMain(event::callEvent); diff --git a/leaf-server/src/main/java/org/dreeam/leaf/async/FixedThreadExecutor.java b/leaf-server/src/main/java/org/dreeam/leaf/async/FixedThreadExecutor.java new file mode 100644 index 00000000..9c696a0d --- /dev/null +++ b/leaf-server/src/main/java/org/dreeam/leaf/async/FixedThreadExecutor.java @@ -0,0 +1,85 @@ +package org.dreeam.leaf.async; + +import net.minecraft.Util; +import org.dreeam.leaf.util.queue.MpmcQueue; + +import java.util.concurrent.Callable; +import java.util.concurrent.FutureTask; +import java.util.concurrent.locks.LockSupport; + +public final class FixedThreadExecutor { + private final Thread[] workers; + private final MpmcQueue channel; + private static volatile boolean SHUTDOWN = false; + + public FixedThreadExecutor(int numThreads, int queue, String prefix) { + if (numThreads <= 0) { + throw new IllegalArgumentException(); + } + this.workers = new Thread[numThreads]; + this.channel = new MpmcQueue<>(Runnable.class, queue); + for (int i = 0; i < numThreads; i++) { + workers[i] = Thread.ofPlatform() + .uncaughtExceptionHandler(Util::onThreadException) + .daemon(false) + .priority(Thread.NORM_PRIORITY) + .name(prefix + " - " + i) + .start(new Worker(channel)); + } + } + + public FutureTask submitOrRun(Callable task) { + if (SHUTDOWN) { + throw new IllegalStateException(); + } + + final FutureTask t = new FutureTask<>(task); + if (!channel.send(t)) { + t.run(); + } + return t; + } + + public void unpack() { + int size = Math.min(Math.max(1, channel.length()), workers.length); + for (int i = 0; i < size; i++) { + LockSupport.unpark(workers[i]); + } + } + + public void shutdown() { + SHUTDOWN = true; + for (final Thread worker : workers) { + LockSupport.unpark(worker); + } + } + + public void join(long timeoutMillis) throws InterruptedException { + final long startTime = System.currentTimeMillis(); + + for (final Thread worker : workers) { + final long remaining = timeoutMillis - System.currentTimeMillis() + startTime; + if (remaining <= 0) { + return; + } + worker.join(remaining); + if (worker.isAlive()) { + return; + } + } + } + + private record Worker(MpmcQueue channel) implements Runnable { + @Override + public void run() { + while (!SHUTDOWN) { + final Runnable task = channel.recv(); + if (task != null) { + task.run(); + } else if (!SHUTDOWN) { + LockSupport.park(); + } + } + } + } +} diff --git a/leaf-server/src/main/java/org/dreeam/leaf/async/ShutdownExecutors.java b/leaf-server/src/main/java/org/dreeam/leaf/async/ShutdownExecutors.java index 3103e125..55d217d3 100644 --- a/leaf-server/src/main/java/org/dreeam/leaf/async/ShutdownExecutors.java +++ b/leaf-server/src/main/java/org/dreeam/leaf/async/ShutdownExecutors.java @@ -5,7 +5,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.dreeam.leaf.async.ai.AsyncGoalThread; import org.dreeam.leaf.async.path.AsyncPathProcessor; -import org.dreeam.leaf.async.tracker.MultithreadedTracker; +import org.dreeam.leaf.async.tracker.AsyncTracker; import java.util.concurrent.TimeUnit; @@ -40,11 +40,11 @@ public class ShutdownExecutors { } } - if (MultithreadedTracker.TRACKER_EXECUTOR != null) { - LOGGER.info("Waiting for mob tracker executor to shutdown..."); - MultithreadedTracker.TRACKER_EXECUTOR.shutdown(); + if (AsyncTracker.TRACKER_EXECUTOR != null) { + LOGGER.info("Waiting for entity tracker executor to shutdown..."); + AsyncTracker.TRACKER_EXECUTOR.shutdown(); try { - MultithreadedTracker.TRACKER_EXECUTOR.awaitTermination(10L, TimeUnit.SECONDS); + AsyncTracker.TRACKER_EXECUTOR.join(10_000L); } catch (InterruptedException ignored) { } } diff --git a/leaf-server/src/main/java/org/dreeam/leaf/async/tracker/AsyncTracker.java b/leaf-server/src/main/java/org/dreeam/leaf/async/tracker/AsyncTracker.java new file mode 100644 index 00000000..554a6758 --- /dev/null +++ b/leaf-server/src/main/java/org/dreeam/leaf/async/tracker/AsyncTracker.java @@ -0,0 +1,91 @@ +package org.dreeam.leaf.async.tracker; + +import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server.ServerEntityLookup; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import org.dreeam.leaf.async.FixedThreadExecutor; +import org.dreeam.leaf.config.modules.async.MultithreadedTracker; +import org.dreeam.leaf.util.EntitySlice; + +import java.util.concurrent.*; + +public final class AsyncTracker { + private static final String THREAD_NAME = "Leaf Async Tracker Thread"; + public static final boolean ENABLED = MultithreadedTracker.enabled; + public static final int QUEUE = 1024; + public static final int MIN_CHUNK = 16; + public static final int THREADS = MultithreadedTracker.threads; + public static final FixedThreadExecutor TRACKER_EXECUTOR = ENABLED ? new FixedThreadExecutor( + THREADS, + QUEUE, + THREAD_NAME + ) : null; + + private AsyncTracker() { + } + + public static void init() { + if (TRACKER_EXECUTOR == null || !ENABLED) { + throw new IllegalStateException(); + } + } + + public static void tick(ServerLevel world) { + ServerEntityLookup entityLookup = (ServerEntityLookup) world.moonrise$getEntityLookup(); + ca.spottedleaf.moonrise.common.list.ReferenceList trackerEntities = entityLookup.trackerEntities; + int trackerEntitiesSize = trackerEntities.size(); + if (trackerEntitiesSize == 0) { + return; + } + Entity[] trackerEntitiesRaw = trackerEntities.getRawDataUnchecked(); + Entity[] entities = new Entity[trackerEntitiesSize]; + System.arraycopy(trackerEntitiesRaw, 0, entities, 0, trackerEntitiesSize); + EntitySlice slice = new EntitySlice(entities); + EntitySlice[] slices = entities.length <= THREADS * MIN_CHUNK ? slice.chunks(MIN_CHUNK) : slice.splitEvenly(THREADS); + @SuppressWarnings("unchecked") + Future[] futures = new Future[slices.length]; + for (int i = 0; i < futures.length; i++) { + futures[i] = TRACKER_EXECUTOR.submitOrRun(new TrackerTask(world, slices[i])); + } + TRACKER_EXECUTOR.unpack(); + world.trackerTask = futures; + } + + public static void onEntitiesTickEnd(ServerLevel world) { + Future[] task = world.trackerTask; + if (task == null) { + return; + } + for (Future fut : task) { + if (!fut.isDone()) { + return; + } + } + handle(world, task); + } + + public static void onTickEnd(MinecraftServer server) { + for (ServerLevel world : server.getAllLevels()) { + Future[] task = world.trackerTask; + if (task != null) { + handle(world, task); + } + } + } + + private static void handle(ServerLevel world, Future[] futures) { + try { + TrackerCtx ctx = futures[0].get(); + for (int i = 1; i < futures.length; i++) { + ctx.join(futures[i].get()); + } + world.trackerTask = null; + ctx.handle(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (ExecutionException e) { + throw new RuntimeException(e); + } + } +} diff --git a/leaf-server/src/main/java/org/dreeam/leaf/async/tracker/MultithreadedTracker.java b/leaf-server/src/main/java/org/dreeam/leaf/async/tracker/MultithreadedTracker.java deleted file mode 100644 index a5f6f362..00000000 --- a/leaf-server/src/main/java/org/dreeam/leaf/async/tracker/MultithreadedTracker.java +++ /dev/null @@ -1,211 +0,0 @@ -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.entity.server.ServerEntityLookup; -import ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity; -import com.google.common.util.concurrent.ThreadFactoryBuilder; -import net.minecraft.Util; -import net.minecraft.server.level.ChunkMap; -import net.minecraft.server.level.FullChunkStatus; -import net.minecraft.server.level.ServerLevel; -import net.minecraft.world.entity.Entity; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.jetbrains.annotations.NotNull; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.RejectedExecutionHandler; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -public class MultithreadedTracker { - - private static final String THREAD_PREFIX = "Leaf Async Tracker"; - private static final Logger LOGGER = LogManager.getLogger(THREAD_PREFIX); - private static long lastWarnMillis = System.currentTimeMillis(); - public static ThreadPoolExecutor TRACKER_EXECUTOR = null; - - private MultithreadedTracker() { - } - - public static void init() { - if (TRACKER_EXECUTOR == null) { - TRACKER_EXECUTOR = new ThreadPoolExecutor( - getCorePoolSize(), - getMaxPoolSize(), - getKeepAliveTime(), TimeUnit.SECONDS, - getQueueImpl(), - getThreadFactory(), - getRejectedPolicy() - ); - } else { - // Temp no-op - //throw new IllegalStateException(); - } - } - - public static void tick(ServerLevel level) { - try { - if (!org.dreeam.leaf.config.modules.async.MultithreadedTracker.compatModeEnabled) { - tickAsync(level); - } else { - tickAsyncWithCompatMode(level); - } - } catch (Exception e) { - LOGGER.error("Error occurred while executing async task.", e); - } - } - - private static void tickAsync(ServerLevel level) { - final NearbyPlayers nearbyPlayers = level.moonrise$getNearbyPlayers(); - final ServerEntityLookup entityLookup = (ServerEntityLookup) level.moonrise$getEntityLookup(); - - final ReferenceList trackerEntities = entityLookup.trackerEntities; - final int trackerEntitiesSize = trackerEntities.size(); - final Entity[] trackerEntitiesRaw = trackerEntities.getRawDataUnchecked(); - - // Move tracking to off-main - TRACKER_EXECUTOR.execute(() -> { - for (int i = 0; i < trackerEntitiesSize; i++) { - Entity entity = trackerEntitiesRaw[i]; - if (entity == null) continue; - - final ChunkMap.TrackedEntity tracker = ((EntityTrackerEntity) entity).moonrise$getTrackedEntity(); - - if (tracker == null) continue; - - synchronized (tracker) { - NearbyPlayers.TrackedChunk trackedChunk = nearbyPlayers.getChunk(entity.chunkPosition()); - tracker.moonrise$tick(trackedChunk); - tracker.serverEntity.sendChanges(); - } - } - }); - } - - private static void tickAsyncWithCompatMode(ServerLevel 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 int trackerEntitiesSize = trackerEntities.size(); - final Runnable[] sendChangesTasks = new Runnable[trackerEntitiesSize]; - final Runnable[] tickTask = new Runnable[trackerEntitiesSize]; - int index = 0; - - for (int i = 0; i < trackerEntitiesSize; i++) { - Entity entity = trackerEntitiesRaw[i]; - if (entity == null) continue; - - final ChunkMap.TrackedEntity tracker = ((EntityTrackerEntity) entity).moonrise$getTrackedEntity(); - - if (tracker == null) continue; - - synchronized (tracker) { - tickTask[index] = tracker.leafTickCompact(nearbyPlayers.getChunk(entity.chunkPosition())); - sendChangesTasks[index] = tracker.serverEntity::sendChanges; // Collect send changes to task array - } - index++; - } - - // batch submit tasks - TRACKER_EXECUTOR.execute(() -> { - for (final Runnable tick : tickTask) { - if (tick == null) continue; - - tick.run(); - } - for (final Runnable sendChanges : sendChangesTasks) { - if (sendChanges == null) continue; - - sendChanges.run(); - } - }); - } - - // Original ChunkMap#newTrackerTick of Paper - // Just for diff usage for future update - private static void tickOriginal(ServerLevel level) { - 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(((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity) entity).moonrise$getChunkData().nearbyPlayers); - if (((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerTrackedEntity) tracker).moonrise$hasPlayers() - || ((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity) entity).moonrise$getChunkStatus().isOrAfter(FullChunkStatus.ENTITY_TICKING)) { - tracker.serverEntity.sendChanges(); - } - } - } - - private static int getCorePoolSize() { - return 1; - } - - private static int getMaxPoolSize() { - return org.dreeam.leaf.config.modules.async.MultithreadedTracker.asyncEntityTrackerMaxThreads; - } - - private static long getKeepAliveTime() { - return org.dreeam.leaf.config.modules.async.MultithreadedTracker.asyncEntityTrackerKeepalive; - } - - private static BlockingQueue getQueueImpl() { - final int queueCapacity = org.dreeam.leaf.config.modules.async.MultithreadedTracker.asyncEntityTrackerQueueSize; - - return new LinkedBlockingQueue<>(queueCapacity); - } - - private static @NotNull ThreadFactory getThreadFactory() { - return new ThreadFactoryBuilder() - .setThreadFactory(MultithreadedTrackerThread::new) - .setNameFormat(THREAD_PREFIX + " Thread - %d") - .setPriority(Thread.NORM_PRIORITY - 2) - .setUncaughtExceptionHandler(Util::onThreadException) - .build(); - } - - private static @NotNull RejectedExecutionHandler getRejectedPolicy() { - return (rejectedTask, executor) -> { - BlockingQueue workQueue = executor.getQueue(); - - if (!executor.isShutdown()) { - if (!workQueue.isEmpty()) { - List pendingTasks = new ArrayList<>(workQueue.size()); - - workQueue.drainTo(pendingTasks); - - for (Runnable pendingTask : pendingTasks) { - pendingTask.run(); - } - } - - rejectedTask.run(); - } - - if (System.currentTimeMillis() - lastWarnMillis > 30000L) { - LOGGER.warn("Async entity tracker is busy! Tracking tasks will be done in the server thread. Increasing max-threads in Leaf config may help."); - lastWarnMillis = System.currentTimeMillis(); - } - }; - } - - public static class MultithreadedTrackerThread extends Thread { - - public MultithreadedTrackerThread(Runnable runnable) { - super(runnable); - } - } -} diff --git a/leaf-server/src/main/java/org/dreeam/leaf/async/tracker/TrackerCtx.java b/leaf-server/src/main/java/org/dreeam/leaf/async/tracker/TrackerCtx.java new file mode 100644 index 00000000..16348b7e --- /dev/null +++ b/leaf-server/src/main/java/org/dreeam/leaf/async/tracker/TrackerCtx.java @@ -0,0 +1,274 @@ +package org.dreeam.leaf.async.tracker; + +import ca.spottedleaf.moonrise.common.misc.NearbyPlayers; +import io.papermc.paper.event.player.PlayerTrackEntityEvent; +import io.papermc.paper.event.player.PlayerUntrackEntityEvent; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ClientGamePacketListener; +import net.minecraft.network.protocol.game.ClientboundBundlePacket; +import net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket; +import net.minecraft.network.protocol.game.ClientboundSetEntityMotionPacket; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.FullChunkStatus; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import net.minecraft.server.network.ServerPlayerConnection; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.boss.wither.WitherBoss; +import net.minecraft.world.entity.decoration.ItemFrame; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.MapItem; +import net.minecraft.world.level.saveddata.maps.MapId; +import net.minecraft.world.level.saveddata.maps.MapItemSavedData; +import org.bukkit.event.player.PlayerVelocityEvent; + +import java.util.Arrays; + +public final class TrackerCtx { + private static final int SIZE_LIMIT_PER_BUNDLE = 4096; + + private final Reference2ReferenceOpenHashMap>> packets; + private final ServerLevel world; + private final ObjectArrayList bukkitVelocityEvent = new ObjectArrayList<>(); + private final ObjectArrayList bukkitItemFrames = new ObjectArrayList<>(); + private final ObjectArrayList witherBosses = new ObjectArrayList<>(); + private final ObjectArrayList paperStopSeen = new ObjectArrayList<>(); + private final ObjectArrayList paperStartSeen = new ObjectArrayList<>(); + private final ObjectArrayList pluginEntity = new ObjectArrayList<>(); + + private record BossEvent(WitherBoss witherBoss, ObjectArrayList add, ObjectArrayList remove) {} + private record PaperStopSeen(Entity e, ObjectArrayList q) {} + private record PaperStartSeen(Entity e, ObjectArrayList q) {} + + public TrackerCtx(ServerLevel world) { + this.packets = new Reference2ReferenceOpenHashMap<>(); + this.world = world; + } + + public void stopSeenByPlayer(ServerPlayerConnection connection, Entity entity) { + if (PlayerUntrackEntityEvent.getHandlerList().getRegisteredListeners().length != 0) { + if (paperStopSeen.isEmpty()) { + paperStopSeen.add(new PaperStopSeen(entity, new ObjectArrayList<>())); + } + if (!paperStopSeen.getLast().e.equals(entity)) { + paperStopSeen.add(new PaperStopSeen(entity, new ObjectArrayList<>())); + } + paperStopSeen.getLast().q.add(connection); + } + if (entity instanceof WitherBoss witherBoss) { + if (witherBosses.isEmpty()) { + witherBosses.add(new BossEvent(witherBoss, new ObjectArrayList<>(), new ObjectArrayList<>())); + } + if (!witherBosses.getLast().witherBoss.equals(witherBoss)) { + witherBosses.add(new BossEvent(witherBoss, new ObjectArrayList<>(), new ObjectArrayList<>())); + } + witherBosses.getLast().remove.add(connection.getPlayer()); + } + } + + public void startSeenByPlayer(ServerPlayerConnection connection, Entity entity) { + if (PlayerTrackEntityEvent.getHandlerList().getRegisteredListeners().length != 0) { + if (paperStartSeen.isEmpty()) { + paperStartSeen.add(new PaperStartSeen(entity, new ObjectArrayList<>())); + } + if (!paperStartSeen.getLast().e.equals(entity)) { + paperStartSeen.add(new PaperStartSeen(entity, new ObjectArrayList<>())); + } + paperStartSeen.getLast().q.add(connection); + } + if (entity instanceof WitherBoss witherBoss) { + if (witherBosses.isEmpty()) { + witherBosses.add(new BossEvent(witherBoss, new ObjectArrayList<>(), new ObjectArrayList<>())); + } + if (!witherBosses.getLast().witherBoss.equals(witherBoss)) { + witherBosses.add(new BossEvent(witherBoss, new ObjectArrayList<>(), new ObjectArrayList<>())); + } + witherBosses.getLast().add.add(connection.getPlayer()); + } + } + + public void updateItemFrame(ItemFrame itemFrame) { + bukkitItemFrames.add(itemFrame); + } + + public void playerVelocity(ServerPlayer player) { + if (PlayerVelocityEvent.getHandlerList().getRegisteredListeners().length == 0) { + player.hurtMarked = false; + player.moonrise$getTrackedEntity().leafBroadcastAndSend(this, new ClientboundSetEntityMotionPacket(player)); + } else { + bukkitVelocityEvent.add(player); + } + } + + public void citizensEntity(Entity entity) { + pluginEntity.add(entity); + } + + public void send(ServerPlayerConnection connection, Packet packet) { + packets.computeIfAbsent(connection, x -> new ObjectArrayList<>()).add(packet); + } + + void join(TrackerCtx other) { + bukkitVelocityEvent.addAll(other.bukkitVelocityEvent); + bukkitItemFrames.addAll(other.bukkitItemFrames); + paperStopSeen.addAll(other.paperStopSeen); + paperStartSeen.addAll(other.paperStartSeen); + pluginEntity.addAll(other.pluginEntity); + if (other.packets.isEmpty()) { + return; + } + var iterator = other.packets.reference2ReferenceEntrySet().fastIterator(); + while (iterator.hasNext()) { + var entry = iterator.next(); + packets.computeIfAbsent(entry.getKey(), x -> new ObjectArrayList<>()).addAll(entry.getValue()); + } + } + + void handle() { + handlePackets(world, packets); + + if (!pluginEntity.isEmpty()) { + for (final Entity entity : pluginEntity) { + final ChunkMap.TrackedEntity tracker = ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity)entity).moonrise$getTrackedEntity(); + if (tracker == null) { + continue; + } + NearbyPlayers.TrackedChunk trackedChunk = world.moonrise$getNearbyPlayers().getChunk(entity.chunkPosition()); + tracker.leafTick(this, trackedChunk); + boolean flag = false; + if (tracker.moonrise$hasPlayers()) { + flag = true; + } else { + FullChunkStatus status = ((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity) entity).moonrise$getChunkStatus(); + if (status != null && status.isOrAfter(FullChunkStatus.ENTITY_TICKING)) { + flag = true; + } + } + if (flag) { + tracker.serverEntity.leafSendChanges(this, tracker); + } + } + pluginEntity.clear(); + } + if (!bukkitVelocityEvent.isEmpty()) { + for (ServerPlayer player : bukkitVelocityEvent) { + if (!world.equals(player.level())) { + continue; + } + boolean cancelled = false; + + org.bukkit.entity.Player player1 = player.getBukkitEntity(); + org.bukkit.util.Vector velocity = player1.getVelocity(); + + PlayerVelocityEvent event = new PlayerVelocityEvent(player1, velocity.clone()); + if (!event.callEvent()) { + cancelled = true; + } else if (!velocity.equals(event.getVelocity())) { + player1.setVelocity(event.getVelocity()); + } + if (!cancelled) { + player.hurtMarked = false; + ChunkMap.TrackedEntity trackedEntity = player.moonrise$getTrackedEntity(); + trackedEntity.leafBroadcast(this, new ClientboundSetEntityMotionPacket(player)); + } + } + bukkitVelocityEvent.clear(); + } + if (!bukkitItemFrames.isEmpty()) { + for (ItemFrame itemFrame : bukkitItemFrames) { + MapId mapId = itemFrame.cachedMapId; // Paper - Perf: Cache map ids on item frames + MapItemSavedData savedData = MapItem.getSavedData(mapId, world); + if (savedData != null) { + ChunkMap.TrackedEntity trackedEntity = itemFrame.moonrise$getTrackedEntity(); + if (trackedEntity != null) { + ItemStack item = itemFrame.getItem(); + for (final net.minecraft.server.network.ServerPlayerConnection connection : trackedEntity.seenBy()) { + final ServerPlayer serverPlayer = connection.getPlayer(); // Paper + savedData.tickCarriedBy(serverPlayer, item); + Packet updatePacket = (Packet) savedData.getUpdatePacket(mapId, serverPlayer); + if (updatePacket != null) { + send(serverPlayer.connection, updatePacket); + } + } + } + } + } + bukkitItemFrames.clear(); + } + if (!witherBosses.isEmpty()) { + for (BossEvent witherBoss : witherBosses) { + for (ServerPlayer player : witherBoss.add) { + if (!world.equals(player.level())) { + continue; + } + witherBoss.witherBoss.bossEvent.leafAddPlayer(this, player); + } + for (ServerPlayer player : witherBoss.remove) { + witherBoss.witherBoss.bossEvent.leafRemovePlayer(this, player); + } + } + witherBosses.clear(); + } + if (!paperStartSeen.isEmpty()) { + for (PaperStartSeen startSeen : paperStartSeen) { + for (ServerPlayerConnection connection : startSeen.q) { + if (!new PlayerTrackEntityEvent( + connection.getPlayer().getBukkitEntity(), + startSeen.e.getBukkitEntity() + ).callEvent()) { + send(connection, new ClientboundRemoveEntitiesPacket(startSeen.e.getId())); + } + } + } + paperStartSeen.clear(); + } + if (!paperStopSeen.isEmpty()) { + for (PaperStopSeen stopSeen : paperStopSeen) { + for (ServerPlayerConnection connection : stopSeen.q) { + new PlayerUntrackEntityEvent( + connection.getPlayer().getBukkitEntity(), + stopSeen.e.getBukkitEntity() + ).callEvent(); + } + } + paperStopSeen.clear(); + } + + handlePackets(world, packets); + } + + private static void handlePackets(ServerLevel world, Reference2ReferenceOpenHashMap>> packets) { + if (packets.isEmpty()) { + return; + } + var iter = packets.reference2ReferenceEntrySet().fastIterator(); + while (iter.hasNext()) { + var entry = iter.next(); + ServerPlayerConnection connection = entry.getKey(); + ObjectArrayList> list = entry.getValue(); + if (!world.equals(connection.getPlayer().level())) { + continue; + } + int size = list.size(); + if (size > SIZE_LIMIT_PER_BUNDLE) { + int from = 0; + while (from < size) { + int chunkLen = Math.min(SIZE_LIMIT_PER_BUNDLE, size - from); + Packet[] chunk = new Packet[chunkLen]; + list.getElements(from, chunk, 0, chunkLen); + connection.send(new ClientboundBundlePacket(Arrays.asList(chunk))); + from += chunkLen; + } + } else { + connection.send(new ClientboundBundlePacket(list)); + } + if (connection instanceof ServerGamePacketListenerImpl conn) { + conn.connection.flushChannel(); + } + } + packets.clear(); + } +} diff --git a/leaf-server/src/main/java/org/dreeam/leaf/async/tracker/TrackerTask.java b/leaf-server/src/main/java/org/dreeam/leaf/async/tracker/TrackerTask.java new file mode 100644 index 00000000..2e176243 --- /dev/null +++ b/leaf-server/src/main/java/org/dreeam/leaf/async/tracker/TrackerTask.java @@ -0,0 +1,56 @@ +package org.dreeam.leaf.async.tracker; + +import ca.spottedleaf.moonrise.common.misc.NearbyPlayers; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.FullChunkStatus; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import org.dreeam.leaf.util.EntitySlice; + +import java.util.concurrent.Callable; + +public final class TrackerTask implements Callable { + public final ServerLevel world; + private final EntitySlice entities; + + public TrackerTask(ServerLevel world, EntitySlice trackerEntities) { + this.world = world; + this.entities = trackerEntities; + } + + @Override + public TrackerCtx call() throws Exception { + NearbyPlayers nearbyPlayers = world.moonrise$getNearbyPlayers(); + TrackerCtx ctx = new TrackerCtx(this.world); + final Entity[] raw = entities.array(); + for (int i = entities.start(); i < entities.end(); i++) { + final Entity entity = raw[i]; + final ChunkMap.TrackedEntity tracker = ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity)entity).moonrise$getTrackedEntity(); + if (tracker == null) { + continue; + } + if (tracker.getClass() != ChunkMap.TrackedEntity.class) { + ctx.citizensEntity(entity); + continue; + } + NearbyPlayers.TrackedChunk trackedChunk = nearbyPlayers.getChunk(entity.chunkPosition()); + + tracker.leafTick(ctx, trackedChunk); + boolean flag = false; + if (tracker.moonrise$hasPlayers()) { + flag = true; + } else { + // may read old value + FullChunkStatus status = ((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity) entity).moonrise$getChunkStatus(); + // removed in world + if (status != null && status.isOrAfter(FullChunkStatus.ENTITY_TICKING)) { + flag = true; + } + } + if (flag) { + tracker.serverEntity.leafSendChanges(ctx, tracker); + } + } + return ctx; + } +} diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/MultithreadedTracker.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/MultithreadedTracker.java index 8c6c257f..03bb0598 100644 --- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/MultithreadedTracker.java +++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/MultithreadedTracker.java @@ -1,8 +1,10 @@ package org.dreeam.leaf.config.modules.async; +import org.dreeam.leaf.async.tracker.AsyncTracker; import org.dreeam.leaf.config.ConfigModules; import org.dreeam.leaf.config.EnumConfigCategory; import org.dreeam.leaf.config.LeafConfig; +import org.dreeam.leaf.config.annotations.Experimental; public class MultithreadedTracker extends ConfigModules { @@ -10,28 +12,20 @@ public class MultithreadedTracker extends ConfigModules { return EnumConfigCategory.ASYNC.getBaseKeyName() + ".async-entity-tracker"; } + @Experimental public static boolean enabled = false; - public static boolean compatModeEnabled = false; - public static int asyncEntityTrackerMaxThreads = 0; - public static int asyncEntityTrackerKeepalive = 60; - public static int asyncEntityTrackerQueueSize = 0; + public static int threads = 0; private static boolean asyncMultithreadedTrackerInitialized; @Override public void onLoaded() { config.addCommentRegionBased(getBasePath(), """ - Make entity tracking saving asynchronously, can improve performance significantly, - especially in some massive entities in small area situations.""", - """ + ** Experimental Feature ** + Make entity tracking asynchronously, can improve performance significantly, + especially in some massive entities in small area situations.""", """ + ** 实验性功能 ** 异步实体跟踪, 在实体数量多且密集的情况下效果明显."""); - config.addCommentRegionBased(getBasePath() + ".compat-mode", """ - 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.""", - """ - 是否启用兼容模式, - 如果你的服务器安装了 Citizens 或其他类似非发包 NPC 插件, 请开启此项."""); if (asyncMultithreadedTrackerInitialized) { config.getConfigSection(getBasePath()); @@ -39,27 +33,18 @@ public class MultithreadedTracker extends ConfigModules { } asyncMultithreadedTrackerInitialized = true; - enabled = config.getBoolean(getBasePath() + ".enabled", enabled); - compatModeEnabled = config.getBoolean(getBasePath() + ".compat-mode", compatModeEnabled); - asyncEntityTrackerMaxThreads = config.getInt(getBasePath() + ".max-threads", asyncEntityTrackerMaxThreads); - asyncEntityTrackerKeepalive = config.getInt(getBasePath() + ".keepalive", asyncEntityTrackerKeepalive); - asyncEntityTrackerQueueSize = config.getInt(getBasePath() + ".queue-size", asyncEntityTrackerQueueSize); - - 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); - - if (asyncEntityTrackerQueueSize <= 0) - asyncEntityTrackerQueueSize = asyncEntityTrackerMaxThreads * 384; - + enabled = config.getBoolean(getBasePath() + ".enabled", false); + threads = config.getInt(getBasePath() + ".threads", 0); + int aval = Runtime.getRuntime().availableProcessors(); + if (threads < 0) { + threads = aval + threads; + } else if (threads == 0) { + threads = Math.min(aval, 8); + } + threads = Math.max(threads, 1); if (enabled) { - org.dreeam.leaf.async.tracker.MultithreadedTracker.init(); + LeafConfig.LOGGER.info("Using {} threads for Async Entity Tracker", threads); + AsyncTracker.init(); } } } diff --git a/leaf-server/src/main/java/org/dreeam/leaf/util/EntitySlice.java b/leaf-server/src/main/java/org/dreeam/leaf/util/EntitySlice.java new file mode 100644 index 00000000..82286bd9 --- /dev/null +++ b/leaf-server/src/main/java/org/dreeam/leaf/util/EntitySlice.java @@ -0,0 +1,108 @@ +package org.dreeam.leaf.util; + +import net.minecraft.world.entity.Entity; +import org.jspecify.annotations.NullMarked; + +import java.util.Iterator; + +@NullMarked +public record EntitySlice(Entity[] array, int start, int end) implements Iterable { + public EntitySlice(final Entity[] entities) { + this(entities, 0, entities.length); + } + + public int size() { + return end - start; + } + + public boolean isEmpty() { + return start >= end; + } + + public Entity get(final int index) { + return array[start + index]; + } + + @Override + public Iterator iterator() { + return new SliceIterator(this); + } + + public EntitySlice[] splitEvenly(int parts) { + if (parts > size()) { + parts = size(); + } + if (parts <= 1) { + return new EntitySlice[]{this}; + } + + final EntitySlice[] result = new EntitySlice[parts]; + final int sliceSize = size(); + final int base = sliceSize / parts; + final int remainder = sliceSize % parts; + + int curr = start; + for (int i = 0; i < parts; i++) { + int endIdx = curr + base + (i < remainder ? 1 : 0); + result[i] = new EntitySlice(array, curr, endIdx); + curr = endIdx; + } + return result; + } + + public EntitySlice[] splitAt(final int index) { + final int m = start + index; + return new EntitySlice[]{ + new EntitySlice(array, start, m), + new EntitySlice(array, m, end) + }; + } + + public EntitySlice subSlice(final int startIndex, final int endIndex) { + return new EntitySlice(array, start + startIndex, start + endIndex); + } + + public EntitySlice subSlice(final int startIndex) { + return subSlice(startIndex, size()); + } + + public EntitySlice[] chunks(final int chunkSize) { + if (isEmpty() || chunkSize <= 0) { + return new EntitySlice[0]; + } + + final int len = (size() + chunkSize - 1) / chunkSize; + EntitySlice[] result = new EntitySlice[len]; + + int curr = start; + for (int i = 0; i < len; i++) { + final int endIdx = Math.min(curr + chunkSize, end); + result[i] = new EntitySlice(array, curr, endIdx); + curr = endIdx; + } + return result; + } + + private static final class SliceIterator implements Iterator { + private final EntitySlice slice; + private int current; + + public SliceIterator(EntitySlice slice) { + this.slice = slice; + this.current = slice.start; + } + + @Override + public boolean hasNext() { + return current < slice.end; + } + + @Override + public Entity next() { + if (!hasNext()) { + throw new IndexOutOfBoundsException(); + } + return slice.array[current++]; + } + } +} diff --git a/leaf-server/src/main/java/org/dreeam/leaf/util/queue/MpmcQueue.java b/leaf-server/src/main/java/org/dreeam/leaf/util/queue/MpmcQueue.java new file mode 100644 index 00000000..4bc7f8cb --- /dev/null +++ b/leaf-server/src/main/java/org/dreeam/leaf/util/queue/MpmcQueue.java @@ -0,0 +1,231 @@ +// Copyright (c) 2018 Aron Wieck Crown Communications GmbH, Karlsruhe, Germany +// Licensed under the terms of MIT license and the Apache License (Version 2.0). + +package org.dreeam.leaf.util.queue; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; + +public final class MpmcQueue { + private static final int MAX_IN_PROGRESS = 16; + private static final long DONE_MASK = 0x0000_0000_0000_FF00L; + private static final long PENDING_MASK = 0x0000_0000_0000_00FFL; + private static final long FAST_PATH_MASK = 0x00FF_FFFF_FFFF_FF00L; + private static final int MAX_CAPACITY = 1 << 30; + private static final int PARALLELISM = Runtime.getRuntime().availableProcessors(); + + private static final VarHandle READ; + private static final VarHandle WRITE; + + private final int mask; + private final T[] buffer; + @SuppressWarnings("unused") + private final Padded padded1 = new Padded(); + @SuppressWarnings("FieldMayBeFinal") + private volatile long reads = 0L; + @SuppressWarnings("unused") + private final Padded padded2 = new Padded(); + @SuppressWarnings("FieldMayBeFinal") + private volatile long writes = 0L; + + static { + try { + MethodHandles.Lookup l = MethodHandles.lookup(); + READ = l.findVarHandle(MpmcQueue.class, "reads", + long.class); + WRITE = l.findVarHandle(MpmcQueue.class, "writes", + long.class); + } catch (ReflectiveOperationException e) { + throw new ExceptionInInitializerError(e); + } + } + + public MpmcQueue(Class clazz, int capacity) { + if (capacity <= 0 || capacity > MAX_CAPACITY) { + throw new IllegalArgumentException(); + } + + this.mask = (1 << (Integer.SIZE - Integer.numberOfLeadingZeros(capacity - 1))) - 1; + //noinspection unchecked + this.buffer = (clazz == Object.class) + ? (T[]) new Object[mask + 1] + : (T[]) java.lang.reflect.Array.newInstance(clazz, mask + 1); + } + + private void spinWait(final int attempts) { + if (attempts == 0) { + } else if (PARALLELISM != 1 && (attempts & 31) != 31) { + Thread.onSpinWait(); + } else { + Thread.yield(); + } + } + + public boolean send(final T item) { + long write = (long) WRITE.getAcquire(this); + boolean success; + long newWrite = 0L; + int index = 0; + int attempts = 0; + while (true) { + spinWait(attempts++); + final int inProgressCnt = (int) (write & PENDING_MASK); + if ((((int) (write >>> 16) + 1) & mask) == (int) ((long) READ.getAcquire(this) >>> 16)) { + success = false; + break; + } + + if (inProgressCnt == MAX_IN_PROGRESS) { + write = (long) WRITE.getAcquire(this); + continue; + } + + index = ((int) (write >>> 16) + inProgressCnt) & mask; + + if (((index + 1) & mask) == (int) ((long) READ.getAcquire(this) >>> 16)) { + success = false; + break; + } + + newWrite = write + 1; + if (WRITE.weakCompareAndSetAcquire(this, write, newWrite)) { + success = true; + break; + } + write = (long) WRITE.getVolatile(this); + } + if (!success) { + return false; + } + buffer[index] = item; + if ((newWrite & FAST_PATH_MASK) == ((long) index << 16) && index < mask) { + WRITE.getAndAddRelease(this, (1L << 16) - 1); + } else { + write = newWrite; + while (true) { + final int inProcessCnt = (int) (write & PENDING_MASK); + final long n; + if (((int) ((write & DONE_MASK) >>> 8) + 1) == inProcessCnt) { + n = ((long) (((int) (write >>> 16) + inProcessCnt) & mask)) << 16; + } else if ((int) (write >>> 16) == index) { + n = (write + (1L << 16) - 1) & (((long) mask << 16) | 0xFFFFL); + } else { + n = write + (1L << 8); + } + + if (WRITE.weakCompareAndSetRelease(this, write, n)) { + break; + } + write = (long) WRITE.getVolatile(this); + spinWait(attempts++); + } + } + + return true; + } + + public T recv() { + long read = (long) READ.getAcquire(this); + boolean success; + int index = 0; + long newRead = 0L; + int attempts = 0; + while (true) { + spinWait(attempts++); + final int inProgressCnt = (int) (read & PENDING_MASK); + if ((int) (read >>> 16) == (int) ((long) WRITE.getAcquire(this) >>> 16)) { + success = false; + break; + } + + if (inProgressCnt == MAX_IN_PROGRESS) { + read = (long) READ.getAcquire(this); + continue; + } + + index = ((int) (read >>> 16) + inProgressCnt) & mask; + + if (index == (int) ((long) WRITE.getAcquire(this) >>> 16)) { + success = false; + break; + } + + newRead = read + 1; + if (READ.weakCompareAndSetAcquire(this, read, newRead)) { + success = true; + break; + } + read = (long) READ.getVolatile(this); + } + if (!success) { + return null; + } + final T result = buffer[index]; + buffer[index] = null; + if ((newRead & FAST_PATH_MASK) == ((long) index << 16) && index < mask) { + READ.getAndAddRelease(this, (1L << 16) - 1); + } else { + read = newRead; + while (true) { + final int inProcessCnt = (int) (read & PENDING_MASK); + final long n; + if (((int) ((read & DONE_MASK) >>> 8) + 1) == inProcessCnt) { + n = ((long) (((int) (read >>> 16) + inProcessCnt) & mask)) << 16; + } else if ((int) (read >>> 16) == index) { + n = (read + (1L << 16) - 1) & (((long) mask << 16) | 0xFFFFL); + } else { + n = read + (1L << 8); + } + + if (READ.weakCompareAndSetRelease(this, read, n)) { + break; + } + read = (long) READ.getVolatile(this); + spinWait(attempts++); + } + } + return result; + } + + public int length() { + final long readCounters = (long) READ.getVolatile(this); + final long writeCounters = (long) WRITE.getVolatile(this); + final int readIndex = (int) (readCounters >>> 16); + final int writeIndex = (int) (writeCounters >>> 16); + return (readIndex <= writeIndex ? + writeIndex - readIndex : + writeIndex + capacity() - readIndex) - (int) (readCounters & PENDING_MASK); + } + + public boolean isEmpty() { + return length() == 0; + } + + public int capacity() { + return buffer.length; + } + + public int remaining() { + final long readCounters = (long) READ.getVolatile(this); + final long writeCounters = (long) WRITE.getVolatile(this); + final int cap = capacity(); + final int readIndex = (int) (readCounters >>> 16); + final int writeIndex = (int) (writeCounters >>> 16); + final int len = readIndex <= writeIndex ? + writeIndex - readIndex : + writeIndex + cap - readIndex; + return cap - 1 - len - (int) (writeCounters & PENDING_MASK); + } + + @SuppressWarnings("unused") + private final static class Padded { + private byte i0, i1, i2, i3, i4, i5, i6, i7, i8, i9, i10, i11, i12, i13, i14, i15; + private byte j0, j1, j2, j3, j4, j5, j6, j7, j8, j9, j10, j11, j12, j13, j14, j15; + private byte k0, k1, k2, k3, k4, k5, k6, k7, k8, k9, k10, k11, k12, k13, k14, k15; + private byte l0, l1, l2, l3, l4, l5, l6, l7, l8, l9, l10, l11, l12, l13, l14, l15; + private byte m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15; + private byte n0, n1, n2, n3, n4, n5, n6, n7, n8, n9, n10, n11, n12, n13, n14, n15; + private byte o0, o1, o2, o3, o4, o5, o6, o7, o8, o9, o10, o11, o12, o13, o14, o15; + private byte p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15; + } +}