diff --git a/patches/server/0003-Sakura-Configuration-Files.patch b/patches/server/0003-Sakura-Configuration-Files.patch index 2ccb2d2..9500c30 100644 --- a/patches/server/0003-Sakura-Configuration-Files.patch +++ b/patches/server/0003-Sakura-Configuration-Files.patch @@ -649,10 +649,10 @@ index 0000000000000000000000000000000000000000..b5c2471a2982f5a5ab64620e90676646 +} diff --git a/src/main/java/me/samsuik/sakura/configuration/WorldConfiguration.java b/src/main/java/me/samsuik/sakura/configuration/WorldConfiguration.java new file mode 100644 -index 0000000000000000000000000000000000000000..f9fe1024c3e5681abe9823d71582d83b50d3c6b0 +index 0000000000000000000000000000000000000000..ff1b7f3ead3ab115fc53b90d9b4bec8fd9b18c0c --- /dev/null +++ b/src/main/java/me/samsuik/sakura/configuration/WorldConfiguration.java -@@ -0,0 +1,204 @@ +@@ -0,0 +1,209 @@ +package me.samsuik.sakura.configuration; + +import com.mojang.logging.LogUtils; @@ -800,6 +800,11 @@ index 0000000000000000000000000000000000000000..f9fe1024c3e5681abe9823d71582d83b + + @Comment("Prevents players swimming using elytra or riptide to enter holes") + public boolean posesShrinkCollisionBox = true; ++ ++ public EntityTracker entityTracker; ++ public class EntityTracker extends ConfigurationPart { ++ public boolean asyncTracking = true; ++ } + } + + public Entity entity; diff --git a/patches/server/0049-Reduce-entity-tracker-player-updates.patch b/patches/server/0049-Reduce-entity-tracker-player-updates.patch index 6225802..0ff98c6 100644 --- a/patches/server/0049-Reduce-entity-tracker-player-updates.patch +++ b/patches/server/0049-Reduce-entity-tracker-player-updates.patch @@ -5,7 +5,7 @@ Subject: [PATCH] Reduce entity tracker player updates diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java -index 8a817dda325898b759de63ab6e4300b050286bf7..baec8d775957f3ef3224d743f961f0a23aee92ab 100644 +index 8a817dda325898b759de63ab6e4300b050286bf7..64bf4398a8a0b429e5a7483cf8a24a02c58b7fb3 100644 --- a/src/main/java/net/minecraft/server/level/ChunkMap.java +++ b/src/main/java/net/minecraft/server/level/ChunkMap.java @@ -1145,6 +1145,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider @@ -35,16 +35,16 @@ index 8a817dda325898b759de63ab6e4300b050286bf7..baec8d775957f3ef3224d743f961f0a2 + this.entityPosition = entity.position(); } -+ final boolean shouldLookForPlayers() { ++ public final boolean shouldLookForPlayers() { + // We have to always update players otherwise they can turn invisible on teleports (why?) -+ if (entity instanceof net.minecraft.world.entity.player.Player || entity.tickCount % playerSearchInterval == 0) { ++ if (this.entity instanceof net.minecraft.world.entity.player.Player || this.entity.tickCount % this.playerSearchInterval == 0) { + return true; + } + -+ Vec3 lastPosition = entityPosition; -+ entityPosition = entity.position(); ++ Vec3 lastPosition = this.entityPosition; ++ this.entityPosition = this.entity.position(); + -+ return entity.position().distanceToSqr(lastPosition) >= (double) range / 2.0; ++ return this.entity.position().distanceToSqr(lastPosition) >= (double) this.range / 2.0; + } + // Sakura end - reduce entities looking for nearby players + diff --git a/patches/server/0071-Async-Entity-Tracking.patch b/patches/server/0071-Async-Entity-Tracking.patch new file mode 100644 index 0000000..f287ad3 --- /dev/null +++ b/patches/server/0071-Async-Entity-Tracking.patch @@ -0,0 +1,418 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Samsuik +Date: Mon, 27 May 2024 18:02:27 +0100 +Subject: [PATCH] Async Entity Tracking + + +diff --git a/src/main/java/me/samsuik/sakura/player/tracking/AsyncEntityTracker.java b/src/main/java/me/samsuik/sakura/player/tracking/AsyncEntityTracker.java +new file mode 100644 +index 0000000000000000000000000000000000000000..54422e6e4bf0860ea504414141821cca7c072b28 +--- /dev/null ++++ b/src/main/java/me/samsuik/sakura/player/tracking/AsyncEntityTracker.java +@@ -0,0 +1,101 @@ ++package me.samsuik.sakura.player.tracking; ++ ++import com.google.common.collect.Iterators; ++import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; ++import net.minecraft.server.level.ChunkMap; ++import org.jetbrains.annotations.NotNull; ++ ++import java.util.ArrayList; ++import java.util.Iterator; ++import java.util.List; ++import java.util.Set; ++import java.util.concurrent.ExecutorService; ++import java.util.concurrent.Executors; ++import java.util.concurrent.ThreadFactory; ++ ++public final class AsyncEntityTracker { ++ private static final int AVAILABLE_THREADS = Math.max(Runtime.getRuntime().availableProcessors() / 5 * 2, 1); // 0.4 ++ private static final ThreadFactory THREAD_FACTORY = EntityTrackerThread::new; ++ private static ExecutorService TRACKER_EXECUTOR = null; ++ ++ private final ChunkMap chunkMap; ++ private volatile boolean processingTick; ++ ++ public AsyncEntityTracker(ChunkMap chunkMap) { ++ this.chunkMap = chunkMap; ++ } ++ ++ private static void tryStartService() { ++ if (TRACKER_EXECUTOR == null) { ++ TRACKER_EXECUTOR = Executors.newFixedThreadPool(AVAILABLE_THREADS, THREAD_FACTORY); ++ } ++ } ++ ++ public void cycle() { ++ if (this.processingTick) { ++ return; // uh oh. ++ } ++ ++ tryStartService(); ++ this.processingTick = true; ++ ++ TrackedEntities trackedEntities = new TrackedEntities(this.chunkMap); ++ this.updatePlayersInParallel(trackedEntities); ++ ++ TRACKER_EXECUTOR.execute(() -> { ++ try { ++ this.processEntities(trackedEntities); ++ } finally { ++ this.processingTick = false; ++ } ++ }); ++ } ++ ++ private void updatePlayersInParallel(TrackedEntities trackedEntities) { ++ trackedEntities.getEntities().parallelStream().forEach(tracker -> { ++ if (!tracker.isActive) return; // removed entity ++ synchronized (tracker.entity) { ++ if (tracker.shouldLookForPlayers()) { ++ tracker.updatePlayers(tracker.entity.getPlayersInTrackRange()); ++ } ++ } ++ if (tracker.entity.isPassengersDirty.getAndSet(false)) { ++ trackedEntities.markEntityAsUpdated(tracker); ++ } ++ }); ++ } ++ ++ private void processEntities(TrackedEntities trackedEntities) { ++ for (ChunkMap.TrackedEntity tracker : trackedEntities) { ++ if (!tracker.isActive) continue; // removed entity ++ tracker.seenByLock.readLock().lock(); ++ synchronized (tracker.entity) { ++ tracker.serverEntity.sendChanges(); ++ } ++ tracker.seenByLock.readLock().unlock(); ++ } ++ } ++ ++ private static class TrackedEntities implements Iterable { ++ private final List entities; ++ private final Set alreadyUpdated; ++ ++ public TrackedEntities(ChunkMap chunkMap) { ++ this.entities = new ArrayList<>(chunkMap.entityMap.values()); ++ this.alreadyUpdated = new ReferenceOpenHashSet<>(); ++ } ++ ++ public void markEntityAsUpdated(ChunkMap.TrackedEntity trackedEntity) { ++ this.alreadyUpdated.add(trackedEntity); ++ } ++ ++ public List getEntities() { ++ return this.entities; ++ } ++ ++ @Override ++ public @NotNull Iterator iterator() { ++ return Iterators.filter(this.entities.iterator(), e -> !this.alreadyUpdated.contains(e)); ++ } ++ } ++} +diff --git a/src/main/java/me/samsuik/sakura/player/tracking/EntityTrackerThread.java b/src/main/java/me/samsuik/sakura/player/tracking/EntityTrackerThread.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a4736257bee2f2c6c51363ca54269d477ae42464 +--- /dev/null ++++ b/src/main/java/me/samsuik/sakura/player/tracking/EntityTrackerThread.java +@@ -0,0 +1,12 @@ ++package me.samsuik.sakura.player.tracking; ++ ++import java.util.concurrent.atomic.AtomicInteger; ++ ++public final class EntityTrackerThread extends Thread { ++ private static final AtomicInteger THREAD_COUNTER = new AtomicInteger(0); ++ ++ public EntityTrackerThread(Runnable runnable) { ++ super(runnable); ++ this.setName("Entity Tracker Thread " + THREAD_COUNTER.getAndIncrement()); ++ } ++} +diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java +index 64bf4398a8a0b429e5a7483cf8a24a02c58b7fb3..c9c1f69f942d58304ec592e62d678cf595905bb8 100644 +--- a/src/main/java/net/minecraft/server/level/ChunkMap.java ++++ b/src/main/java/net/minecraft/server/level/ChunkMap.java +@@ -1163,8 +1163,14 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + } + // Paper end - optimised tracker +- ++ // Sakura start - async entity tracking ++ private final me.samsuik.sakura.player.tracking.AsyncEntityTracker asyncEntityTracker = new me.samsuik.sakura.player.tracking.AsyncEntityTracker(this); + protected void tick() { ++ if (this.level.sakuraConfig().players.entityTracker.asyncTracking) { ++ this.asyncEntityTracker.cycle(); ++ return; ++ } ++ // Sakura end - async entity tracking + // Paper start - optimized tracker + if (true) { + this.processTrackQueue(); +@@ -1308,12 +1314,14 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + public class TrackedEntity { + + public final ServerEntity serverEntity; +- final Entity entity; ++ public final Entity entity; // Sakura - package-protected -> public + private final int range; + SectionPos lastSectionPos; + public final Set seenBy = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>(); // Paper - Perf: optimise map impl + private final int playerSearchInterval; // Sakura - reduce entity tracker player updates + private Vec3 entityPosition; // Sakura - reduce entity tracker player updates ++ public final java.util.concurrent.locks.ReentrantReadWriteLock seenByLock = new java.util.concurrent.locks.ReentrantReadWriteLock(); // Sakura - async entity tracking ++ public volatile boolean isActive = true; // Sakura - async entity tracking + + public TrackedEntity(Entity entity, int i, int j, boolean flag) { + this.serverEntity = new ServerEntity(ChunkMap.this.level, entity, j, flag, this::broadcast, this.seenBy); // CraftBukkit +@@ -1343,7 +1351,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + // Paper start - use distance map to optimise tracker + com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet lastTrackerCandidates; + +- final void updatePlayers(com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newTrackerCandidates) { ++ public final void updatePlayers(com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newTrackerCandidates) { // Sakura - async entity tracking + com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet oldTrackerCandidates = this.lastTrackerCandidates; + this.lastTrackerCandidates = newTrackerCandidates; + +@@ -1355,7 +1363,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + continue; + } + ServerPlayer player = (ServerPlayer)raw; +- this.updatePlayer(player); ++ this.sakura_updatePlayer(player); // Sakura - async entity tracking + } + } + +@@ -1370,7 +1378,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + + for (ServerPlayerConnection conn : this.seenBy.toArray(new ServerPlayerConnection[0])) { // avoid CME + if (newTrackerCandidates == null || !newTrackerCandidates.contains(conn.getPlayer())) { +- this.updatePlayer(conn.getPlayer()); ++ this.sakura_updatePlayer(conn.getPlayer()); // Sakura - async entity tracking + } + } + } +@@ -1412,18 +1420,26 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + this.serverEntity.removePairing(serverplayerconnection.getPlayer()); + } + ++ this.isActive = false; // Sakura - async entity tracking + } + + public void removePlayer(ServerPlayer player) { + org.spigotmc.AsyncCatcher.catchOp("player tracker clear"); // Spigot ++ this.seenByLock.writeLock().lock(); // Sakura - async entity tracking + if (this.seenBy.remove(player.connection)) { + this.serverEntity.removePairing(player); + } ++ this.seenByLock.writeLock().unlock(); // Sakura - async entity tracking + + } + + public void updatePlayer(ServerPlayer player) { + org.spigotmc.AsyncCatcher.catchOp("player tracker update"); // Spigot ++ // Sakura start - async entity tracking ++ this.sakura_updatePlayer(player); ++ } ++ private void sakura_updatePlayer(ServerPlayer player) { ++ // Sakura end - async entity tracking + if (player != this.entity) { + // Paper start - remove allocation of Vec3D here + // Vec3 vec3d = player.position().subtract(this.entity.position()); +@@ -1466,6 +1482,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + flag = false; + } + // CraftBukkit end ++ this.seenByLock.writeLock().lock(); // Sakura - async entity tracking + if (flag) { + if (this.seenBy.add(player.connection)) { + // Paper start - entity tracking events +@@ -1477,6 +1494,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } else if (this.seenBy.remove(player.connection)) { + this.serverEntity.removePairing(player); + } ++ this.seenByLock.writeLock().unlock(); // Sakura - async entity tracking + + } + } +diff --git a/src/main/java/net/minecraft/server/level/ServerEntity.java b/src/main/java/net/minecraft/server/level/ServerEntity.java +index b37f568fbd39be15f08e00f4ea5f28738a1d99fe..aab41f2bc51d2a3766170293e6262f9bb7d3072a 100644 +--- a/src/main/java/net/minecraft/server/level/ServerEntity.java ++++ b/src/main/java/net/minecraft/server/level/ServerEntity.java +@@ -94,6 +94,11 @@ public class ServerEntity { + } + + public void sendChanges() { ++ // Sakura start - async entity tracking ++ this.sendChanges(this.entity.isPassengersDirty.getAndSet(false)); ++ } ++ private void updateDirtyPassengers() { ++ // Sakura end - async entity tracking + List list = this.entity.getPassengers(); + + if (!list.equals(this.lastPassengers)) { +@@ -108,7 +113,13 @@ public class ServerEntity { + }); + this.lastPassengers = list; + } +- ++ // Sakura start - async entity tracking ++ } ++ public final void sendChanges(boolean isPassengersDirty) { ++ if (isPassengersDirty) { ++ this.updateDirtyPassengers(); ++ } ++ // Sakura end - async entity tracking + Entity entity = this.entity; + + if (!this.trackedPlayers.isEmpty() && entity instanceof ItemFrame) { // Paper - Perf: Only tick item frames if players can see it +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index 2a9a6a9f00343f614a0d2430095a17088861eb1f..8d27289aa81aa60ba6bdde57b74fc4a0a76df4e5 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -923,7 +923,11 @@ public class ServerLevel extends Level implements WorldGenLevel { + // Sakura end + + gameprofilerfiller.push("tick"); ++ // Sakura start - async entity tracking ++ synchronized (entity) { + this.guardEntityTick(this::tickNonPassenger, entity); ++ } ++ // Sakura end - async entity tracking + gameprofilerfiller.pop(); + } + } +diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java +index 08d33295967f66051b9e846d854ffac6fe885281..520f158dba52c22a33166e0ad88cffab31c426b4 100644 +--- a/src/main/java/net/minecraft/world/entity/Entity.java ++++ b/src/main/java/net/minecraft/world/entity/Entity.java +@@ -301,6 +301,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, S + private int id; + public boolean blocksBuilding; + public ImmutableList passengers; ++ public final java.util.concurrent.atomic.AtomicBoolean isPassengersDirty = new java.util.concurrent.atomic.AtomicBoolean(); // Sakura - aysnc entity tracking + protected int boardingCooldown; + @Nullable + private Entity vehicle; +@@ -3421,6 +3422,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, S + this.passengers = ImmutableList.copyOf(list); + } + ++ this.isPassengersDirty.set(true); // Sakura - async entity tracking + this.gameEvent(GameEvent.ENTITY_MOUNT, passenger); + } + } +@@ -3469,6 +3471,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, S + } + + entity.boardingCooldown = 60; ++ this.isPassengersDirty.set(true); // Sakura - async entity tracking + this.gameEvent(GameEvent.ENTITY_DISMOUNT, entity); + } + return true; // CraftBukkit +diff --git a/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java b/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java +index 45269115e63cfc3bd7dc740a5694e2cc7c35bcb1..fd782a72f934e2de943d664568ccc7293b980e69 100644 +--- a/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java ++++ b/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java +@@ -63,7 +63,7 @@ public class MapItemSavedData extends SavedData { + public final List carriedBy = Lists.newArrayList(); + public final Map carriedByPlayers = Maps.newHashMap(); + private final Map bannerMarkers = Maps.newHashMap(); +- public final Map decorations = Maps.newLinkedHashMap(); ++ public final Map decorations = java.util.Collections.synchronizedMap(Maps.newLinkedHashMap()); // Sakura - async entity tracking + private final Map frameMarkers = Maps.newHashMap(); + private int trackedDecorationCount; + private org.bukkit.craftbukkit.map.RenderData vanillaRender = new org.bukkit.craftbukkit.map.RenderData(); // Paper +@@ -584,6 +584,7 @@ public class MapItemSavedData extends SavedData { + // Paper start + private void addSeenPlayers(java.util.Collection icons) { + org.bukkit.entity.Player player = (org.bukkit.entity.Player) this.player.getBukkitEntity(); ++ synchronized (MapItemSavedData.this.decorations) { // Sakura - async entity tracking + MapItemSavedData.this.decorations.forEach((name, mapIcon) -> { + // If this cursor is for a player check visibility with vanish system + org.bukkit.entity.Player other = org.bukkit.Bukkit.getPlayerExact(name); // Spigot +@@ -591,6 +592,7 @@ public class MapItemSavedData extends SavedData { + icons.add(mapIcon); + } + }); ++ } // Sakura - async entity tracking + } + private boolean shouldUseVanillaMap() { + return mapView.getRenderers().size() == 1 && mapView.getRenderers().get(0).getClass() == org.bukkit.craftbukkit.map.CraftMapRenderer.class; +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +index 4731d10dd5e493af9564d38d8bf1ff223390bd75..b1904549b1b1180ebae822e63871f6e80d9100cc 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +@@ -715,9 +715,11 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { + ChunkMap.TrackedEntity entityTracker = world.getChunkSource().chunkMap.entityMap.get(this.getEntityId()); + + if (entityTracker != null) { ++ entityTracker.seenByLock.readLock().lock(); // Sakura - async entity tracking + for (ServerPlayerConnection connection : entityTracker.seenBy) { + players.add(connection.getPlayer().getBukkitEntity()); + } ++ entityTracker.seenByLock.readLock().unlock(); // Sakura - async entity tracking + } + + return players.build(); +@@ -1013,9 +1015,11 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { + } + + // Paper start, resend possibly desynced entity instead of add entity packet ++ entityTracker.seenByLock.readLock().lock(); // Sakura - async entity tracking + for (ServerPlayerConnection playerConnection : entityTracker.seenBy) { + this.getHandle().getEntityData().resendPossiblyDesyncedEntity(playerConnection.getPlayer()); + } ++ entityTracker.seenByLock.readLock().unlock(); // Sakura - async entity tracking + // Paper end + } + +@@ -1183,10 +1187,12 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { + return java.util.Collections.emptySet(); + } + ++ this.entity.tracker.seenByLock.readLock().lock(); // Sakura - async entity tracking + Set set = new java.util.HashSet<>(this.entity.tracker.seenBy.size()); + for (net.minecraft.server.network.ServerPlayerConnection connection : this.entity.tracker.seenBy) { + set.add(connection.getPlayer().getBukkitEntity().getPlayer()); + } ++ this.entity.tracker.seenByLock.readLock().unlock(); // Sakura - async entity tracking + return set; + } + // Paper end - tracked players API +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +index 29f0c4c3fd9185bf8768572c135b50a9db34dbbe..cd462589ab90bf8dfd4a213f49c1257c8152e137 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +@@ -2060,9 +2060,11 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + } + + ChunkMap.TrackedEntity entry = tracker.entityMap.get(other.getId()); ++ entry.seenByLock.readLock().lock(); // Sakura - async entity tracking + if (entry != null && !entry.seenBy.contains(this.getHandle().connection)) { + entry.updatePlayer(this.getHandle()); + } ++ entry.seenByLock.readLock().unlock(); // Sakura - async entity tracking + + this.server.getPluginManager().callEvent(new PlayerShowEntityEvent(this, entity)); + } +diff --git a/src/main/java/org/bukkit/craftbukkit/map/CraftMapRenderer.java b/src/main/java/org/bukkit/craftbukkit/map/CraftMapRenderer.java +index 15e9dd8844f893de5e8372b847c9e8295d6f69ca..8948474a27b70ed00a2eb0fc6db3beadd2a083ee 100644 +--- a/src/main/java/org/bukkit/craftbukkit/map/CraftMapRenderer.java ++++ b/src/main/java/org/bukkit/craftbukkit/map/CraftMapRenderer.java +@@ -34,6 +34,7 @@ public class CraftMapRenderer extends MapRenderer { + cursors.removeCursor(cursors.getCursor(0)); + } + ++ synchronized (this.worldMap.decorations) { // Sakura - async entity tracking + for (String key : this.worldMap.decorations.keySet()) { + // If this cursor is for a player check visibility with vanish system + Player other = Bukkit.getPlayerExact((String) key); +@@ -44,6 +45,7 @@ public class CraftMapRenderer extends MapRenderer { + MapDecoration decoration = this.worldMap.decorations.get(key); + cursors.addCursor(decoration.x(), decoration.y(), (byte) (decoration.rot() & 15), decoration.type().getIcon(), true, decoration.name() == null ? null : io.papermc.paper.adventure.PaperAdventure.asAdventure(decoration.name())); // Paper + } ++ } // Sakura - async entity tracking + } + + }