From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Mykyta Komarn Date: Sat, 3 Oct 2020 22:40:00 -0700 Subject: [PATCH] Async entity tracking Entity tracking is done the same way, but asynchronously. Lowers the MSPT and NSPT by a lot. The changes made: Synchronised the trackedEntities map in PlayerChunkMap, also added additional lock to it. Created a tracker executor which is processing the track queue asynchronously. A minor change was to replace 2 array lists with our GlueList implementation. Certain stuff had to be ensured to be called on the main thread when the tracker entry was ticked due to how the game's code works and due to how bukkit's event system works. Entity tracking is some of the heavy stuff the server does. Making this async is trivial for making the performance go up. Co-authored-by: Ivan Pekov diff --git a/src/main/java/net/minecraft/server/EntityPlayer.java b/src/main/java/net/minecraft/server/EntityPlayer.java index b43b02c0bdd5dbf0b7d30de90bdc2f74c015ecc8..d3d1120395ee0e5be781febefa502c40ad9dacdd 100644 --- a/src/main/java/net/minecraft/server/EntityPlayer.java +++ b/src/main/java/net/minecraft/server/EntityPlayer.java @@ -49,6 +49,7 @@ public class EntityPlayer extends EntityHuman implements ICrafting { public final MinecraftServer server; public final PlayerInteractManager playerInteractManager; public final Deque removeQueue = new ArrayDeque<>(); // Paper + public final java.util.concurrent.locks.Lock removeQueueLock = new java.util.concurrent.locks.ReentrantLock(); // Yatopia private final AdvancementDataPlayer advancementDataPlayer; private final ServerStatisticManager serverStatisticManager; private float lastHealthScored = Float.MIN_VALUE; @@ -459,6 +460,10 @@ public class EntityPlayer extends EntityHuman implements ICrafting { this.activeContainer = this.defaultContainer; } + // Yatopia start + removeQueueLock.lock(); + try { + // Yatopia end while (!this.removeQueue.isEmpty()) { int i = Math.min(this.removeQueue.size(), Integer.MAX_VALUE); int[] aint = new int[i]; @@ -479,6 +484,9 @@ public class EntityPlayer extends EntityHuman implements ICrafting { this.playerConnection.sendPacket(new PacketPlayOutEntityDestroy(aint)); } + } finally { // Yatopia start + removeQueueLock.unlock(); + } // Yatopia end Entity entity = this.getSpecatorTarget(); @@ -1545,9 +1553,16 @@ public class EntityPlayer extends EntityHuman implements ICrafting { // Applies to the same player, so we need to not duplicate our removal queue. The rest of this method does "resetting" // type logic so it does need to be called, maybe? This is silly. // this.removeQueue.addAll(entityplayer.removeQueue); + // Yatopia start + removeQueueLock.lock(); + try { + // Yatopia end if (this.removeQueue != entityplayer.removeQueue) { this.removeQueue.addAll(entityplayer.removeQueue); } + } finally { // Yatopia start + removeQueueLock.unlock(); + } // Yatopia end // Paper end this.cd = entityplayer.cd; this.ci = entityplayer.ci; @@ -1733,13 +1748,27 @@ public class EntityPlayer extends EntityHuman implements ICrafting { if (entity instanceof EntityHuman) { this.playerConnection.sendPacket(new PacketPlayOutEntityDestroy(new int[]{entity.getId()})); } else { + // Yatopia start + removeQueueLock.lock(); + try { + // Yatopia end this.removeQueue.add((Integer) entity.getId()); // CraftBukkit - decompile error + } finally { // Yatopia start + removeQueueLock.unlock(); + } // Yatopia end } } public void d(Entity entity) { + // Yatopia start + removeQueueLock.lock(); + try { + // Yatopia end this.removeQueue.remove((Integer) entity.getId()); // CraftBukkit - decompile error + } finally { // Yatopia start + removeQueueLock.unlock(); + } // Yatopia end } @Override @@ -1801,10 +1830,18 @@ public class EntityPlayer extends EntityHuman implements ICrafting { this.getBukkitEntity().teleport(new Location(newSpectatorTarget.getWorld().getWorld(), newSpectatorTarget.locX(), newSpectatorTarget.locY(), newSpectatorTarget.locZ(), this.yaw, this.pitch), TeleportCause.SPECTATE); // Correctly handle cross-world entities from api calls by using CB teleport // Make sure we're tracking the entity before sending - PlayerChunkMap.EntityTracker tracker = ((WorldServer)newSpectatorTarget.world).getChunkProvider().playerChunkMap.trackedEntities.get(newSpectatorTarget.getId()); + // Yatopia start + PlayerChunkMap chunkMap = ((WorldServer)newSpectatorTarget.world).getChunkProvider().playerChunkMap; + chunkMap.trackedEntitiesLock.lock(); + try { + PlayerChunkMap.EntityTracker tracker = chunkMap.trackedEntities.get(newSpectatorTarget.getId()); + // Yatopia end if (tracker != null) { // dumb plugins... tracker.updatePlayer(this); } + } finally { // Yatopia start + chunkMap.trackedEntitiesLock.unlock(); + } // Yatopia end } else { this.playerConnection.teleport(this.spectatedEntity.locX(), this.spectatedEntity.locY(), this.spectatedEntity.locZ(), this.yaw, this.pitch, TeleportCause.SPECTATE); // CraftBukkit } diff --git a/src/main/java/net/minecraft/server/EntityTNTPrimed.java b/src/main/java/net/minecraft/server/EntityTNTPrimed.java index 3e71332c47000b21ff90aec6937f90dc639a41bd..b12aa1b3bf1397eea42cffb1c67c03448a56409b 100644 --- a/src/main/java/net/minecraft/server/EntityTNTPrimed.java +++ b/src/main/java/net/minecraft/server/EntityTNTPrimed.java @@ -87,18 +87,33 @@ public class EntityTNTPrimed extends Entity { */ // Send position and velocity updates to nearby players on every tick while the TNT is in water. // This does pretty well at keeping their clients in sync with the server. - PlayerChunkMap.EntityTracker ete = ((WorldServer)this.world).getChunkProvider().playerChunkMap.trackedEntities.get(this.getId()); + // Yatopia start + PlayerChunkMap chunkMap = ((WorldServer)this.world).getChunkProvider().playerChunkMap; + chunkMap.trackedEntitiesLock.lock(); + try { + PlayerChunkMap.EntityTracker ete = chunkMap.trackedEntities.get(this.getId()); + // Yatopia end if (ete != null) { PacketPlayOutEntityVelocity velocityPacket = new PacketPlayOutEntityVelocity(this); PacketPlayOutEntityTeleport positionPacket = new PacketPlayOutEntityTeleport(this); + // Yatopia start + ete.trackedPlayersLock.lock(); + try { + // Yatopia end ete.trackedPlayers.stream() .filter(viewer -> (viewer.locX() - this.locX()) * (viewer.locY() - this.locY()) * (viewer.locZ() - this.locZ()) < 16 * 16) .forEach(viewer -> { viewer.playerConnection.sendPacket(velocityPacket); viewer.playerConnection.sendPacket(positionPacket); }); + } finally { // Yatopia start + ete.trackedPlayersLock.unlock(); + } // Yatopia end } + } finally { // Yatopia start + chunkMap.trackedEntitiesLock.unlock(); + } // Yatopia end } // Paper end } diff --git a/src/main/java/net/minecraft/server/EntityTrackerEntry.java b/src/main/java/net/minecraft/server/EntityTrackerEntry.java index aea72b0db10eed151db18490c02f291c3cded92a..8e35ada472e9ba2e896c83cbbbf9a093063e6a65 100644 --- a/src/main/java/net/minecraft/server/EntityTrackerEntry.java +++ b/src/main/java/net/minecraft/server/EntityTrackerEntry.java @@ -39,6 +39,7 @@ public class EntityTrackerEntry { private boolean r; // CraftBukkit start final Set trackedPlayers; // Paper - private -> package + final java.util.concurrent.locks.Lock trackedPlayersLock = new java.util.concurrent.locks.ReentrantLock(); // Yatopia // Paper start private java.util.Map trackedPlayerMap = null; @@ -74,7 +75,7 @@ public class EntityTrackerEntry { public final void tick() { this.a(); } // Paper - OBFHELPER public void a() { - com.tuinity.tuinity.util.TickThread.softEnsureTickThread("Tracker update"); // Tuinity + // com.tuinity.tuinity.util.TickThread.softEnsureTickThread("Tracker update"); // Tuinity // Yatopia List list = this.tracker.getPassengers(); if (!list.equals(this.p)) { @@ -89,18 +90,25 @@ public class EntityTrackerEntry { if (this.tickCounter % 10 == 0 && itemstack.getItem() instanceof ItemWorldMap) { // CraftBukkit - Moved this.tickCounter % 10 logic here so item frames do not enter the other blocks WorldMap worldmap = ItemWorldMap.getSavedMap(itemstack, this.b); + trackedPlayersLock.lock(); // Yatopia + try { // Yatopia Iterator iterator = this.trackedPlayers.iterator(); // CraftBukkit while (iterator.hasNext()) { EntityPlayer entityplayer = (EntityPlayer) iterator.next(); + MCUtil.ensureMain(() -> { // Yatopia - ensure worldmap updates are done on the main thread worldmap.a((EntityHuman) entityplayer, itemstack); + }); // Yatopia Packet packet = ((ItemWorldMap) itemstack.getItem()).a(itemstack, (World) this.b, (EntityHuman) entityplayer); if (packet != null) { entityplayer.playerConnection.sendPacket(packet); } } + } finally { // Yatopia start + trackedPlayersLock.unlock(); + } // Yatopia end } this.c(); @@ -240,6 +248,7 @@ public class EntityTrackerEntry { ++this.tickCounter; if (this.tracker.velocityChanged) { // CraftBukkit start - Create PlayerVelocity event + MCUtil.ensureMain(() -> { // Yatopia - ensure this is called on the main thread boolean cancelled = false; if (this.tracker instanceof EntityPlayer) { @@ -259,6 +268,7 @@ public class EntityTrackerEntry { if (!cancelled) { this.broadcastIncludingSelf(new PacketPlayOutEntityVelocity(this.tracker)); } + }); // Yatopia // CraftBukkit end this.tracker.velocityChanged = false; } @@ -282,7 +292,9 @@ public class EntityTrackerEntry { // Purpur end public void a(EntityPlayer entityplayer) { + MCUtil.ensureMain(() -> { // Yatopia - ensure main this.tracker.c(entityplayer); + }); // Yatopia entityplayer.c(this.tracker); } @@ -291,7 +303,9 @@ public class EntityTrackerEntry { entityplayer.playerConnection.getClass(); this.a(playerconnection::sendPacket, entityplayer); // CraftBukkit - add player + MCUtil.ensureMain(() -> { // Yatopia - ensure main this.tracker.b(entityplayer); + }); // Yatopia entityplayer.d(this.tracker); } diff --git a/src/main/java/net/minecraft/server/PlayerChunkMap.java b/src/main/java/net/minecraft/server/PlayerChunkMap.java index 6c8cb39ac8786734cda994ef29ba74c685f3b9be..9cde23731ca8e22b5263784e65c7efb0ce5312f8 100644 --- a/src/main/java/net/minecraft/server/PlayerChunkMap.java +++ b/src/main/java/net/minecraft/server/PlayerChunkMap.java @@ -11,6 +11,7 @@ import com.google.common.collect.Sets; import com.mojang.datafixers.DataFixer; import com.mojang.datafixers.util.Either; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectMaps; // Yatopia import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.longs.Long2ByteMap; import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap; @@ -113,10 +114,12 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { private final File w; private final PlayerMap playerMap; public final Int2ObjectMap trackedEntities; + public final java.util.concurrent.locks.Lock trackedEntitiesLock = new java.util.concurrent.locks.ReentrantLock(); // Yatopia private final Long2ByteMap z; private final Queue A; private final Queue getUnloadQueueTasks() { return this.A; } // Paper - OBFHELPER int viewDistance; // Paper - private -> package private public final com.destroystokyo.paper.util.PlayerMobDistanceMap playerMobDistanceMap; // Paper + private static final java.util.concurrent.ExecutorService trackerExecutor = java.util.concurrent.Executors.newCachedThreadPool(new com.google.common.util.concurrent.ThreadFactoryBuilder().setDaemon(true).setNameFormat("Entity Tracker - %d").build()); // Yatopia // CraftBukkit start - recursion-safe executor for Chunk loadCallback() and unloadCallback() public final CallbackExecutor callbackExecutor = new CallbackExecutor(); @@ -301,7 +304,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { this.unloadQueue = new LongOpenHashSet(); this.u = new AtomicInteger(); this.playerMap = new PlayerMap(); - this.trackedEntities = new Int2ObjectOpenHashMap(); + this.trackedEntities = Int2ObjectMaps.synchronize(new Int2ObjectOpenHashMap<>()); // Yatopia this.z = new Long2ByteOpenHashMap(); this.A = new com.destroystokyo.paper.utils.CachedSizeConcurrentLinkedQueue<>(); // Paper - need constant-time size() this.definedStructureManager = definedstructuremanager; @@ -2008,6 +2011,8 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { protected void addEntity(Entity entity) { org.spigotmc.AsyncCatcher.catchOp("entity track"); // Spigot // Paper start - ignore and warn about illegal addEntity calls instead of crashing server + trackedEntitiesLock.lock(); // Yatopia + try { // Yatopia if (!entity.valid || entity.world != this.world || this.trackedEntities.containsKey(entity.getId())) { new Throwable("[ERROR] Illegal PlayerChunkMap::addEntity for world " + this.world.getWorld().getName() + ": " + entity + (this.trackedEntities.containsKey(entity.getId()) ? " ALREADY CONTAINED (This would have crashed your server)" : "")) @@ -2047,10 +2052,15 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { } } + } finally { // Yatopia start + trackedEntitiesLock.unlock(); + } // Yatopia end } protected void removeEntity(Entity entity) { org.spigotmc.AsyncCatcher.catchOp("entity untrack"); // Spigot + trackedEntitiesLock.lock(); // Yatopia + try { // Yatopia if (entity instanceof EntityPlayer) { EntityPlayer entityplayer = (EntityPlayer) entity; @@ -2069,6 +2079,9 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { if (playerchunkmap_entitytracker1 != null) { playerchunkmap_entitytracker1.a(); } + } finally { // Yatopia start + trackedEntitiesLock.unlock(); + } // Yatopia end entity.tracker = null; // Paper - We're no longer tracked } @@ -2077,12 +2090,14 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { this.world.timings.tracker1.startTiming(); try { com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet.Iterator iterator = this.world.getChunkProvider().entityTickingChunks.iterator(); + trackedEntitiesLock.lock(); // Yatopia try { while (iterator.hasNext()) { Chunk chunk = iterator.next(); Entity[] entities = chunk.entities.getRawData(); for (int i = 0, len = chunk.entities.size(); i < len; ++i) { Entity entity = entities[i]; + if (entity == null) continue; // Yatopia EntityTracker tracker = this.trackedEntities.get(entity.getId()); if (tracker != null) { tracker.updatePlayers(tracker.tracker.getPlayersInTrackRange()); @@ -2092,6 +2107,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { } } finally { iterator.finishedIterating(); + trackedEntitiesLock.unlock(); // Yatopia } } finally { this.world.timings.tracker1.stopTiming(); @@ -2099,10 +2115,10 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { } // Paper end - optimised tracker - protected void g() { + protected void g() { // Yatopia - diff on change - make sure paper didn't drop tracker optimizations, otherwise kaboom // Paper start - optimized tracker if (true) { - this.processTrackQueue(); + trackerExecutor.execute(this::processTrackQueue); // Yatopia return; } // Paper end - optimized tracker @@ -2146,20 +2162,30 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { } protected void broadcast(Entity entity, Packet packet) { + trackedEntitiesLock.lock(); // Yatopia + try { // Yatopia PlayerChunkMap.EntityTracker playerchunkmap_entitytracker = (PlayerChunkMap.EntityTracker) this.trackedEntities.get(entity.getId()); if (playerchunkmap_entitytracker != null) { playerchunkmap_entitytracker.broadcast(packet); } + } finally { // Yatopia start + trackedEntitiesLock.unlock(); + } // Yatopia end } protected void broadcastIncludingSelf(Entity entity, Packet packet) { + trackedEntitiesLock.lock(); // Yatopia + try { // Yatopia PlayerChunkMap.EntityTracker playerchunkmap_entitytracker = (PlayerChunkMap.EntityTracker) this.trackedEntities.get(entity.getId()); if (playerchunkmap_entitytracker != null) { playerchunkmap_entitytracker.broadcastIncludingSelf(packet); } + } finally { // Yatopia start + trackedEntitiesLock.unlock(); + } // Yatopia end } @@ -2272,11 +2298,13 @@ Sections go from 0..16. Now whenever a section is not empty, it can potentially entityplayer.a(chunk.getPos(), apacket[0], apacket[1]); PacketDebug.a(this.world, chunk.getPos()); - List list = Lists.newArrayList(); - List list1 = Lists.newArrayList(); + List list = new net.yatopia.server.list.GlueList<>(); // Yatopia + List list1 = new net.yatopia.server.list.GlueList<>(); // Yatopia // Paper start - optimise entity tracker // use the chunk entity list, not the whole trackedEntities map... Entity[] entities = chunk.entities.getRawData(); + trackedEntitiesLock.lock(); // Yatopia + try { // Yatopia for (int i = 0, size = chunk.entities.size(); i < size; ++i) { Entity entity = entities[i]; if (entity == entityplayer) { @@ -2298,6 +2326,9 @@ Sections go from 0..16. Now whenever a section is not empty, it can potentially list1.add(entity); } } + } finally { // Yatopia start + trackedEntitiesLock.unlock(); + } // Yatopia end // Paper end - optimise entity tracker Iterator iterator; @@ -2345,6 +2376,7 @@ Sections go from 0..16. Now whenever a section is not empty, it can potentially // their first update (which is forced to have absolute coordinates), false afterward. public java.util.Map trackedPlayerMap = new java.util.HashMap<>(); public Set trackedPlayers = trackedPlayerMap.keySet(); + public java.util.concurrent.locks.Lock trackedPlayersLock = new java.util.concurrent.locks.ReentrantLock(); // Yatopia public EntityTracker(Entity entity, int i, int j, boolean flag) { this.trackerEntry = new EntityTrackerEntry(PlayerChunkMap.this.world, entity, j, flag, this::broadcast, trackedPlayerMap); // CraftBukkit // Paper @@ -2381,11 +2413,18 @@ Sections go from 0..16. Now whenever a section is not empty, it can potentially // stuff could have been removed, so we need to check the trackedPlayers set // for players that were removed + // Yatopia start + trackedPlayersLock.lock(); + try { + // Yatopia end for (EntityPlayer player : this.trackedPlayers.toArray(new EntityPlayer[0])) { // avoid CME if (newTrackerCandidates == null || !newTrackerCandidates.contains(player)) { this.updatePlayer(player); } } + } finally { // Yatopia start + trackedPlayersLock.unlock(); + } // Yatopia end } // Paper end - use distance map to optimise tracker @@ -2398,6 +2437,10 @@ Sections go from 0..16. Now whenever a section is not empty, it can potentially } public void broadcast(Packet packet) { + // Yatopia start + trackedPlayersLock.lock(); + try { + // Yatopia end Iterator iterator = this.trackedPlayers.iterator(); while (iterator.hasNext()) { @@ -2405,6 +2448,9 @@ Sections go from 0..16. Now whenever a section is not empty, it can potentially entityplayer.playerConnection.sendPacket(packet); } + } finally { // Yatopia start + trackedPlayersLock.unlock(); + } // Yatopia end } @@ -2417,6 +2463,10 @@ Sections go from 0..16. Now whenever a section is not empty, it can potentially } public void a() { + // Yatopia start + trackedPlayersLock.lock(); + try { + // Yatopia end Iterator iterator = this.trackedPlayers.iterator(); while (iterator.hasNext()) { @@ -2424,19 +2474,29 @@ Sections go from 0..16. Now whenever a section is not empty, it can potentially this.trackerEntry.a(entityplayer); } + } finally { // Yatopia start + trackedPlayersLock.unlock(); + } // Yatopia end } public void clear(EntityPlayer entityplayer) { org.spigotmc.AsyncCatcher.catchOp("player tracker clear"); // Spigot + // Yatopia start + trackedPlayersLock.lock(); + try { + // Yatopia end if (this.trackedPlayers.remove(entityplayer)) { this.trackerEntry.a(entityplayer); } + } finally { // Yatopia start + trackedPlayersLock.unlock(); + } // Yatopia end } public void updatePlayer(EntityPlayer entityplayer) { - org.spigotmc.AsyncCatcher.catchOp("player tracker update"); // Spigot + // org.spigotmc.AsyncCatcher.catchOp("player tracker update"); // Spigot // Yatopia if (entityplayer != this.tracker) { // Paper start - remove allocation of Vec3D here //Vec3D vec3d = entityplayer.getPositionVector().d(this.tracker.getPositionVector()); // MC-155077, SPIGOT-5113 @@ -2447,6 +2507,10 @@ Sections go from 0..16. Now whenever a section is not empty, it can potentially int i = Math.min(this.b(), (PlayerChunkMap.this.viewDistance - 1) * 16); boolean flag = vec3d_dx >= (double) (-i) && vec3d_dx <= (double) i && vec3d_dz >= (double) (-i) && vec3d_dz <= (double) i && this.tracker.a(entityplayer); // Paper - remove allocation of Vec3D here + // Yatopia start + trackedPlayersLock.lock(); + try { + // Yatopia end if (flag) { boolean flag1 = this.tracker.attachedToPlayer; @@ -2467,7 +2531,14 @@ Sections go from 0..16. Now whenever a section is not empty, it can potentially } } + // Yatopia start + entityplayer.removeQueueLock.lock(); + try { + // Yatopia end entityplayer.removeQueue.remove(Integer.valueOf(this.tracker.getId())); + } finally { // Yatopia start + entityplayer.removeQueueLock.unlock(); + } // Yatopia end // CraftBukkit end if (flag1 && this.trackedPlayerMap.putIfAbsent(entityplayer, true) == null) { // Paper @@ -2476,6 +2547,9 @@ Sections go from 0..16. Now whenever a section is not empty, it can potentially } else if (this.trackedPlayers.remove(entityplayer)) { this.trackerEntry.a(entityplayer); } + } finally { // Yatopia start + trackedPlayersLock.unlock(); + } // Yatopia end } } diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java index 7e91e95941039cce630ed3eb88e1919e1d08c091..e87c02eec6885805a86e67fada0cf033ced5f304 100644 --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java @@ -1253,10 +1253,16 @@ public class CraftPlayer extends CraftHumanEntity implements Player { private void unregisterPlayer(EntityPlayer other) { PlayerChunkMap tracker = ((WorldServer) entity.world).getChunkProvider().playerChunkMap; // Paper end + // Yatopia start + tracker.trackedEntitiesLock.lock(); + try { // Yatopia end PlayerChunkMap.EntityTracker entry = tracker.trackedEntities.get(other.getId()); if (entry != null) { entry.clear(getHandle()); } + } finally { // Yatopia start + tracker.trackedEntitiesLock.unlock(); + } // Yatopia end // Remove the hidden player from this player user list, if they're on it if (other.sentListPacket) { @@ -1303,10 +1309,25 @@ public class CraftPlayer extends CraftHumanEntity implements Player { getHandle().playerConnection.sendPacket(new PacketPlayOutPlayerInfo(PacketPlayOutPlayerInfo.EnumPlayerInfoAction.ADD_PLAYER, other)); + // Yatopia start + tracker.trackedEntitiesLock.lock(); + try { // Yatopia end PlayerChunkMap.EntityTracker entry = tracker.trackedEntities.get(other.getId()); - if (entry != null && !entry.trackedPlayers.contains(getHandle())) { + if (entry != null) { // Yatopia - check moved down + // Yatopia start + entry.trackedPlayersLock.lock(); + try { + if (!entry.trackedPlayers.contains(getHandle())) { entry.updatePlayer(getHandle()); + } + } finally { + entry.trackedPlayersLock.unlock(); + } + // Yatopia end } + } finally { // Yatopia start + tracker.trackedEntitiesLock.unlock(); + } // Yatopia end } // Paper start private void reregisterPlayer(EntityPlayer player) {