diff --git a/patches/api/0003-Tuinity-View-distance-api.patch b/patches/api/0003-Tuinity-View-distance-api.patch new file mode 100644 index 000000000..8b55e7c1f --- /dev/null +++ b/patches/api/0003-Tuinity-View-distance-api.patch @@ -0,0 +1,85 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?=E3=84=97=E3=84=A0=CB=8B=20=E3=84=91=E3=84=A7=CB=8A?= + +Date: Sat, 18 Apr 2020 19:53:16 +0800 +Subject: [PATCH] Tuinity View distance api + +Allow setting a view distance where entities and chunks do not tick. +This could allow some servers to look like they have a higher view +distance than they actually have. + +Also add World#setViewDistance + +diff --git a/src/main/java/org/bukkit/World.java b/src/main/java/org/bukkit/World.java +index db18f70ec37253232fb2cfd08ccd07d13c7c457d..be92b1a3e062cb7c54d5a1ea83ea22437b8562d4 100644 +--- a/src/main/java/org/bukkit/World.java ++++ b/src/main/java/org/bukkit/World.java +@@ -3166,6 +3166,34 @@ public interface World extends PluginMessageRecipient, Metadatable { + int getViewDistance(); + // Spigot end + ++ // Tuinity start - view distance api ++ /** ++ * Sets the view distance for this world. ++ * @param viewDistance view distance in [2, 32] ++ */ ++ void setViewDistance(int viewDistance); ++ ++ /** ++ * Returns the no-tick view distance for this world. ++ *

++ * No-tick view distance is the view distance where chunks will load, however the chunks and their entities will not ++ * be set to tick. ++ *

++ * @return The no-tick view distance for this world. ++ */ ++ int getNoTickViewDistance(); ++ ++ /** ++ * Sets the no-tick view distance for this world. ++ *

++ * No-tick view distance is the view distance where chunks will load, however the chunks and their entities will not ++ * be set to tick. ++ *

++ * @param viewDistance view distance in [2, 32] ++ */ ++ void setNoTickViewDistance(int viewDistance); ++ // Tuinity end - view distance api ++ + // Spigot start + public class Spigot { + +diff --git a/src/main/java/org/bukkit/entity/Player.java b/src/main/java/org/bukkit/entity/Player.java +index 787bb144b5918ef1e46d69cb72b84128d2b678f3..c542305c69da3b9e5c2485e3c0cdf7ec60b1239b 100644 +--- a/src/main/java/org/bukkit/entity/Player.java ++++ b/src/main/java/org/bukkit/entity/Player.java +@@ -1732,6 +1732,29 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM + T getClientOption(@NotNull ClientOption option); + // Paper end + ++ // Tuinity start ++ /** ++ * Returns the no-tick view distance for this player. ++ *

++ * No-tick view distance is the view distance where chunks will load, however the chunks and their entities will not ++ * be set to tick. ++ *

++ * @return The no-tick view distance for this player. ++ */ ++ int getNoTickViewDistance(); ++ ++ /** ++ * Sets the no-tick view distance for this player. Setting to a value of -1 will default to the world no-tick ++ * view distance. ++ *

++ * No-tick view distance is the view distance where chunks will load, however the chunks and their entities will not ++ * be set to tick. ++ *

++ * @param viewDistance view distance in [2, 32] or -1 ++ */ ++ void setNoTickViewDistance(int viewDistance); ++ // Tuinity end ++ + // Spigot start + public class Spigot extends Entity.Spigot { + diff --git a/patches/server/0023-Tuinity-Util-patch.patch b/patches/server/0023-Tuinity-Util-patch.patch new file mode 100644 index 000000000..3799ef540 --- /dev/null +++ b/patches/server/0023-Tuinity-Util-patch.patch @@ -0,0 +1,242 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?=E3=84=97=E3=84=A0=CB=8B=20=E3=84=91=E3=84=A7=CB=8A?= + +Date: Sat, 18 Apr 2020 19:54:01 +0800 +Subject: [PATCH] Tuinity Util patch + + +diff --git a/src/main/java/net/minecraft/server/ChunkProviderServer.java b/src/main/java/net/minecraft/server/ChunkProviderServer.java +index 1119dd5048cbb8cdd5727e18b154ed15a9a06b63..6a2a6e671f230fd6a8adb5e6ccec22dede740f44 100644 +--- a/src/main/java/net/minecraft/server/ChunkProviderServer.java ++++ b/src/main/java/net/minecraft/server/ChunkProviderServer.java +@@ -683,11 +683,10 @@ public class ChunkProviderServer extends IChunkProvider { + EnumCreatureType[] aenumcreaturetype = EnumCreatureType.values(); + // Paper start - per player mob spawning + int[] worldMobCount; +- if (this.playerChunkMap.playerMobDistanceMap != null) { ++ // Tuinity start - use view distance map ++ if (this.world.paperConfig.perPlayerMobSpawns) { + // update distance map +- this.world.timings.playerMobDistanceMapUpdate.startTiming(); +- this.playerChunkMap.playerMobDistanceMap.update(this.world.players, this.playerChunkMap.viewDistance); +- this.world.timings.playerMobDistanceMapUpdate.stopTiming(); ++ // Tuinity end - use view distance map + // re-set mob counts + for (EntityPlayer player : this.world.players) { + Arrays.fill(player.mobCounts, 0); +@@ -770,9 +769,23 @@ public class ChunkProviderServer extends IChunkProvider { + + if (this.world.paperConfig.perPlayerMobSpawns) { + int minDiff = Integer.MAX_VALUE; +- for (EntityPlayer entityplayer : this.playerChunkMap.playerMobDistanceMap.getPlayersInRange(chunk.getPos())) { ++ // Tuinity start - use view distance map ++ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet players = this.playerChunkMap.playerViewDistanceMap.getObjectsInRange(chunk.coordinateKey); ++ if (players != null) { ++ Object[] backingSet = players.getBackingSet(); ++ for (int index = 0, len = backingSet.length; index < len; ++index) { ++ Object temp = backingSet[index]; ++ if (!(temp instanceof EntityPlayer)) { ++ continue; ++ } ++ EntityPlayer entityplayer = (EntityPlayer)temp; ++ if (entityplayer.isSpectator() || !entityplayer.affectsSpawning) { ++ continue; ++ } ++ // Tuinity end - use view distance map + minDiff = Math.min(limit - this.playerChunkMap.getMobCountNear(entityplayer, enumcreaturetype), minDiff); + } ++ } // Tuinity - use view distance map + difference = (minDiff == Integer.MAX_VALUE) ? 0 : minDiff; + } + +diff --git a/src/main/java/net/minecraft/server/EntityPlayer.java b/src/main/java/net/minecraft/server/EntityPlayer.java +index b4c3dffaff5ea64d9189304c5e84c515bf40ed65..ec2f66ef0cc56229078dacad61cd6ab3a5c6cb11 100644 +--- a/src/main/java/net/minecraft/server/EntityPlayer.java ++++ b/src/main/java/net/minecraft/server/EntityPlayer.java +@@ -110,6 +110,10 @@ public class EntityPlayer extends EntityHuman implements ICrafting { + + public final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet cachedSingleHashSet; // Paper + ++ // Tuinity start ++ public final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet cachedSingleHashSetTuinity; ++ // Tuinity end ++ + public EntityPlayer(MinecraftServer minecraftserver, WorldServer worldserver, GameProfile gameprofile, PlayerInteractManager playerinteractmanager) { + super((World) worldserver, gameprofile); + playerinteractmanager.player = this; +@@ -128,6 +132,9 @@ public class EntityPlayer extends EntityHuman implements ICrafting { + this.canPickUpLoot = true; + this.maxHealthCache = this.getMaxHealth(); + this.cachedSingleMobDistanceMap = new com.destroystokyo.paper.util.PooledHashSets.PooledObjectLinkedOpenHashSet<>(this); // Paper ++ // Tuinity start ++ this.cachedSingleHashSetTuinity = new com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<>(this); ++ // Tuinity end + } + + // Yes, this doesn't match Vanilla, but it's the best we can do for now. +diff --git a/src/main/java/net/minecraft/server/HeightMap.java b/src/main/java/net/minecraft/server/HeightMap.java +index 29cb545a864dea09ba52b6071f5280cdddd33808..37a6da9ee9aab7acaf2f1559917e415217444747 100644 +--- a/src/main/java/net/minecraft/server/HeightMap.java ++++ b/src/main/java/net/minecraft/server/HeightMap.java +@@ -119,6 +119,7 @@ public class HeightMap { + } + } + ++ public final int get(int x, int z) { return this.a(x, z); } // Tuinity - OBFHELPER + public int a(int i, int j) { + return this.a(c(i, j)); + } +diff --git a/src/main/java/net/minecraft/server/PlayerChunkMap.java b/src/main/java/net/minecraft/server/PlayerChunkMap.java +index 7b7e1b2b45dcfe55c4052dacdcc2ca629ca4521b..2acdb72d7ee4bba043f8b083a2ed6c2ec1a5111d 100644 +--- a/src/main/java/net/minecraft/server/PlayerChunkMap.java ++++ b/src/main/java/net/minecraft/server/PlayerChunkMap.java +@@ -81,7 +81,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + public final Int2ObjectMap trackedEntities; + private final Queue z; + int viewDistance; // Paper - private -> package private +- public final com.destroystokyo.paper.util.PlayerMobDistanceMap playerMobDistanceMap; // Paper ++ //public final com.destroystokyo.paper.util.PlayerMobDistanceMap playerMobDistanceMap; // Paper // Tuinity - replaced by view distance map + + // CraftBukkit start - recursion-safe executor for Chunk loadCallback() and unloadCallback() + public final CallbackExecutor callbackExecutor = new CallbackExecutor(); +@@ -136,6 +136,29 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + + + // Paper end ++ // Tuinity start - distance maps ++ final com.destroystokyo.paper.util.misc.PooledLinkedHashSets pooledEntityPlayerSets = new com.destroystokyo.paper.util.misc.PooledLinkedHashSets<>(); ++ public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerViewDistanceMap; ++ ++ void addPlayerToDistanceMapsTuinity(EntityPlayer player) { ++ this.updateMapsTuinity(player); ++ } ++ ++ void removePlayerFromDistanceMapsTuinity(EntityPlayer player) { ++ this.playerViewDistanceMap.remove(player); ++ } ++ ++ void updateDistanceMapsTuinity(EntityPlayer player) { ++ this.updateMapsTuinity(player); ++ } ++ ++ private void updateMapsTuinity(EntityPlayer player) { ++ int chunkX = net.minecraft.server.MCUtil.getChunkCoordinate(player.locX()); ++ int chunkZ = net.minecraft.server.MCUtil.getChunkCoordinate(player.locZ()); ++ ++ this.playerViewDistanceMap.update(player, chunkX, chunkZ, this.viewDistance); ++ } ++ // Tuinity end + + // Paper start - Reduce entity tracker updates on move + private double trackerUpdateDistanceSquared; +@@ -201,8 +224,12 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + this.l = supplier; + this.m = new VillagePlace(new File(this.w, "poi"), datafixer, this.world); // Paper + this.setViewDistance(i); +- this.playerMobDistanceMap = this.world.paperConfig.perPlayerMobSpawns ? new com.destroystokyo.paper.util.PlayerMobDistanceMap() : null; // Paper ++ //this.playerMobDistanceMap = this.world.paperConfig.perPlayerMobSpawns ? new com.destroystokyo.paper.util.PlayerMobDistanceMap() : null; // Paper // Tuinity - distance maps + this.trackerUpdateDistanceSquared = Math.pow(this.world.paperConfig.trackerUpdateDistance, 2); // Paper ++ // Tuinity start - distance maps ++ com.destroystokyo.paper.util.misc.PooledLinkedHashSets sets = this.pooledEntityPlayerSets; ++ this.playerViewDistanceMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(sets); ++ // Tuinity end - distance maps + } + + public void updatePlayerMobTypeMap(Entity entity) { +@@ -213,9 +240,23 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + int chunkZ = (int)Math.floor(entity.locZ()) >> 4; + int index = entity.getEntityType().getEnumCreatureType().ordinal(); + +- for (EntityPlayer player : this.playerMobDistanceMap.getPlayersInRange(chunkX, chunkZ)) { ++ // Tuinity start - use view distance map ++ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet players = this.playerViewDistanceMap.getObjectsInRange(chunkX, chunkZ); ++ if (players != null) { ++ Object[] backingSet = players.getBackingSet(); ++ for (int i = 0, len = backingSet.length; i < len; ++i) { ++ Object temp = backingSet[i]; ++ if (!(temp instanceof EntityPlayer)) { ++ continue; ++ } ++ EntityPlayer player = (EntityPlayer)temp; ++ if (player.isSpectator() || !player.affectsSpawning) { ++ continue; ++ } ++ // Tuinity end - use view distance map + ++player.mobCounts[index]; + } ++ } // Tuinity - use view distance map + } + + public int getMobCountNear(EntityPlayer entityPlayer, EnumCreatureType enumCreatureType) { +@@ -1545,7 +1586,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + } + + this.updateMaps(entityplayer); // Paper - distance maps +- ++ this.updateDistanceMapsTuinity(entityplayer); // Tuinity - distance maps + } + + @Override +diff --git a/src/main/java/net/minecraft/server/World.java b/src/main/java/net/minecraft/server/World.java +index 75605d44bc4c051a7afa327a8749a0a67790b165..7129cb0260316e166b4b15cded0e0c0b32433b7f 100644 +--- a/src/main/java/net/minecraft/server/World.java ++++ b/src/main/java/net/minecraft/server/World.java +@@ -1214,9 +1214,11 @@ public abstract class World implements GeneratorAccess, AutoCloseable { + int k = MathHelper.floor((axisalignedbb.minZ - 2.0D) / 16.0D); + int l = MathHelper.floor((axisalignedbb.maxZ + 2.0D) / 16.0D); + ++ ChunkProviderServer chunkProvider = ((ChunkProviderServer)this.chunkProvider); // Tuinity - optimize for loaded chunks ++ + for (int i1 = i; i1 <= j; ++i1) { + for (int j1 = k; j1 <= l; ++j1) { +- Chunk chunk = (Chunk)this.getChunkIfLoadedImmediately(i1, j1); // Paper ++ Chunk chunk = chunkProvider.getChunkAtIfLoadedMainThread(i1, j1); // Paper // Tuinity - optimize for loaded chunks + + if (chunk != null) { + chunk.a(entity, axisalignedbb, list, predicate); +@@ -1235,9 +1237,11 @@ public abstract class World implements GeneratorAccess, AutoCloseable { + int l = MathHelper.f((axisalignedbb.maxZ + 2.0D) / 16.0D); + List list = Lists.newArrayList(); + ++ ChunkProviderServer chunkProvider = ((ChunkProviderServer)this.chunkProvider); // Tuinity - optimize for loaded chunks ++ + for (int i1 = i; i1 < j; ++i1) { + for (int j1 = k; j1 < l; ++j1) { +- Chunk chunk = (Chunk)this.getChunkIfLoadedImmediately(i1, j1); // Paper ++ Chunk chunk = chunkProvider.getChunkAtIfLoadedMainThread(i1, j1); // Paper // Tuinity - optimize for loaded chunks + + if (chunk != null) { + chunk.a(entitytypes, axisalignedbb, list, predicate); +@@ -1257,10 +1261,11 @@ public abstract class World implements GeneratorAccess, AutoCloseable { + int l = MathHelper.f((axisalignedbb.maxZ + 2.0D) / 16.0D); + List list = Lists.newArrayList(); + IChunkProvider ichunkprovider = this.getChunkProvider(); ++ ChunkProviderServer chunkProvider = ((ChunkProviderServer)this.chunkProvider); // Tuinity - optimize for loaded chunks + + for (int i1 = i; i1 < j; ++i1) { + for (int j1 = k; j1 < l; ++j1) { +- Chunk chunk = (Chunk)this.getChunkIfLoadedImmediately(i1, j1); // Paper ++ Chunk chunk = chunkProvider.getChunkAtIfLoadedMainThread(i1, j1); // Paper // Tuinity - optimize for loaded chunks + + if (chunk != null) { + chunk.a(oclass, axisalignedbb, list, predicate); +diff --git a/src/main/java/net/minecraft/server/WorldServer.java b/src/main/java/net/minecraft/server/WorldServer.java +index 78fdacdaf119b9c3fab477ee98089bbad5da2d73..4d080d74dc949ea93ce34aa1906144740b9c99f6 100644 +--- a/src/main/java/net/minecraft/server/WorldServer.java ++++ b/src/main/java/net/minecraft/server/WorldServer.java +@@ -1227,6 +1227,7 @@ public class WorldServer extends World { + + this.registerEntity(entityplayer); + this.getChunkProvider().playerChunkMap.addPlayerToDistanceMaps(entityplayer); // Paper - distance maps ++ this.getChunkProvider().playerChunkMap.addPlayerToDistanceMapsTuinity(entityplayer); // Tuinity - distance maps + } + + // CraftBukkit start +@@ -1438,6 +1439,7 @@ public class WorldServer extends World { + + this.players.remove(entityplayer); + this.getChunkProvider().playerChunkMap.removePlayerFromDistanceMaps(entityplayer); // Paper - distance maps ++ this.getChunkProvider().playerChunkMap.removePlayerFromDistanceMapsTuinity(entityplayer); // Tuinity - distance maps + } + + this.getScoreboard().a(entity); diff --git a/patches/server/0024-Tuinity-Per-player-view-distance-implementation.patch b/patches/server/0024-Tuinity-Per-player-view-distance-implementation.patch new file mode 100644 index 000000000..b7217beff --- /dev/null +++ b/patches/server/0024-Tuinity-Per-player-view-distance-implementation.patch @@ -0,0 +1,1496 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?=E3=84=97=E3=84=A0=CB=8B=20=E3=84=91=E3=84=A7=CB=8A?= + +Date: Sat, 18 Apr 2020 19:57:30 +0800 +Subject: [PATCH] Tuinity Per player view distance implementation + +This patch implements CraftPlayer#setViewDistance and +CraftPlayer#getViewDistance + +This patch replaces the existing system to handle player tickets +and chunk sending to players. + Most of the work is pulled by two +PlayerAreaMap's controlling: a radius for sending chunks and a radius +for adding player tickets. Currently the difference between the two +radii is just 1. +Normal vanilla has a difference of two, but this +causes issues where outer chunks in the view distance sometimes +do not send. This is because the outer radius is not guaranteed to +be at ticket level 32, which is required to be sent to players. + +The ticket manager in ChunkMapDistance for players is no longer used and +is entirely replaced by the ticket distance map. The ticket tracker also +adds a configurable chunk load rate per player. + +This patch moves the order of the distance map update in movePlayer +until after a client is sent a center section packet. This is required +to avoid the client rejecting chunks it receives when teleporting. + +diff --git a/src/main/java/io/akarin/server/Config.java b/src/main/java/io/akarin/server/Config.java +index 2ac8f02a97429f04f3e5c9206ec228edccaf24c9..be87db9f63b17114f5ca2a9e2de9f56e528d4048 100644 +--- a/src/main/java/io/akarin/server/Config.java ++++ b/src/main/java/io/akarin/server/Config.java +@@ -96,6 +96,45 @@ public final class Config { + return Config.config.getDouble(path, dfl); + } + ++ public static double maxChunkSendsPerPlayer; // per second ++ ++ public static int[] maxChunkSendsPerPlayerChoice = new int[100]; ++ ++ private static void maxChunkLoadsPerPlayer() { ++ if (TuinityConfig.configVersion < 1) { ++ TuinityConfig.set("max-pending-chunk-tickets-per-player", null); ++ } ++ maxChunkSendsPerPlayer = TuinityConfig.getDouble("target-chunk-sends-per-player-per-second", 40.0); ++ if (maxChunkSendsPerPlayer <= -1.0) { ++ maxChunkSendsPerPlayer = Integer.MAX_VALUE; ++ } else if (maxChunkSendsPerPlayer <= 1.0) { ++ maxChunkSendsPerPlayer = 1.0; ++ } else if (maxChunkSendsPerPlayer > Integer.MAX_VALUE) { ++ maxChunkSendsPerPlayer = Integer.MAX_VALUE; ++ } ++ ++ double rateTick = maxChunkSendsPerPlayer / 20.0; ++ double a = Math.floor(rateTick); ++ double b = Math.ceil(rateTick); ++ ++ // we want to spread out a and b over the interval so it's smooth ++ ++ int aInt = (int)a; ++ int bInt = (int)b; ++ double total = b; ++ maxChunkSendsPerPlayerChoice[0] = bInt; ++ ++ for (int i = 1, len = maxChunkSendsPerPlayerChoice.length; i < len; ++i) { ++ if (total / (double)i >= rateTick) { ++ total += a; ++ maxChunkSendsPerPlayerChoice[i] = aInt; ++ } else { ++ total += b; ++ maxChunkSendsPerPlayerChoice[i] = bInt; ++ } ++ } ++ } ++ + public static final class WorldConfig { + + public final String worldName; +@@ -178,6 +217,14 @@ public final class Config { + this.worldDefaults.addDefault(path, Double.valueOf(dfl)); + return this.config.getDouble(path, this.worldDefaults.getDouble(path)); + } ++ ++ public int noTickViewDistance; ++ public int maxPendingChunkLoads; ++ private void noTickViewDistance() { ++ this.noTickViewDistance = this.getInt("no-tick-view-distance", -1); ++ this.maxPendingChunkLoads = this.getInt("max-pending-chunk-loads", 1); ++ } ++ + } + + } +diff --git a/src/main/java/net/minecraft/server/ChunkMapDistance.java b/src/main/java/net/minecraft/server/ChunkMapDistance.java +index 1890fad9ed10c514b7b5982d2c112f7dc52866a5..984767304e6b5270cc8f86785b755a02d78d72ed 100644 +--- a/src/main/java/net/minecraft/server/ChunkMapDistance.java ++++ b/src/main/java/net/minecraft/server/ChunkMapDistance.java +@@ -1,5 +1,6 @@ + package net.minecraft.server; + ++import net.minecraft.server.MCUtil; // Tuinity + import com.google.common.collect.ImmutableList; + import com.google.common.collect.Sets; + import com.mojang.datafixers.util.Either; +@@ -27,12 +28,12 @@ import org.apache.logging.log4j.Logger; + public abstract class ChunkMapDistance { + + private static final Logger LOGGER = LogManager.getLogger(); +- private static final int b = 33 + ChunkStatus.a(ChunkStatus.FULL) - 2; ++ private static final int b = 33 + ChunkStatus.a(ChunkStatus.FULL) - 2; public static int getPlayerTicketLevel() { return ChunkMapDistance.b; } // Tuinity - OBFHELPER + private final Long2ObjectMap> c = new Long2ObjectOpenHashMap(); + public final Long2ObjectOpenHashMap>> tickets = new Long2ObjectOpenHashMap(); + private final ChunkMapDistance.a e = new ChunkMapDistance.a(); + private final ChunkMapDistance.b f = new ChunkMapDistance.b(8); +- private final ChunkMapDistance.c g = new ChunkMapDistance.c(33); ++ //private final ChunkMapDistance.c g = new ChunkMapDistance.c(33); // Tuinity - no longer used + private final java.util.Queue pendingChunkUpdates = new java.util.ArrayDeque<>(); // PAIL pendingChunkUpdates // Paper - use a queue // Akarin - backport Tuinity - use a better queue + private final ChunkTaskQueueSorter i; + private final Mailbox> j; +@@ -41,6 +42,32 @@ public abstract class ChunkMapDistance { + private final Executor m; + private long currentTick; + ++ // Tuinity start ++ protected PlayerChunkMap chunkMap; ++ protected final ChunkMapDistance.TicketTracker playerTickViewDistanceHandler = new TicketTracker(ChunkMapDistance.getPlayerTicketLevel()) { ++ @Override ++ protected int tryQueueChunk(int chunkX, int chunkZ, EntityPlayer player) { ++ long coordinate = MCUtil.getCoordinateKey(chunkX, chunkZ); ++ PlayerChunk currentChunk = ChunkMapDistance.this.chunkMap.getUpdatingChunk(coordinate); ++ if (currentChunk != null) { ++ Chunk fullChunk = currentChunk.getFullReadyChunk(); ++ if (fullChunk != null && fullChunk.areNeighboursLoaded(2)) { ++ this.chunkReferenceMap.putIfAbsent(coordinate, LOADED_PLAYER_REFERENCE); ++ ChunkMapDistance.this.addTicket(coordinate, new Ticket<>(TicketType.PLAYER, this.ticketLevel, new ChunkCoordIntPair(chunkX, chunkZ))); ++ return QUEUED; ++ } ++ } ++ ++ return FAILED; ++ } ++ ++ @Override ++ protected int getMaxChunkLoads(EntityPlayer player) { ++ return Integer.MAX_VALUE; ++ } ++ }; ++ // Tuinity end ++ + protected ChunkMapDistance(Executor executor, Executor executor1) { + executor1.getClass(); + Mailbox mailbox = Mailbox.a("player ticket throttler", executor1::execute); +@@ -86,7 +113,7 @@ public abstract class ChunkMapDistance { + + public boolean a(PlayerChunkMap playerchunkmap) { + this.f.a(); +- this.g.a(); ++ //this.g.a(); // Tuinity - no longer used + int i = Integer.MAX_VALUE - this.e.a(Integer.MAX_VALUE); + boolean flag = i != 0; + +@@ -242,7 +269,7 @@ public abstract class ChunkMapDistance { + return new ObjectOpenHashSet(); + })).add(entityplayer); + this.f.b(i, 0, true); +- this.g.b(i, 0, true); ++ //this.g.b(i, 0, true); // Tuinity - no longer used + } + + public void b(SectionPosition sectionposition, EntityPlayer entityplayer) { +@@ -253,7 +280,7 @@ public abstract class ChunkMapDistance { + if (objectset.isEmpty()) { + this.c.remove(i); + this.f.b(i, Integer.MAX_VALUE, false); +- this.g.b(i, Integer.MAX_VALUE, false); ++ //this.g.b(i, Integer.MAX_VALUE, false); // Tuinity - no longer used + } + + } +@@ -271,9 +298,16 @@ public abstract class ChunkMapDistance { + return s; + } + ++ protected void setViewDistance(int viewDistance) { this.a(viewDistance); } // Tuinity - OBFHELPER + protected void a(int i) { +- this.g.a(i); ++ //this.g.a(i); // Tuinity - no longer used + } ++ // Tuinity start - per player view distance ++ protected void setGlobalViewDistance(int viewDistance, PlayerChunkMap chunkMap) { ++ this.chunkMap = chunkMap; ++ this.setViewDistance(viewDistance); ++ } ++ // Tuinity end + + public int b() { + this.f.a(); +@@ -354,6 +388,222 @@ public abstract class ChunkMapDistance { + } + } + ++ // Tuinity start - Per player view distance ++ abstract class TicketTracker { ++ ++ static final int LOADED_PLAYER_REFERENCE = -2; ++ ++ protected final it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap chunkReferenceMap = new it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap(8192, 0.25f); ++ { ++ this.chunkReferenceMap.defaultReturnValue(-1); ++ } ++ protected final it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap lastLoadedRadiusByPlayer = new it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap(512, 0.5f); ++ { ++ this.lastLoadedRadiusByPlayer.defaultReturnValue(-1); ++ } ++ ++ protected final it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap pendingChunkLoadsByPlayer = new it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap(512, 0.5f); ++ protected final it.unimi.dsi.fastutil.ints.Int2LongOpenHashMap lastChunkPositionByPlayer = new it.unimi.dsi.fastutil.ints.Int2LongOpenHashMap(512, 0.5f); ++ { ++ this.lastChunkPositionByPlayer.defaultReturnValue(Long.MIN_VALUE); ++ } ++ ++ protected final int ticketLevel; ++ ++ public TicketTracker(int ticketLevel) { ++ this.ticketLevel = ticketLevel; ++ } ++ ++ protected final java.util.List players = new java.util.ArrayList<>(256); ++ ++ protected com.destroystokyo.paper.util.misc.PlayerAreaMap areaMap; ++ ++ static final int ALREADY_QUEUED = 0; ++ static final int QUEUED = 1; ++ static final int FAILED = 2; ++ ++ protected abstract int tryQueueChunk(int chunkX, int chunkZ, EntityPlayer player); ++ ++ protected abstract int getMaxChunkLoads(EntityPlayer player); ++ ++ public void tick() { ++ for (EntityPlayer player : this.players) { ++ int playerId = player.getId(); ++ int lastLoadedRadius = this.lastLoadedRadiusByPlayer.get(playerId); ++ int pendingChunkLoads = this.pendingChunkLoadsByPlayer.get(playerId); ++ long lastChunkPos = this.lastChunkPositionByPlayer.get(playerId); ++ long currentChunkPos = this.areaMap.getLastCoordinate(player); ++ ++ if (currentChunkPos == Long.MIN_VALUE) { ++ // not tracking for whatever reason... ++ continue; ++ } ++ ++ int newX = MCUtil.getCoordinateX(currentChunkPos); ++ int newZ = MCUtil.getCoordinateZ(currentChunkPos); ++ ++ // handle movement ++ if (currentChunkPos != lastChunkPos) { ++ this.lastChunkPositionByPlayer.put(playerId, currentChunkPos); ++ if (lastChunkPos != Long.MIN_VALUE) { ++ int oldX = MCUtil.getCoordinateX(lastChunkPos); ++ int oldZ = MCUtil.getCoordinateZ(lastChunkPos); ++ ++ int radiusDiff = Math.max(Math.abs(newX - oldX), Math.abs(newZ - oldZ)); ++ lastLoadedRadius = Math.max(-1, lastLoadedRadius - radiusDiff); ++ this.lastLoadedRadiusByPlayer.put(playerId, lastLoadedRadius); ++ } ++ } ++ ++ int maxChunkLoads = this.getMaxChunkLoads(player); ++ ++ int radius = lastLoadedRadius + 1; ++ int viewDistance = this.areaMap.getLastViewDistance(player); ++ ++ if (radius > viewDistance) { ++ // distance map will unload our chunks ++ this.lastLoadedRadiusByPlayer.put(playerId, viewDistance); ++ continue; ++ } ++ ++ if (pendingChunkLoads >= maxChunkLoads) { ++ continue; ++ } ++ ++ radius_loop: ++ for (; radius <= viewDistance; ++radius) { ++ for (int offset = 0; offset <= radius; ++offset) { ++ // try to load the chunks closest to the player by distance ++ // so instead of going left->right on the x axis, we start at the center of the view distance square ++ // and go left and right at the same time ++ ++ // try top 2 chunks ++ // top left ++ int attempt = 0; ++ if ((attempt = this.tryQueueChunk(newX - offset, newZ + radius, player)) == QUEUED) { ++ if (++pendingChunkLoads >= maxChunkLoads) { ++ break radius_loop; ++ } ++ } else if (attempt == FAILED) { ++ break radius_loop; ++ } ++ ++ // top right ++ if ((attempt = this.tryQueueChunk(newX + offset, newZ + radius, player)) == QUEUED) { ++ if (++pendingChunkLoads >= maxChunkLoads) { ++ break radius_loop; ++ } ++ } else if (attempt == FAILED) { ++ break radius_loop; ++ } ++ ++ // try bottom 2 chunks ++ ++ // bottom left ++ if ((attempt = this.tryQueueChunk(newX - offset, newZ - radius, player)) == QUEUED) { ++ if (++pendingChunkLoads >= maxChunkLoads) { ++ break radius_loop; ++ } ++ } else if (attempt == FAILED) { ++ break radius_loop; ++ } ++ ++ // bottom right ++ if ((attempt = this.tryQueueChunk(newX + offset, newZ - radius, player)) == QUEUED) { ++ if (++pendingChunkLoads >= maxChunkLoads) { ++ break radius_loop; ++ } ++ } else if (attempt == FAILED) { ++ break radius_loop; ++ } ++ ++ // try left 2 chunks ++ ++ // left down ++ if ((attempt = this.tryQueueChunk(newX - radius, newZ - offset, player)) == QUEUED) { ++ if (++pendingChunkLoads >= maxChunkLoads) { ++ break radius_loop; ++ } ++ } else if (attempt == FAILED) { ++ break radius_loop; ++ } ++ ++ // left up ++ if ((attempt = this.tryQueueChunk(newX - radius, newZ + offset, player)) == QUEUED) { ++ if (++pendingChunkLoads >= maxChunkLoads) { ++ break radius_loop; ++ } ++ } else if (attempt == FAILED) { ++ break radius_loop; ++ } ++ ++ // try right 2 chunks ++ ++ // right down ++ if ((attempt = this.tryQueueChunk(newX + radius, newZ - offset, player)) == QUEUED) { ++ if (++pendingChunkLoads >= maxChunkLoads) { ++ break radius_loop; ++ } ++ } else if (attempt == FAILED) { ++ break radius_loop; ++ } ++ ++ // right up ++ if ((attempt = this.tryQueueChunk(newX + radius, newZ + offset, player)) == QUEUED) { ++ if (++pendingChunkLoads >= maxChunkLoads) { ++ break radius_loop; ++ } ++ } else if (attempt == FAILED) { ++ break radius_loop; ++ } ++ } ++ } ++ ++ int newLoadedRadius = radius - 1; ++ if (newLoadedRadius != lastLoadedRadius) { ++ this.lastLoadedRadiusByPlayer.put(playerId, newLoadedRadius); ++ } ++ this.pendingChunkLoadsByPlayer.put(playerId, pendingChunkLoads); ++ } ++ } ++ ++ public void addPlayer(EntityPlayer player) { ++ this.players.add(player); ++ } ++ ++ public void removePlayer(EntityPlayer player) { ++ this.players.remove(player); ++ this.lastLoadedRadiusByPlayer.remove(player.getId()); ++ this.pendingChunkLoadsByPlayer.remove(player.getId()); ++ this.lastChunkPositionByPlayer.remove(player.getId()); ++ } ++ ++ public void onChunkLoad(int chunkX, int chunkZ) { ++ long coordinate = MCUtil.getCoordinateKey(chunkX, chunkZ); ++ int playerReference = this.chunkReferenceMap.replace(coordinate, LOADED_PLAYER_REFERENCE); ++ if (playerReference != -1) { ++ this.pendingChunkLoadsByPlayer.computeIfPresent(playerReference, (Integer keyInMap, Integer valueInMap) -> { ++ return valueInMap - 1; ++ }); ++ } ++ } ++ ++ // this is invoked if and only if there are no other players in range of the chunk. ++ public void playerMoveOutOfRange(int chunkX, int chunkZ) { ++ long coordinate = MCUtil.getCoordinateKey(chunkX, chunkZ); ++ int playerReference = this.chunkReferenceMap.remove(coordinate); ++ if (playerReference != -1) { ++ if (playerReference != LOADED_PLAYER_REFERENCE) { ++ this.pendingChunkLoadsByPlayer.computeIfPresent(playerReference, (Integer keyInMap, Integer valueInMap) -> { ++ return valueInMap - 1; ++ }); ++ } ++ ChunkMapDistance.this.removeTicket(coordinate, new Ticket<>(TicketType.PLAYER, this.ticketLevel, new ChunkCoordIntPair(chunkX, chunkZ))); ++ } ++ } ++ } ++ // Tuinity end - per player view distance ++ + class c extends ChunkMapDistance.b { + + private int e = 0; +diff --git a/src/main/java/net/minecraft/server/ChunkProviderServer.java b/src/main/java/net/minecraft/server/ChunkProviderServer.java +index 6a2a6e671f230fd6a8adb5e6ccec22dede740f44..adc2010e643eb14ccedbb0c3af9ec7e02ab0daa2 100644 +--- a/src/main/java/net/minecraft/server/ChunkProviderServer.java ++++ b/src/main/java/net/minecraft/server/ChunkProviderServer.java +@@ -534,7 +534,7 @@ public class ChunkProviderServer extends IChunkProvider { + return this.serverThreadQueue.executeNext(); + } + +- private boolean tickDistanceManager() { ++ boolean tickDistanceManager() { // Tuinity - remove private + boolean flag = this.chunkMapDistance.a(this.playerChunkMap); + boolean flag1 = this.playerChunkMap.b(); + +@@ -643,6 +643,11 @@ public class ChunkProviderServer extends IChunkProvider { + this.world.getMinecraftServer().midTickLoadChunks(); // Paper + this.tickDistanceManager(); + this.world.timings.doChunkMap.stopTiming(); // Spigot ++ // Tuinity start ++ this.playerChunkMap.chunkLoadScheduler.queueMaxLoads(); ++ this.playerChunkMap.getChunkMapDistanceManager().playerTickViewDistanceHandler.tick(); ++ this.playerChunkMap.chunkSendThrottler.tick(); ++ // Tuinity end + // this.world.getMethodProfiler().exitEnter("chunks"); // Akarin - remove caller + this.world.timings.chunks.startTiming(); // Paper - timings + this.tickChunks(); +diff --git a/src/main/java/net/minecraft/server/EntityEnderDragon.java b/src/main/java/net/minecraft/server/EntityEnderDragon.java +index 2887cb14e4f9f90412cbf6f83c651e5866fb16b3..b9b2b053878cc21245e6bbb2174dc51c35c8e624 100644 +--- a/src/main/java/net/minecraft/server/EntityEnderDragon.java ++++ b/src/main/java/net/minecraft/server/EntityEnderDragon.java +@@ -579,9 +579,9 @@ public class EntityEnderDragon extends EntityInsentient implements IMonster { + // CraftBukkit start - Use relative location for far away sounds + // this.world.b(1028, new BlockPosition(this), 0); + // Paper start +- int viewDistance = ((WorldServer) this.world).spigotConfig.viewDistance * 16; // Paper - updated to use worlds actual view distance incase we have to uncomment this due to removal of player view distance API ++ //int viewDistance = ((WorldServer) this.world).spigotConfig.viewDistance * 16; // Paper - updated to use worlds actual view distance incase we have to uncomment this due to removal of player view distance API // Tuinity - per player view distance + for (EntityPlayer player : ((WorldServer)world).getPlayers()) { +- //final int viewDistance = player.getViewDistance(); // TODO apply view distance api patch ++ final int viewDistance = player.getEffectiveViewDistance(player.getWorldServer().getChunkProvider().playerChunkMap) << 4; // Tuinity - per player view distance + // Paper end + double deltaX = this.locX() - player.locX(); + double deltaZ = this.locZ() - player.locZ(); +diff --git a/src/main/java/net/minecraft/server/EntityPlayer.java b/src/main/java/net/minecraft/server/EntityPlayer.java +index ec2f66ef0cc56229078dacad61cd6ab3a5c6cb11..def793590801b369c12b21de6116fc72028c75b1 100644 +--- a/src/main/java/net/minecraft/server/EntityPlayer.java ++++ b/src/main/java/net/minecraft/server/EntityPlayer.java +@@ -114,6 +114,33 @@ public class EntityPlayer extends EntityHuman implements ICrafting { + public final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet cachedSingleHashSetTuinity; + // Tuinity end + ++ // Tuinity start - view distance api ++ final it.unimi.dsi.fastutil.longs.LongOpenHashSet loadedChunks = new it.unimi.dsi.fastutil.longs.LongOpenHashSet(); ++ ++ boolean needsChunkCenterUpdate; ++ int viewDistance = -1; ++ public final int getRawViewDistance() { ++ return this.viewDistance; ++ } ++ public final int getEffectiveViewDistance() { ++ return this.getEffectiveViewDistance(((WorldServer)this.world).getChunkProvider().playerChunkMap); ++ } ++ public final int getEffectiveViewDistance(PlayerChunkMap chunkMap) { ++ return this.viewDistance == -1 ? chunkMap.viewDistance : this.viewDistance; ++ } ++ ++ int noTickViewDistance = -1; ++ public final int getRawNoTickViewDistance() { ++ return this.noTickViewDistance; ++ } ++ public final int getEffectiveNoTickViewDistance() { ++ return this.getEffectiveNoTickViewDistance(((WorldServer)this.world).getChunkProvider().playerChunkMap); ++ } ++ public final int getEffectiveNoTickViewDistance(PlayerChunkMap chunkMap) { ++ return this.noTickViewDistance == -1 ? chunkMap.getEffectiveNoTickViewDistance() : this.noTickViewDistance; ++ } ++ // Tuinity end - view distance api ++ + public EntityPlayer(MinecraftServer minecraftserver, WorldServer worldserver, GameProfile gameprofile, PlayerInteractManager playerinteractmanager) { + super((World) worldserver, gameprofile); + playerinteractmanager.player = this; +diff --git a/src/main/java/net/minecraft/server/EntityWither.java b/src/main/java/net/minecraft/server/EntityWither.java +index 8977c3516b2ee9b970b6274a5c71982b019ac2a6..bace6cf36a9e888278a6c7a5c5866d9f3b9ecf87 100644 +--- a/src/main/java/net/minecraft/server/EntityWither.java ++++ b/src/main/java/net/minecraft/server/EntityWither.java +@@ -208,9 +208,9 @@ public class EntityWither extends EntityMonster implements IRangedEntity { + // CraftBukkit start - Use relative location for far away sounds + // this.world.b(1023, new BlockPosition(this), 0); + // Paper start +- int viewDistance = ((WorldServer) this.world).spigotConfig.viewDistance * 16; // Paper - updated to use worlds actual view distance incase we have to uncomment this due to removal of player view distance API ++ //int viewDistance = ((WorldServer) this.world).spigotConfig.viewDistance * 16; // Paper - updated to use worlds actual view distance incase we have to uncomment this due to removal of player view distance API // Tuinity - per player view distance + for (EntityPlayer player : ((WorldServer)world).getPlayers()) { +- //final int viewDistance = player.getViewDistance(); // TODO apply view distance api patch ++ final int viewDistance = player.getEffectiveViewDistance(player.getWorldServer().getChunkProvider().playerChunkMap) << 4; // Tuinity - per player view distance + // Paper end + double deltaX = this.locX() - player.locX(); + double deltaZ = this.locZ() - player.locZ(); +diff --git a/src/main/java/net/minecraft/server/PlayerChunk.java b/src/main/java/net/minecraft/server/PlayerChunk.java +index a9a2ce3d3f46894c3eb973df239fec088eb1b711..7ad1d23dfb7c9b008f72f62f632c7b613ebd77df 100644 +--- a/src/main/java/net/minecraft/server/PlayerChunk.java ++++ b/src/main/java/net/minecraft/server/PlayerChunk.java +@@ -270,6 +270,7 @@ public class PlayerChunk { + return this.entityTickingFuture; + } + ++ public final CompletableFuture> getFullChunkFuture() { return this.c(); } // Tuinity - OBFHELPER + public CompletableFuture> c() { + return this.fullChunkFuture; + } +@@ -305,7 +306,7 @@ public class PlayerChunk { + } + + public void a(int i, int j, int k) { +- Chunk chunk = this.getChunk(); ++ Chunk chunk = this.getFullReadyChunk(); // Tuinity - per player view distance - allow block updates in non-ticking chunks + + if (chunk != null) { + this.r |= 1 << (j >> 4); +@@ -325,7 +326,7 @@ public class PlayerChunk { + } + + public void a(EnumSkyBlock enumskyblock, int i) { +- Chunk chunk = this.getChunk(); ++ Chunk chunk = this.getFullReadyChunk(); // Tuinity - per player view distance - allow block updates in non-ticking chunks + + if (chunk != null) { + chunk.setNeedsSaving(true); +@@ -415,9 +416,57 @@ public class PlayerChunk { + } + + private void a(Packet packet, boolean flag) { +- this.players.a(this.location, flag).forEach((entityplayer) -> { +- entityplayer.playerConnection.sendPacket(packet); +- }); ++ // Tuinity start - per player view distance ++ // there can be potential desync with player's last mapped section and the view distance map, so use the ++ // view distance map here. ++ PlayerChunkMap chunkMap = ((PlayerChunkMap)this.players); ++ com.destroystokyo.paper.util.misc.PlayerAreaMap viewDistanceMap = chunkMap.playerViewDistanceBroadcastMap; ++ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet players = viewDistanceMap.getObjectsInRange(this.location); ++ if (players == null) { ++ return; ++ } ++ ++ long coordinate = net.minecraft.server.MCUtil.getCoordinateKey(this.location); ++ ++ if (flag) { // flag -> border only ++ Object[] backingSet = players.getBackingSet(); ++ for (int i = 0, len = backingSet.length; i < len; ++i) { ++ Object temp = backingSet[i]; ++ if (!(temp instanceof EntityPlayer)) { ++ continue; ++ } ++ EntityPlayer player = (EntityPlayer)temp; ++ if (!player.loadedChunks.contains(coordinate)) { ++ continue; ++ } ++ ++ int viewDistance = viewDistanceMap.getLastViewDistance(player); ++ long lastPosition = viewDistanceMap.getLastCoordinate(player); ++ ++ int distX = Math.abs(net.minecraft.server.MCUtil.getCoordinateX(lastPosition) - this.location.x); ++ int distZ = Math.abs(net.minecraft.server.MCUtil.getCoordinateZ(lastPosition) - this.location.z); ++ ++ if (Math.max(distX, distZ) == viewDistance) { ++ player.playerConnection.sendPacket(packet); ++ } ++ } ++ } else { ++ Object[] backingSet = players.getBackingSet(); ++ for (int i = 0, len = backingSet.length; i < len; ++i) { ++ Object temp = backingSet[i]; ++ if (!(temp instanceof EntityPlayer)) { ++ continue; ++ } ++ EntityPlayer player = (EntityPlayer)temp; ++ if (!player.loadedChunks.contains(coordinate)) { ++ continue; ++ } ++ player.playerConnection.sendPacket(packet); ++ } ++ } ++ ++ return; ++ // Tuinity end - per player view distance + } + + public CompletableFuture> a(ChunkStatus chunkstatus, PlayerChunkMap playerchunkmap) { +@@ -618,6 +667,9 @@ public class PlayerChunk { + + + ++ // Tuinity start - per player view distance implementation ++ PlayerChunk.this.chunkMap.getChunkMapDistanceManager().playerTickViewDistanceHandler.onChunkLoad(this.location.x, this.location.z); ++ // Tuinity end - per player view distance implementation + } + }); + // Paper end +diff --git a/src/main/java/net/minecraft/server/PlayerChunkMap.java b/src/main/java/net/minecraft/server/PlayerChunkMap.java +index 2acdb72d7ee4bba043f8b083a2ed6c2ec1a5111d..16bb6c01c51906c8765158bd2baf568a6957d811 100644 +--- a/src/main/java/net/minecraft/server/PlayerChunkMap.java ++++ b/src/main/java/net/minecraft/server/PlayerChunkMap.java +@@ -80,7 +80,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + private final PlayerMap playerMap; + public final Int2ObjectMap trackedEntities; + private final Queue z; +- int viewDistance; // Paper - private -> package private ++ int viewDistance; public final int getViewDistance() { return this.viewDistance; } // Tuinity - OBFHELPER // Paper - private -> package private + //public final com.destroystokyo.paper.util.PlayerMobDistanceMap playerMobDistanceMap; // Paper // Tuinity - replaced by view distance map + + // CraftBukkit start - recursion-safe executor for Chunk loadCallback() and unloadCallback() +@@ -111,6 +111,354 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + // Paper start - distance maps + private final com.destroystokyo.paper.util.misc.PooledLinkedHashSets pooledLinkedPlayerHashSets = new com.destroystokyo.paper.util.misc.PooledLinkedHashSets<>(); + ++ // Tuinity start - per player view distance ++ int noTickViewDistance; ++ public final int getRawNoTickViewDistance() { ++ return this.noTickViewDistance; ++ } ++ public final int getEffectiveNoTickViewDistance() { ++ return this.noTickViewDistance == -1 ? this.viewDistance : this.noTickViewDistance; ++ } ++ // we use this map to broadcast chunks to clients ++ // they do not render chunks without having at least neighbours in a 1 chunk radius loaded ++ public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerViewDistanceBroadcastMap; ++ public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerViewDistanceTickMap; ++ public final com.destroystokyo.paper.util.misc.PlayerDistanceTrackingAreaMap playerViewDistanceNoTickMap; ++ ++ final ChunkSendThrottler chunkSendThrottler = new ChunkSendThrottler(); ++ ++ public void updateViewDistance(EntityPlayer player, int viewDistance, int noTickViewDistance) { ++ player.viewDistance = viewDistance; ++ player.noTickViewDistance = noTickViewDistance; ++ ++ int chunkX = net.minecraft.server.MCUtil.getChunkCoordinate(player.locX()); ++ int chunkZ = net.minecraft.server.MCUtil.getChunkCoordinate(player.locZ()); ++ ++ int effectiveViewDistance = viewDistance == -1 ? this.viewDistance : viewDistance; ++ int effectiveNoTickViewDistance = Math.max(effectiveViewDistance, noTickViewDistance == -1 ? this.getEffectiveNoTickViewDistance() : noTickViewDistance); ++ ++ player.playerConnection.sendPacket(new PacketPlayOutViewDistance(effectiveNoTickViewDistance)); ++ ++ if (!this.cannotLoadChunks(player)) { ++ this.playerViewDistanceTickMap.update(player, chunkX, chunkZ, effectiveViewDistance); ++ this.playerViewDistanceNoTickMap.update(player, chunkX, chunkZ, effectiveNoTickViewDistance + 2); // clients need chunk neighbours // add an extra one for antixray ++ } ++ this.playerViewDistanceMap.update(player, chunkX, chunkZ, effectiveViewDistance); ++ player.needsChunkCenterUpdate = true; ++ this.playerViewDistanceBroadcastMap.update(player, chunkX, chunkZ, effectiveNoTickViewDistance + 1); // clients need chunk neighbours ++ player.needsChunkCenterUpdate = false; ++ } ++ ++ final class ChunkSendThrottler { ++ ++ static final int ALREADY_QUEUED = 0; ++ static final int QUEUED = 1; ++ static final int FAILED = 2; ++ ++ protected final it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap lastLoadedRadiusByPlayer = new it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap(512, 0.5f); ++ ++ { ++ this.lastLoadedRadiusByPlayer.defaultReturnValue(-1); ++ } ++ ++ protected final it.unimi.dsi.fastutil.ints.Int2LongOpenHashMap lastChunkPositionByPlayer = new it.unimi.dsi.fastutil.ints.Int2LongOpenHashMap(512, 0.5f); ++ ++ { ++ this.lastChunkPositionByPlayer.defaultReturnValue(Long.MIN_VALUE); ++ } ++ ++ protected final it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap chunkSendCountPerPlayer = new it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap(512, 0.5f); ++ ++ protected final it.unimi.dsi.fastutil.ints.Int2LongOpenHashMap lastChunkSendStartTimePerPlayer = new it.unimi.dsi.fastutil.ints.Int2LongOpenHashMap(512, 0.5f); ++ ++ protected final java.util.List players = new java.util.ArrayList<>(256); ++ ++ protected final it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap cachedChunkPackets = new it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<>(); ++ ++ void addPlayer(EntityPlayer player) { ++ this.players.add(player); ++ } ++ ++ void removePlayer(EntityPlayer player) { ++ this.players.remove(player); ++ this.lastLoadedRadiusByPlayer.remove(player.getId()); ++ this.chunkSendCountPerPlayer.remove(player.getId()); ++ this.lastChunkPositionByPlayer.remove(player.getId()); ++ player.loadedChunks.clear(); ++ } ++ ++ int trySendChunk(int chunkX, int chunkZ, EntityPlayer player) { ++ long coordinate = net.minecraft.server.MCUtil.getCoordinateKey(chunkX, chunkZ); ++ PlayerChunk playerChunk = PlayerChunkMap.this.getUpdatingChunk(coordinate); ++ ++ if (playerChunk == null) { ++ return FAILED; ++ } ++ Chunk chunk = playerChunk.getFullReadyChunk(); ++ if (chunk == null || !chunk.areNeighboursLoaded(1)) { ++ return FAILED; ++ } ++ ++ if (!player.loadedChunks.add(coordinate)) { ++ return ALREADY_QUEUED; ++ } ++ ++ Packet[] chunkPackets = this.cachedChunkPackets.computeIfAbsent(coordinate, (long keyInMap) -> new Packet[2]); ++ PlayerChunkMap.this.sendChunk(player, chunkPackets, chunk); ++ ++ return QUEUED; ++ } ++ ++ void tick() { ++ int maxChunkSends = io.akarin.server.Config.maxChunkSendsPerPlayerChoice[MinecraftServer.currentTick % io.akarin.server.Config.maxChunkSendsPerPlayerChoice.length]; ++ for (EntityPlayer player : this.players) { ++ int playerId = player.getId(); ++ int lastLoadedRadius = this.lastLoadedRadiusByPlayer.get(playerId); ++ long lastChunkPos = this.lastChunkPositionByPlayer.get(playerId); ++ long currentChunkPos = PlayerChunkMap.this.playerViewDistanceBroadcastMap.getLastCoordinate(player); ++ ++ if (currentChunkPos == Long.MIN_VALUE) { ++ // not tracking for whatever reason... ++ continue; ++ } ++ ++ int newX = net.minecraft.server.MCUtil.getCoordinateX(currentChunkPos); ++ int newZ = net.minecraft.server.MCUtil.getCoordinateZ(currentChunkPos); ++ ++ // handle movement ++ if (currentChunkPos != lastChunkPos) { ++ this.lastChunkPositionByPlayer.put(playerId, currentChunkPos); ++ if (lastChunkPos != Long.MIN_VALUE) { ++ int oldX = net.minecraft.server.MCUtil.getCoordinateX(lastChunkPos); ++ int oldZ = net.minecraft.server.MCUtil.getCoordinateZ(lastChunkPos); ++ ++ int radiusDiff = Math.max(Math.abs(newX - oldX), Math.abs(newZ - oldZ)); ++ lastLoadedRadius = Math.max(-1, lastLoadedRadius - radiusDiff); ++ this.lastLoadedRadiusByPlayer.put(playerId, lastLoadedRadius); ++ } ++ } ++ ++ int radius = lastLoadedRadius + 1; ++ int viewDistance = PlayerChunkMap.this.playerViewDistanceBroadcastMap.getLastViewDistance(player); ++ ++ if (radius > viewDistance) { ++ // distance map will unload our chunks ++ this.lastLoadedRadiusByPlayer.put(playerId, viewDistance); ++ continue; ++ } ++ ++ int totalChunkSends = 0; ++ ++ if (totalChunkSends >= maxChunkSends) { ++ continue; ++ } ++ ++ radius_loop: ++ for (; radius <= viewDistance; ++radius) { ++ for (int offset = 0; offset <= radius; ++offset) { ++ // try to load the chunks closest to the player by distance ++ // so instead of going left->right on the x axis, we start at the center of the view distance square ++ // and go left and right at the same time ++ ++ // try top 2 chunks ++ // top left ++ int attempt = 0; ++ if ((attempt = this.trySendChunk(newX - offset, newZ + radius, player)) == QUEUED) { ++ if (++totalChunkSends >= maxChunkSends) { ++ break radius_loop; ++ } ++ } else if (attempt == FAILED) { ++ break radius_loop; ++ } ++ ++ // top right ++ if ((attempt = this.trySendChunk(newX + offset, newZ + radius, player)) == QUEUED) { ++ if (++totalChunkSends >= maxChunkSends) { ++ break radius_loop; ++ } ++ } else if (attempt == FAILED) { ++ break radius_loop; ++ } ++ ++ // try bottom 2 chunks ++ ++ // bottom left ++ if ((attempt = this.trySendChunk(newX - offset, newZ - radius, player)) == QUEUED) { ++ if (++totalChunkSends >= maxChunkSends) { ++ break radius_loop; ++ } ++ } else if (attempt == FAILED) { ++ break radius_loop; ++ } ++ ++ // bottom right ++ if ((attempt = this.trySendChunk(newX + offset, newZ - radius, player)) == QUEUED) { ++ if (++totalChunkSends >= maxChunkSends) { ++ break radius_loop; ++ } ++ } else if (attempt == FAILED) { ++ break radius_loop; ++ } ++ ++ // try left 2 chunks ++ ++ // left down ++ if ((attempt = this.trySendChunk(newX - radius, newZ - offset, player)) == QUEUED) { ++ if (++totalChunkSends >= maxChunkSends) { ++ break radius_loop; ++ } ++ } else if (attempt == FAILED) { ++ break radius_loop; ++ } ++ ++ // left up ++ if ((attempt = this.trySendChunk(newX - radius, newZ + offset, player)) == QUEUED) { ++ if (++totalChunkSends >= maxChunkSends) { ++ break radius_loop; ++ } ++ } else if (attempt == FAILED) { ++ break radius_loop; ++ } ++ ++ // try right 2 chunks ++ ++ // right down ++ if ((attempt = this.trySendChunk(newX + radius, newZ - offset, player)) == QUEUED) { ++ if (++totalChunkSends >= maxChunkSends) { ++ break radius_loop; ++ } ++ } else if (attempt == FAILED) { ++ break radius_loop; ++ } ++ ++ // right up ++ if ((attempt = this.trySendChunk(newX + radius, newZ + offset, player)) == QUEUED) { ++ if (++totalChunkSends >= maxChunkSends) { ++ break radius_loop; ++ } ++ } else if (attempt == FAILED) { ++ break radius_loop; ++ } ++ } ++ } ++ int newLoadedRadius = radius - 1; ++ if (newLoadedRadius != lastLoadedRadius) { ++ this.lastLoadedRadiusByPlayer.put(playerId, newLoadedRadius); ++ } ++ } ++ this.cachedChunkPackets.clear(); ++ } ++ } ++ ++ final class ChunkLoadScheduler implements com.destroystokyo.paper.util.misc.PlayerDistanceTrackingAreaMap.DistanceChangeCallback { ++ ++ // higher priroity = lower index ++ // index = distance from player ++ private final it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet[] queuedLoadsByPriority = new it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet[65]; ++ private final java.util.BitSet nonEmptyQueues = new java.util.BitSet(this.queuedLoadsByPriority.length); ++ ++ private final it.unimi.dsi.fastutil.longs.Long2LongLinkedOpenHashMap pendingLoads = new it.unimi.dsi.fastutil.longs.Long2LongLinkedOpenHashMap(64, 0.7f); ++ { ++ this.pendingLoads.defaultReturnValue(-1L); ++ } ++ ++ private final it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet loadedChunks = new it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet(8192, 0.7f); ++ ++ private long chunkLoadCounter; ++ ++ { ++ for (int i = 0, len = this.queuedLoadsByPriority.length; i < len; ++i) { ++ this.queuedLoadsByPriority[i] = new it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet(32, 0.7f); ++ } ++ } ++ ++ private final int maxPendingLoads; ++ ++ ChunkLoadScheduler(int maxPendingLoads) { ++ this.maxPendingLoads = maxPendingLoads; ++ } ++ ++ void queueMaxLoads() { ++ while (this.pendingLoads.size() < this.maxPendingLoads) { ++ int firstQueue = this.nonEmptyQueues.nextSetBit(0); ++ if (firstQueue == -1) { ++ // nothing left ++ return; ++ } ++ ++ long coordinate = this.queuedLoadsByPriority[firstQueue].removeFirstLong(); ++ if (this.queuedLoadsByPriority[firstQueue].isEmpty()) { ++ this.nonEmptyQueues.clear(firstQueue); ++ } ++ ChunkCoordIntPair chunkPos = new ChunkCoordIntPair(coordinate); ++ PlayerChunkMap.this.world.getChunkProvider().addTicketAtLevel(TicketType.PLAYER, chunkPos, 33, chunkPos); ++ ++ // now hook into the load ++ ++ PlayerChunkMap.this.world.getChunkProvider().tickDistanceManager(); // bring the chunkholder up to 33 ++ PlayerChunk chunkHolder = PlayerChunkMap.this.getUpdatingChunk(coordinate); ++ ++ CompletableFuture> loadFuture = chunkHolder.getFullChunkFuture(); ++ ++ final long loadCounterExpected = this.chunkLoadCounter++; ++ ++ this.pendingLoads.put(coordinate, loadCounterExpected); ++ ++ loadFuture.thenAcceptAsync((either) -> { ++ // no matter what, try to load more chunks ++ ++ if (!ChunkLoadScheduler.this.pendingLoads.remove(coordinate, loadCounterExpected)) { ++ // something cancelled us (or we multi loaded) ++ ChunkLoadScheduler.this.queueMaxLoads(); ++ return; ++ } ++ ChunkLoadScheduler.this.loadedChunks.add(coordinate); ++ ChunkLoadScheduler.this.queueMaxLoads(); ++ }, PlayerChunkMap.this.world.getChunkProvider().serverThreadQueue).exceptionally((throwable) -> { ++ MinecraftServer.LOGGER.fatal("Failed to handle load future for player chunk load", throwable); ++ return null; ++ }); ++ } ++ } ++ ++ @Override ++ public void accept(int posX, int posZ, int oldNearestDistance, int newNearestDistance, com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet state) { ++ long coordinate = net.minecraft.server.MCUtil.getCoordinateKey(posX, posZ); ++ ++ if (newNearestDistance == -1 && ((this.loadedChunks.remove(coordinate)) || (this.pendingLoads.remove(coordinate) != -1L))) { ++ // load needs to be dropped. ++ ChunkCoordIntPair chunkPos = new ChunkCoordIntPair(posX, posZ); ++ PlayerChunkMap.this.world.getChunkProvider().removeTicketAtLevel(TicketType.PLAYER, chunkPos, 33, chunkPos); ++ return; ++ } ++ ++ if (this.loadedChunks.contains(coordinate) || this.pendingLoads.containsKey(coordinate)) { ++ // no need to queue - we're already loaded/loading ++ return; ++ } ++ ++ if (oldNearestDistance != -1) { ++ // remove from old ++ it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet queue = this.queuedLoadsByPriority[oldNearestDistance]; ++ if (queue.remove(coordinate) && queue.isEmpty()) { ++ this.nonEmptyQueues.clear(oldNearestDistance); ++ } ++ ++ if (newNearestDistance == -1) { ++ // we're done here - removed from the queue, and there is no pending load ++ return; ++ } ++ } ++ ++ // queue the load ++ this.queuedLoadsByPriority[newNearestDistance].add(coordinate); ++ this.nonEmptyQueues.set(newNearestDistance); ++ } ++ } ++ final ChunkLoadScheduler chunkLoadScheduler; ++ ++ // Tuinity end - per player view distance ++ + void addPlayerToDistanceMaps(EntityPlayer player) { + this.updateMaps(player); + +@@ -142,10 +490,22 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + + void addPlayerToDistanceMapsTuinity(EntityPlayer player) { + this.updateMapsTuinity(player); ++ ++ // Tuinity start - per player view distance ++ this.getChunkMapDistanceManager().playerTickViewDistanceHandler.addPlayer(player); ++ this.chunkSendThrottler.addPlayer(player); ++ // Tuinity end - per player view distance + } + + void removePlayerFromDistanceMapsTuinity(EntityPlayer player) { + this.playerViewDistanceMap.remove(player); ++ // Tuinity start - per player view distance ++ this.playerViewDistanceBroadcastMap.remove(player); ++ this.playerViewDistanceTickMap.remove(player); ++ this.playerViewDistanceNoTickMap.remove(player); ++ this.getChunkMapDistanceManager().playerTickViewDistanceHandler.removePlayer(player); ++ this.chunkSendThrottler.removePlayer(player); ++ // Tuinity end - per player view distance + } + + void updateDistanceMapsTuinity(EntityPlayer player) { +@@ -156,7 +516,20 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + int chunkX = net.minecraft.server.MCUtil.getChunkCoordinate(player.locX()); + int chunkZ = net.minecraft.server.MCUtil.getChunkCoordinate(player.locZ()); + +- this.playerViewDistanceMap.update(player, chunkX, chunkZ, this.viewDistance); ++ this.playerViewDistanceMap.update(player, chunkX, chunkZ, player.getEffectiveViewDistance(this)); // Tuinity - per player view distance ++ ++ // Tuinity start - per player view distance ++ int effectiveViewDistance = player.getEffectiveViewDistance(this); ++ int effectiveNoTickViewDistance = Math.max(effectiveViewDistance, player.getEffectiveNoTickViewDistance(this)); ++ ++ if (!this.cannotLoadChunks(player)) { ++ this.playerViewDistanceTickMap.update(player, chunkX, chunkZ, effectiveViewDistance); ++ this.playerViewDistanceNoTickMap.update(player, chunkX, chunkZ, effectiveNoTickViewDistance + 2); // clients need chunk neighbours // add an extra one for antixray ++ } ++ player.needsChunkCenterUpdate = true; ++ this.playerViewDistanceBroadcastMap.update(player, chunkX, chunkZ, effectiveNoTickViewDistance + 1); // clients need chunk neighbours ++ player.needsChunkCenterUpdate = false; ++ // Tuinity end - per player view distance + } + // Tuinity end + +@@ -230,6 +603,37 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + com.destroystokyo.paper.util.misc.PooledLinkedHashSets sets = this.pooledEntityPlayerSets; + this.playerViewDistanceMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(sets); + // Tuinity end - distance maps ++ // Tuinity start - per player view distance ++ this.chunkLoadScheduler = new ChunkLoadScheduler(this.world.akarinConfig.maxPendingChunkLoads); ++ this.setNoTickViewDistance(this.world.akarinConfig.noTickViewDistance); ++ this.playerViewDistanceTickMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(sets, ++ null, ++ (EntityPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, ++ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { ++ if (newState != null) { ++ return; ++ } ++ PlayerChunkMap.this.chunkDistanceManager.playerTickViewDistanceHandler.playerMoveOutOfRange(rangeX, rangeZ); ++ }); ++ this.chunkDistanceManager.playerTickViewDistanceHandler.areaMap = this.playerViewDistanceTickMap; ++ this.playerViewDistanceNoTickMap = new com.destroystokyo.paper.util.misc.PlayerDistanceTrackingAreaMap(sets, ++ null, ++ null, ++ this.chunkLoadScheduler); ++ this.playerViewDistanceBroadcastMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(sets, ++ (EntityPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, ++ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { ++ if (player.needsChunkCenterUpdate) { ++ player.needsChunkCenterUpdate = false; ++ player.playerConnection.sendPacket(new PacketPlayOutViewCentre(currPosX, currPosZ)); ++ } ++ }, ++ (EntityPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, ++ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { ++ PlayerChunkMap.this.sendChunk(player, new ChunkCoordIntPair(rangeX, rangeZ), null, true, false); // unloaded, loaded ++ player.loadedChunks.remove(net.minecraft.server.MCUtil.getCoordinateKey(rangeX, rangeZ)); ++ }); ++ // Tuinity end - per player view distance + } + + public void updatePlayerMobTypeMap(Entity entity) { +@@ -1053,11 +1457,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + completablefuture1.thenAcceptAsync((either) -> { + either.mapLeft((chunk) -> { + this.u.getAndIncrement(); +- Packet[] apacket = new Packet[2]; +- +- this.a(chunkcoordintpair, false).forEach((entityplayer) -> { +- this.a(entityplayer, apacket, chunk); +- }); ++ // Tuinity - per player view distance - moved to full chunk load, instead of ticking load + return Either.left(chunk); + }); + }, (runnable) -> { +@@ -1161,32 +1561,39 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + } // Paper + } + +- protected void setViewDistance(int i) { +- int j = MathHelper.clamp(i + 1, 3, 33); ++ public void setViewDistance(int i) { // Tuinity - make public ++ int j = MathHelper.clamp(i + 1, 3, 33) - 1; // Tuinity - we correctly handle view distance, no need to add 1 + + if (j != this.viewDistance) { + int k = this.viewDistance; + + this.viewDistance = j; +- this.chunkDistanceManager.a(this.viewDistance); +- ObjectIterator objectiterator = this.updatingChunks.values().iterator(); +- +- while (objectiterator.hasNext()) { +- PlayerChunk playerchunk = (PlayerChunk) objectiterator.next(); +- ChunkCoordIntPair chunkcoordintpair = playerchunk.i(); +- Packet[] apacket = new Packet[2]; ++ // Tuinity start - view distance api ++ this.chunkDistanceManager.setGlobalViewDistance(this.viewDistance, this); ++ if (this.world != null && this.world.players != null) { // ... called inside constructor, where these may not be initialized ++ for (EntityPlayer player : this.world.players) { ++ this.updateViewDistance(player, player.getRawViewDistance(), player.getRawNoTickViewDistance()); ++ } ++ } ++ // Tuinity end - view distance api ++ } + +- this.a(chunkcoordintpair, false).forEach((entityplayer) -> { +- int l = b(chunkcoordintpair, entityplayer, true); +- boolean flag = l <= k; +- boolean flag1 = l <= this.viewDistance; ++ } + +- this.sendChunk(entityplayer, chunkcoordintpair, apacket, flag, flag1); +- }); ++ // Tuinity start - no tick view distance ++ public void setNoTickViewDistance(int noTickViewDistance) { ++ // modeled after the above ++ noTickViewDistance = noTickViewDistance < 0 ? -1 : MathHelper.clamp(noTickViewDistance, 2, 32); ++ if (this.noTickViewDistance != noTickViewDistance) { ++ this.noTickViewDistance = noTickViewDistance; ++ if (this.world != null && this.world.players != null) { // ... called inside constructor, where these may not be initialized ++ for (EntityPlayer player : this.world.players) { ++ this.updateViewDistance(player, player.getRawViewDistance(), player.getRawNoTickViewDistance()); ++ } + } + } +- + } ++ // Tuinity end - no tick view distance + + protected void sendChunk(EntityPlayer entityplayer, ChunkCoordIntPair chunkcoordintpair, Packet[] apacket, boolean flag, boolean flag1) { + if (entityplayer.world == this.world) { +@@ -1194,7 +1601,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + PlayerChunk playerchunk = this.getVisibleChunk(chunkcoordintpair.pair()); + + if (playerchunk != null) { +- Chunk chunk = playerchunk.getChunk(); ++ Chunk chunk = playerchunk.getFullReadyChunk(); // Tuinity - per player view distance + + if (chunk != null) { + this.a(entityplayer, apacket, chunk); +@@ -1440,6 +1847,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + }); + } + ++ private boolean cannotLoadChunks(EntityPlayer entityplayer) { return this.b(entityplayer); } // Tuinity - OBFHELPER + private boolean b(EntityPlayer entityplayer) { + return entityplayer.isSpectator() && !this.world.getGameRules().getBoolean(GameRules.SPECTATORS_GENERATE_CHUNKS); + } +@@ -1465,13 +1873,11 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + } + } + +- for (int k = i - this.viewDistance; k <= i + this.viewDistance; ++k) { +- for (int l = j - this.viewDistance; l <= j + this.viewDistance; ++l) { +- ChunkCoordIntPair chunkcoordintpair = new ChunkCoordIntPair(k, l); +- +- this.sendChunk(entityplayer, chunkcoordintpair, new Packet[2], !flag, flag); +- } ++ // Tuinity start - view distance map handles this ++ if (flag) { ++ this.updateDistanceMapsTuinity(entityplayer); + } ++ // Tuinity end - view distance map handles this + + } + +@@ -1479,7 +1885,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + SectionPosition sectionposition = SectionPosition.a((Entity) entityplayer); + + entityplayer.a(sectionposition); +- entityplayer.playerConnection.sendPacket(new PacketPlayOutViewCentre(sectionposition.a(), sectionposition.c())); ++ //entityplayer.playerConnection.sendPacket(new PacketPlayOutViewCentre(sectionposition.a(), sectionposition.c())); // Tuinity - distance map handles this now + return sectionposition; + } + +@@ -1546,56 +1952,54 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + int k1; + int l1; + +- if (Math.abs(i1 - i) <= this.viewDistance * 2 && Math.abs(j1 - j) <= this.viewDistance * 2) { +- k1 = Math.min(i, i1) - this.viewDistance; +- l1 = Math.min(j, j1) - this.viewDistance; +- int i2 = Math.max(i, i1) + this.viewDistance; +- int j2 = Math.max(j, j1) + this.viewDistance; ++ // Tuinity - handled by distance map ++ this.updateMaps(entityplayer); // Paper - distance maps ++ this.updateDistanceMapsTuinity(entityplayer); // Tuinity - distance maps ++ } + +- for (int k2 = k1; k2 <= i2; ++k2) { +- for (int l2 = l1; l2 <= j2; ++l2) { +- ChunkCoordIntPair chunkcoordintpair = new ChunkCoordIntPair(k2, l2); +- boolean flag3 = a(chunkcoordintpair, i1, j1) <= this.viewDistance; +- boolean flag4 = a(chunkcoordintpair, i, j) <= this.viewDistance; ++ @Override ++ public Stream a(ChunkCoordIntPair chunkcoordintpair, boolean flag) { ++ // Tuinity start - per player view distance ++ // there can be potential desync with player's last mapped section and the view distance map, so use the ++ // view distance map here. ++ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet inRange = this.playerViewDistanceBroadcastMap.getObjectsInRange(chunkcoordintpair); ++ ++ if (inRange == null) { ++ return Stream.empty(); ++ } ++ // all current cases are inlined so we wont hit this code, it's just in case plugins or future updates use it ++ List players = new ArrayList<>(); ++ Object[] backingSet = inRange.getBackingSet(); ++ ++ if (flag) { // flag -> border only ++ for (int i = 0, len = backingSet.length; i < len; ++i) { ++ Object temp = backingSet[i]; ++ if (!(temp instanceof EntityPlayer)) { ++ continue; ++ } ++ EntityPlayer player = (EntityPlayer)temp; ++ int viewDistance = this.playerViewDistanceBroadcastMap.getLastViewDistance(player); ++ long lastPosition = this.playerViewDistanceBroadcastMap.getLastCoordinate(player); + +- this.sendChunk(entityplayer, chunkcoordintpair, new Packet[2], flag3, flag4); ++ int distX = Math.abs(net.minecraft.server.MCUtil.getCoordinateX(lastPosition) - chunkcoordintpair.x); ++ int distZ = Math.abs(net.minecraft.server.MCUtil.getCoordinateZ(lastPosition) - chunkcoordintpair.z); ++ ++ if (Math.max(distX, distZ) == viewDistance) { ++ players.add(player); + } + } + } else { +- ChunkCoordIntPair chunkcoordintpair1; +- boolean flag5; +- boolean flag6; +- +- for (k1 = i1 - this.viewDistance; k1 <= i1 + this.viewDistance; ++k1) { +- for (l1 = j1 - this.viewDistance; l1 <= j1 + this.viewDistance; ++l1) { +- chunkcoordintpair1 = new ChunkCoordIntPair(k1, l1); +- flag5 = true; +- flag6 = false; +- this.sendChunk(entityplayer, chunkcoordintpair1, new Packet[2], true, false); +- } +- } +- +- for (k1 = i - this.viewDistance; k1 <= i + this.viewDistance; ++k1) { +- for (l1 = j - this.viewDistance; l1 <= j + this.viewDistance; ++l1) { +- chunkcoordintpair1 = new ChunkCoordIntPair(k1, l1); +- flag5 = false; +- flag6 = true; +- this.sendChunk(entityplayer, chunkcoordintpair1, new Packet[2], false, true); ++ for (int i = 0, len = backingSet.length; i < len; ++i) { ++ Object temp = backingSet[i]; ++ if (!(temp instanceof EntityPlayer)) { ++ continue; + } ++ EntityPlayer player = (EntityPlayer)temp; ++ players.add(player); + } + } + +- this.updateMaps(entityplayer); // Paper - distance maps +- this.updateDistanceMapsTuinity(entityplayer); // Tuinity - distance maps +- } +- +- @Override +- public Stream a(ChunkCoordIntPair chunkcoordintpair, boolean flag) { +- return this.playerMap.a(chunkcoordintpair.pair()).filter((entityplayer) -> { +- int i = b(chunkcoordintpair, entityplayer, true); +- +- return i > this.viewDistance ? false : !flag || i == this.viewDistance; +- }); ++ return players.stream(); + } + + protected void addEntity(Entity entity) { +@@ -1733,6 +2137,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + + } + ++ final void sendChunk(EntityPlayer entityplayer, Packet[] apacket, Chunk chunk) { this.a(entityplayer, apacket, chunk); } // Tuinity - OBFHELPER + private void a(EntityPlayer entityplayer, Packet[] apacket, Chunk chunk) { + if (apacket[0] == null) { + apacket[0] = new PacketPlayOutMapChunk(chunk, 65535, true); // Paper - Anti-Xray +@@ -1866,7 +2271,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + org.spigotmc.AsyncCatcher.catchOp("player tracker update"); // Spigot + if (entityplayer != this.tracker) { + Vec3D vec3d = entityplayer.getPositionVector().d(this.tracker.getPositionVector()); // MC-155077, SPIGOT-5113 +- int i = Math.min(this.b(), (PlayerChunkMap.this.viewDistance - 1) * 16); ++ int i = Math.min(this.b(), (entityplayer.getEffectiveViewDistance(PlayerChunkMap.this)) * 16); // Tuinity - per player view distance + boolean flag = vec3d.x >= (double) (-i) && vec3d.x <= (double) i && vec3d.z >= (double) (-i) && vec3d.z <= (double) i && this.tracker.a(entityplayer); + + if (flag) { +@@ -1877,7 +2282,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + PlayerChunk playerchunk = PlayerChunkMap.this.getVisibleChunk(chunkcoordintpair.pair()); + + if (playerchunk != null && playerchunk.getChunk() != null) { +- flag1 = PlayerChunkMap.b(chunkcoordintpair, entityplayer, false) <= PlayerChunkMap.this.viewDistance; ++ flag1 = PlayerChunkMap.b(chunkcoordintpair, entityplayer, false) <= (1 + PlayerChunkMap.this.playerViewDistanceTickMap.getLastViewDistance(entityplayer)); // Tuinity - per player view distance + } + } + +diff --git a/src/main/java/net/minecraft/server/PlayerList.java b/src/main/java/net/minecraft/server/PlayerList.java +index 9ca3fdc9cf5df3651d9b9267806c9f4b566515cb..723f781b2e4d015703b0e0488893352fdcb17e86 100644 +--- a/src/main/java/net/minecraft/server/PlayerList.java ++++ b/src/main/java/net/minecraft/server/PlayerList.java +@@ -157,7 +157,7 @@ public abstract class PlayerList { + + // CraftBukkit - getType() + // Spigot - view distance +- playerconnection.sendPacket(new PacketPlayOutLogin(entityplayer.getId(), entityplayer.playerInteractManager.getGameMode(), WorldData.c(worlddata.getSeed()), worlddata.isHardcore(), worldserver.worldProvider.getDimensionManager().getType(), this.getMaxPlayers(), worlddata.getType(), worldserver.spigotConfig.viewDistance, flag1, !flag)); ++ playerconnection.sendPacket(new PacketPlayOutLogin(entityplayer.getId(), entityplayer.playerInteractManager.getGameMode(), WorldData.c(worlddata.getSeed()), worlddata.isHardcore(), worldserver.worldProvider.getDimensionManager().getType(), this.getMaxPlayers(), worlddata.getType(), Math.max(entityplayer.getEffectiveViewDistance(worldserver.getChunkProvider().playerChunkMap), entityplayer.getEffectiveNoTickViewDistance(worldserver.getChunkProvider().playerChunkMap)), flag1, !flag)); // Tuinity - per player view distance + entityplayer.getBukkitEntity().sendSupportedChannels(); // CraftBukkit + // Akarin start - send multiple packets at once + /* +@@ -714,7 +714,7 @@ public abstract class PlayerList { + WorldData worlddata = worldserver.getWorldData(); + + entityplayer1.playerConnection.sendPacket(new PacketPlayOutRespawn(worldserver.worldProvider.getDimensionManager().getType(), WorldData.c(worldserver.getWorldData().getSeed()), worldserver.getWorldData().getType(), entityplayer1.playerInteractManager.getGameMode())); +- entityplayer1.playerConnection.sendPacket(new PacketPlayOutViewDistance(worldserver.spigotConfig.viewDistance)); // Spigot ++ entityplayer1.playerConnection.sendPacket(new PacketPlayOutViewDistance(Math.max(entityplayer1.getEffectiveViewDistance(worldserver.getChunkProvider().playerChunkMap), entityplayer1.getEffectiveNoTickViewDistance(worldserver.getChunkProvider().playerChunkMap)))); // Spigot // Tuinity - per player view distance + entityplayer1.spawnIn(worldserver); + entityplayer1.dead = false; + entityplayer1.playerConnection.teleport(new Location(worldserver.getWorld(), entityplayer1.locX(), entityplayer1.locY(), entityplayer1.locZ(), entityplayer1.yaw, entityplayer1.pitch)); +@@ -1198,7 +1198,7 @@ public abstract class PlayerList { + + public void a(int i) { + this.viewDistance = i; +- this.sendAll(new PacketPlayOutViewDistance(i)); ++ //this.sendAll(new PacketPlayOutViewDistance(i)); // Tuinity - move into setViewDistance + Iterator iterator = this.server.getWorlds().iterator(); + + while (iterator.hasNext()) { +diff --git a/src/main/java/net/minecraft/server/World.java b/src/main/java/net/minecraft/server/World.java +index 7129cb0260316e166b4b15cded0e0c0b32433b7f..804efba68124cc4e3b7acb071b3c369c682ec4ec 100644 +--- a/src/main/java/net/minecraft/server/World.java ++++ b/src/main/java/net/minecraft/server/World.java +@@ -440,8 +440,13 @@ public abstract class World implements GeneratorAccess, AutoCloseable { + this.b(blockposition, iblockdata1, iblockdata2); + } + +- if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0) && (this.isClientSide || chunk == null || (chunk.getState() != null && chunk.getState().isAtLeast(PlayerChunk.State.TICKING)))) { // allow chunk to be null here as chunk.isReady() is false when we send our notification during block placement ++ if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0) && (this.isClientSide || chunk == null || (chunk.getState() != null && chunk.getState().isAtLeast(PlayerChunk.State.TICKING)))) { // allow chunk to be null here as chunk.isReady() is false when we send our notification during block placement // Tuinity - diff on change, see below + this.notify(blockposition, iblockdata1, iblockdata, i); ++ // Tuinity start - per player view distance - allow block updates for non-ticking chunks in player view distance ++ // if copied from above ++ } else if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0) && (this.isClientSide || chunk == null || ((WorldServer)this).getChunkProvider().playerChunkMap.playerViewDistanceBroadcastMap.getObjectsInRange(net.minecraft.server.MCUtil.getCoordinateKey(blockposition)) != null)) { ++ ((WorldServer)this).getChunkProvider().flagDirty(blockposition); ++ // Tuinity end - per player view distance + } + + if (!this.isClientSide && (i & 1) != 0) { +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +index 0f3199ba4a228cc60db3abfa4ac926b92cbb4b4b..2b15ddcaa2dbfa0721ddc18bfd6e85e5eb2bb1cb 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +@@ -3,6 +3,7 @@ package org.bukkit.craftbukkit; + import com.google.common.base.Preconditions; + import com.google.common.collect.ImmutableList; + import com.google.common.collect.ImmutableMap; ++import com.tuinity.tuinity.util.TickThread; + import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; + import java.io.File; + import java.io.IOException; +@@ -2486,10 +2487,43 @@ public class CraftWorld implements World { + // Spigot start + @Override + public int getViewDistance() { +- return world.spigotConfig.viewDistance; ++ return getHandle().getChunkProvider().playerChunkMap.getViewDistance(); // Tuinity start - per player view distance + } + // Spigot end + ++ // Tuinity start - per player view distance ++ ++ ++ @Override ++ public void setViewDistance(int viewDistance) { ++ TickThread.ensureTickThread("Cannot update view distance safely off of the main thread"); ++ if (viewDistance < 2 || viewDistance > 32) { ++ throw new IllegalArgumentException("View distance " + viewDistance + " is out of range of [2, 32]"); ++ } ++ net.minecraft.server.PlayerChunkMap chunkMap = getHandle().getChunkProvider().playerChunkMap; ++ if (viewDistance != chunkMap.getViewDistance()) { ++ chunkMap.setViewDistance(viewDistance); ++ } ++ } ++ ++ @Override ++ public int getNoTickViewDistance() { ++ return getHandle().getChunkProvider().playerChunkMap.getEffectiveNoTickViewDistance(); ++ } ++ ++ @Override ++ public void setNoTickViewDistance(int viewDistance) { ++ TickThread.ensureTickThread("Cannot update view distance safely off of the main thread"); ++ if (viewDistance < 2 || viewDistance > 32) { ++ throw new IllegalArgumentException("View distance " + viewDistance + " is out of range of [2, 32]"); ++ } ++ net.minecraft.server.PlayerChunkMap chunkMap = getHandle().getChunkProvider().playerChunkMap; ++ if (viewDistance != chunkMap.getRawNoTickViewDistance()) { ++ chunkMap.setNoTickViewDistance(viewDistance); ++ } ++ } ++ // Tuinity end - per player view distance ++ + // Spigot start + private final Spigot spigot = new Spigot() + { +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +index b76379a17ca4c1ad009acfd4407d103ecb476a4e..f0c7e5c2145f42818701c91ee6b2fcde16bd12e0 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +@@ -3,6 +3,7 @@ package org.bukkit.craftbukkit.entity; + import com.destroystokyo.paper.ClientOption.ChatVisibility; + import com.destroystokyo.paper.PaperSkinParts; + import com.destroystokyo.paper.ClientOption; ++import com.tuinity.tuinity.util.TickThread; // Tuinity + import com.destroystokyo.paper.Title; + import com.destroystokyo.paper.profile.CraftPlayerProfile; + import com.destroystokyo.paper.profile.PlayerProfile; +@@ -1966,13 +1967,39 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + + @Override + public int getViewDistance() { +- throw new NotImplementedException("Per-Player View Distance APIs need further understanding to properly implement"); // TODO ++ return getHandle().getEffectiveViewDistance(); // Tuinity - per player view distance + } + + @Override + public void setViewDistance(int viewDistance) { +- throw new NotImplementedException("Per-Player View Distance APIs need further understanding to properly implement"); // TODO ++ // Tuinity start - per player view distance ++ TickThread.ensureTickThread("Cannot update view distance safely off of the main thread"); ++ if ((viewDistance < 2 || viewDistance > 32) && viewDistance != -1) { ++ throw new IllegalArgumentException("View distance " + viewDistance + " is out of range of [2, 32]"); ++ } ++ if (viewDistance != getHandle().getRawViewDistance()) { ++ ((WorldServer)getHandle().world).getChunkProvider().playerChunkMap.updateViewDistance(getHandle(), viewDistance, getHandle().getRawNoTickViewDistance()); ++ } ++ // Tuinity end - per player view distance ++ } ++ ++ // Tuinity start - per player view distance ++ @Override ++ public int getNoTickViewDistance() { ++ return getHandle().getEffectiveNoTickViewDistance(); ++ } ++ ++ @Override ++ public void setNoTickViewDistance(int viewDistance) { ++ TickThread.ensureTickThread("Cannot update view distance safely off of the main thread"); ++ if ((viewDistance < 2 || viewDistance > 32) && viewDistance != -1) { ++ throw new IllegalArgumentException("View distance " + viewDistance + " is out of range of [2, 32]"); ++ } ++ if (viewDistance != getHandle().getRawNoTickViewDistance()) { ++ ((WorldServer)getHandle().world).getChunkProvider().playerChunkMap.updateViewDistance(getHandle(), getHandle().getRawViewDistance(), viewDistance); ++ } + } ++ // Tuinity end - per player view distance + + @Override + public T getClientOption(ClientOption type) { +diff --git a/src/main/java/org/spigotmc/ActivationRange.java b/src/main/java/org/spigotmc/ActivationRange.java +index 795817177aae00a5157ae77b2192841c8d028732..f6e8f7d9c64bf8f34b13636e4d0e13746fd2c4f4 100644 +--- a/src/main/java/org/spigotmc/ActivationRange.java ++++ b/src/main/java/org/spigotmc/ActivationRange.java +@@ -46,6 +46,7 @@ import net.minecraft.server.EntityInsentient; + import net.minecraft.server.EntityLlama; + import net.minecraft.server.EntityWaterAnimal; + // Paper end ++import net.minecraft.server.WorldServer; // Tuinity + + public class ActivationRange + { +@@ -137,14 +138,15 @@ public class ActivationRange + final int waterActivationRange = world.spigotConfig.waterActivationRange; // Paper + final ChunkProviderServer chunkProvider = (ChunkProviderServer) world.getChunkProvider(); // Paper + +- int maxRange = Math.max( monsterActivationRange, animalActivationRange ); +- maxRange = Math.max( maxRange, raiderActivationRange ); +- maxRange = Math.max( maxRange, miscActivationRange ); +- maxRange = Math.min( ( world.spigotConfig.viewDistance << 4 ) - 8, maxRange ); ++ // Tuinity start - per player view distance ++ int maxRangeTemp = Math.max( monsterActivationRange, animalActivationRange ); ++ maxRangeTemp = Math.max( maxRangeTemp, raiderActivationRange ); ++ maxRangeTemp = Math.max( maxRangeTemp, miscActivationRange ); + + for ( EntityHuman player : world.getPlayers() ) + { +- ++ final int maxRange = Math.min( ( ( player instanceof net.minecraft.server.EntityPlayer ? ((net.minecraft.server.EntityPlayer)player).getEffectiveViewDistance(((WorldServer)world).getChunkProvider().playerChunkMap) : world.spigotConfig.viewDistance ) << 4 ) - 8, maxRangeTemp ); ++ // Tuinity end - per player view distance + player.activatedTick = MinecraftServer.currentTick; + maxBB = player.getBoundingBox().grow( maxRange, 256, maxRange ); + ActivationType.MISC.boundingBox = player.getBoundingBox().grow( miscActivationRange, 256, miscActivationRange ); diff --git a/scripts/importSources.sh b/scripts/importSources.sh index eaebac897..7a7d47a87 100755 --- a/scripts/importSources.sh +++ b/scripts/importSources.sh @@ -121,6 +121,7 @@ done importToPaperWorkspace CommandGive importToPaperWorkspace PathDestination +importToPaperWorkspace HeightMap # Library import format (multiple files are supported): # importLibraryToPaperWorkspace com.mojang datafixerupper com/mojang/datafixers/util Either.java @@ -138,4 +139,4 @@ importToPaperWorkspace PathDestination echo " Subtask finished" echo "----------------------------------------" fi -) \ No newline at end of file +)