2023-02-23 17:13:45 +01:00
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Spottedleaf <Spottedleaf@users.noreply.github.com>
Date: Wed, 1 Feb 2023 21:06:31 -0800
Subject: [PATCH] New player chunk loader system
diff --git a/src/main/java/co/aikar/timings/TimingsExport.java b/src/main/java/co/aikar/timings/TimingsExport.java
2023-03-30 03:26:03 +02:00
index c07eb451a576811a39021f6f97103c77488fd001..a2f71a6d1a9e98133dff6cd0f625da9435a8af14 100644
2023-02-23 17:13:45 +01:00
--- a/src/main/java/co/aikar/timings/TimingsExport.java
+++ b/src/main/java/co/aikar/timings/TimingsExport.java
2023-03-30 03:26:03 +02:00
@@ -164,9 +164,9 @@ public class TimingsExport extends Thread {
2023-02-23 17:13:45 +01:00
return pair(rule, world.getWorld().getGameRuleValue(rule));
})),
// Paper start - replace chunk loader system
- pair("ticking-distance", world.getChunkSource().chunkMap.playerChunkManager.getTargetTickViewDistance()),
- pair("no-ticking-distance", world.getChunkSource().chunkMap.playerChunkManager.getTargetNoTickViewDistance()),
- pair("sending-distance", world.getChunkSource().chunkMap.playerChunkManager.getTargetSendDistance())
+ pair("ticking-distance", world.getWorld().getSimulationDistance()),
+ pair("no-ticking-distance", world.getWorld().getViewDistance()),
+ pair("sending-distance", world.getWorld().getSendViewDistance())
// Paper end - replace chunk loader system
));
}));
diff --git a/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java b/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java
2023-03-25 00:22:47 +01:00
index 0e45a340ae534caf676b7f9d0adcbcee5829925e..61d03808c8d1ab822d9b2f31fab0de14089a3b15 100644
2023-02-23 17:13:45 +01:00
--- a/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java
+++ b/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java
@@ -129,15 +129,15 @@ public final class ChunkSystem {
}
public static int getSendViewDistance(final ServerPlayer player) {
- return io.papermc.paper.chunk.PlayerChunkLoader.getSendViewDistance(player);
2023-03-25 00:22:47 +01:00
+ return io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.getAPISendViewDistance(player);
2023-02-23 17:13:45 +01:00
}
public static int getLoadViewDistance(final ServerPlayer player) {
- return io.papermc.paper.chunk.PlayerChunkLoader.getLoadViewDistance(player);
2023-03-25 00:22:47 +01:00
+ return io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.getLoadViewDistance(player);
2023-02-23 17:13:45 +01:00
}
public static int getTickViewDistance(final ServerPlayer player) {
- return io.papermc.paper.chunk.PlayerChunkLoader.getTickViewDistance(player);
2023-03-25 00:22:47 +01:00
+ return io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.getAPITickViewDistance(player);
2023-02-23 17:13:45 +01:00
}
private ChunkSystem() {
2023-03-25 00:22:47 +01:00
diff --git a/src/main/java/io/papermc/paper/chunk/system/RegionizedPlayerChunkLoader.java b/src/main/java/io/papermc/paper/chunk/system/RegionizedPlayerChunkLoader.java
2023-02-23 17:13:45 +01:00
new file mode 100644
2023-03-25 00:22:47 +01:00
index 0000000000000000000000000000000000000000..99dc2038aa4fb0f70dc3670e451018ff541d3a85
2023-02-23 17:13:45 +01:00
--- /dev/null
2023-03-25 00:22:47 +01:00
+++ b/src/main/java/io/papermc/paper/chunk/system/RegionizedPlayerChunkLoader.java
Fix several issues, mostly saving pending teleporting entities
The place/portal async function now track entities that have been
removed from the world but have not teleported. When the server
shuts down, these entities will have their passenger tree restored
and re-added to the entity slices at the location they were teleporting to,
or in the case of portals that did not run placeAsync yet,
the location they entered the portal on. This should ensure that
for regular teleports that the entity is placed at its correct
target location, and for portalling to ensure that either
the entity is placed at the portal entrace location (where
they entered) or the portal destination. In any case,
the entity is preserved in a location and will survive
the shutdown.
Additionally, move player saving until after the worlds save. This
is to ensure that the save logic is performed only after
all teleportations have completed.
Fix some other misc issues as well:
- Fix double nether portal creation by checking if a portal exists again
before creating it, fixing a race condition where two entites would portal
and neither would see that the other created a portal.
- Make all remove ticket add an unknown ticket.
In general this behavior is better since it means that unloads will only
ever occur at the next tick, rather than during the tick logic. Thus,
there will be no cases where a chunk is unloaded unexpectedly.
- Do not use fastFloor for calculating chunk position from block position
It is not going to return a good value outside of [-1024, 1024]
- Always perform mid tick update for ticking regionised player chunk loader
If no entities were loaded, no chunks were loaded, and nothing else -
the logic would not have otherwise ran. This fixed some rare cases of
chunks never loading for players after logging in.
2023-03-02 04:12:31 +01:00
@@ -0,0 +1,1304 @@
2023-02-23 17:13:45 +01:00
+package io.papermc.paper.chunk.system;
+
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
+import io.papermc.paper.chunk.system.scheduling.ChunkHolderManager;
+import io.papermc.paper.configuration.GlobalConfiguration;
+import io.papermc.paper.util.CoordinateUtils;
+import io.papermc.paper.util.IntegerUtil;
+import io.papermc.paper.util.IntervalledCounter;
+import io.papermc.paper.util.TickThread;
+import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap;
+import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue;
+import it.unimi.dsi.fastutil.longs.LongArrayList;
+import it.unimi.dsi.fastutil.longs.LongComparator;
+import it.unimi.dsi.fastutil.longs.LongHeapPriorityQueue;
+import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
+import net.minecraft.network.protocol.Packet;
+import net.minecraft.network.protocol.game.ClientboundSetChunkCacheCenterPacket;
+import net.minecraft.network.protocol.game.ClientboundSetChunkCacheRadiusPacket;
+import net.minecraft.network.protocol.game.ClientboundSetSimulationDistancePacket;
+import net.minecraft.server.level.ChunkMap;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.server.level.TicketType;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.GameRules;
+import net.minecraft.world.level.chunk.ChunkAccess;
+import net.minecraft.world.level.chunk.ChunkStatus;
+import net.minecraft.world.level.chunk.LevelChunk;
+import net.minecraft.world.level.levelgen.BelowZeroRetrogen;
+import org.apache.commons.lang3.mutable.MutableObject;
+import org.bukkit.craftbukkit.entity.CraftPlayer;
+import org.bukkit.entity.Player;
+
+import java.util.ArrayDeque;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
2023-03-25 00:22:47 +01:00
+public class RegionizedPlayerChunkLoader {
2023-02-23 17:13:45 +01:00
+
+ public static final TicketType<Long> REGION_PLAYER_TICKET = TicketType.create("region_player_ticket", Long::compareTo);
+
+ public static final int MIN_VIEW_DISTANCE = 2;
+ public static final int MAX_VIEW_DISTANCE = 32;
+
+ public static final int TICK_TICKET_LEVEL = 31;
+ public static final int GENERATED_TICKET_LEVEL = 33 + ChunkStatus.getDistance(ChunkStatus.FULL);
+ public static final int LOADED_TICKET_LEVEL = 33 + ChunkStatus.getDistance(ChunkStatus.EMPTY);
+
+ public static final record ViewDistances(
+ int tickViewDistance,
+ int loadViewDistance,
+ int sendViewDistance
+ ) {
+ public ViewDistances setTickViewDistance(final int distance) {
+ return new ViewDistances(distance, this.loadViewDistance, this.sendViewDistance);
+ }
+
+ public ViewDistances setLoadViewDistance(final int distance) {
+ return new ViewDistances(this.tickViewDistance, distance, this.sendViewDistance);
+ }
+
+
+ public ViewDistances setSendViewDistance(final int distance) {
+ return new ViewDistances(this.tickViewDistance, this.loadViewDistance, distance);
+ }
+ }
+
+ public static int getAPITickViewDistance(final Player player) {
+ return getAPITickViewDistance(((CraftPlayer)player).getHandle());
+ }
+
+ public static int getAPITickViewDistance(final ServerPlayer player) {
+ final ServerLevel level = (ServerLevel)player.level;
+ final PlayerChunkLoaderData data = player.chunkLoader;
+ if (data == null) {
+ return level.playerChunkLoader.getAPITickDistance();
+ }
+ return data.lastTickDistance;
+ }
+
+ public static int getAPIViewDistance(final Player player) {
+ return getAPIViewDistance(((CraftPlayer)player).getHandle());
+ }
+
+ public static int getAPIViewDistance(final ServerPlayer player) {
+ final ServerLevel level = (ServerLevel)player.level;
+ final PlayerChunkLoaderData data = player.chunkLoader;
+ if (data == null) {
+ return level.playerChunkLoader.getAPIViewDistance();
+ }
+ // view distance = load distance + 1
+ return data.lastLoadDistance - 1;
+ }
+
+ public static int getLoadViewDistance(final ServerPlayer player) {
+ final ServerLevel level = (ServerLevel)player.level;
+ final PlayerChunkLoaderData data = player.chunkLoader;
+ if (data == null) {
+ return level.playerChunkLoader.getAPIViewDistance();
+ }
+ // view distance = load distance + 1
+ return data.lastLoadDistance - 1;
+ }
+
+ public static int getAPISendViewDistance(final Player player) {
+ return getAPISendViewDistance(((CraftPlayer)player).getHandle());
+ }
+
+ public static int getAPISendViewDistance(final ServerPlayer player) {
+ final ServerLevel level = (ServerLevel)player.level;
+ final PlayerChunkLoaderData data = player.chunkLoader;
+ if (data == null) {
+ return level.playerChunkLoader.getAPISendViewDistance();
+ }
+ return data.lastSendDistance;
+ }
+
+ private final ServerLevel world;
+
2023-03-25 00:22:47 +01:00
+ public RegionizedPlayerChunkLoader(final ServerLevel world) {
2023-02-23 17:13:45 +01:00
+ this.world = world;
+ }
+
+ public void addPlayer(final ServerPlayer player) {
+ TickThread.ensureTickThread(player, "Cannot add player to player chunk loader async");
+ if (!player.isRealPlayer) {
+ return;
+ }
+
+ if (player.chunkLoader != null) {
+ throw new IllegalStateException("Player is already added to player chunk loader");
+ }
+
+ final PlayerChunkLoaderData loader = new PlayerChunkLoaderData(this.world, player);
+
+ player.chunkLoader = loader;
+ loader.add();
+ }
+
+ public void updatePlayer(final ServerPlayer player) {
+ final PlayerChunkLoaderData loader = player.chunkLoader;
+ if (loader != null) {
+ loader.update();
+ }
+ }
+
+ public void removePlayer(final ServerPlayer player) {
+ TickThread.ensureTickThread(player, "Cannot remove player from player chunk loader async");
+ if (!player.isRealPlayer) {
+ return;
+ }
+
+ final PlayerChunkLoaderData loader = player.chunkLoader;
+
+ if (loader == null) {
+ throw new IllegalStateException("Player is already removed from player chunk loader");
+ }
+
+ loader.remove();
+ player.chunkLoader = null;
+ }
+
+ public void setSendDistance(final int distance) {
+ this.world.setSendViewDistance(distance);
+ }
+
+ public void setLoadDistance(final int distance) {
+ this.world.setLoadViewDistance(distance);
+ }
+
+ public void setTickDistance(final int distance) {
+ this.world.setTickViewDistance(distance);
+ }
+
+ // Note: follow the player chunk loader so everything stays consistent...
+ public int getAPITickDistance() {
+ final ViewDistances distances = this.world.getViewDistances();
+ final int tickViewDistance = PlayerChunkLoaderData.getTickDistance(-1, distances.tickViewDistance);
+ return tickViewDistance;
+ }
+
+ public int getAPIViewDistance() {
+ final ViewDistances distances = this.world.getViewDistances();
+ final int tickViewDistance = PlayerChunkLoaderData.getTickDistance(-1, distances.tickViewDistance);
+ final int loadDistance = PlayerChunkLoaderData.getLoadViewDistance(tickViewDistance, -1, distances.loadViewDistance);
+
+ // loadDistance = api view distance + 1
+ return loadDistance - 1;
+ }
+
+ public int getAPISendViewDistance() {
+ final ViewDistances distances = this.world.getViewDistances();
+ final int tickViewDistance = PlayerChunkLoaderData.getTickDistance(-1, distances.tickViewDistance);
+ final int loadDistance = PlayerChunkLoaderData.getLoadViewDistance(tickViewDistance, -1, distances.loadViewDistance);
+ final int sendViewDistance = PlayerChunkLoaderData.getSendViewDistance(
+ loadDistance, -1, -1, distances.sendViewDistance
+ );
+
+ return sendViewDistance;
+ }
+
+ public boolean isChunkSent(final ServerPlayer player, final int chunkX, final int chunkZ, final boolean borderOnly) {
+ return borderOnly ? this.isChunkSentBorderOnly(player, chunkX, chunkZ) : this.isChunkSent(player, chunkX, chunkZ);
+ }
+
+ public boolean isChunkSent(final ServerPlayer player, final int chunkX, final int chunkZ) {
+ final PlayerChunkLoaderData loader = player.chunkLoader;
+ if (loader == null) {
+ return false;
+ }
+
+ return loader.sentChunks.contains(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+ }
+
+ public boolean isChunkSentBorderOnly(final ServerPlayer player, final int chunkX, final int chunkZ) {
+ final PlayerChunkLoaderData loader = player.chunkLoader;
+ if (loader == null) {
+ return false;
+ }
+
+ for (int dz = -1; dz <= 1; ++dz) {
+ for (int dx = -1; dx <= 1; ++dx) {
+ if (!loader.sentChunks.contains(CoordinateUtils.getChunkKey(dx + chunkX, dz + chunkZ))) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ public void tick() {
+ TickThread.ensureTickThread("Cannot tick player chunk loader async");
Fix several issues, mostly saving pending teleporting entities
The place/portal async function now track entities that have been
removed from the world but have not teleported. When the server
shuts down, these entities will have their passenger tree restored
and re-added to the entity slices at the location they were teleporting to,
or in the case of portals that did not run placeAsync yet,
the location they entered the portal on. This should ensure that
for regular teleports that the entity is placed at its correct
target location, and for portalling to ensure that either
the entity is placed at the portal entrace location (where
they entered) or the portal destination. In any case,
the entity is preserved in a location and will survive
the shutdown.
Additionally, move player saving until after the worlds save. This
is to ensure that the save logic is performed only after
all teleportations have completed.
Fix some other misc issues as well:
- Fix double nether portal creation by checking if a portal exists again
before creating it, fixing a race condition where two entites would portal
and neither would see that the other created a portal.
- Make all remove ticket add an unknown ticket.
In general this behavior is better since it means that unloads will only
ever occur at the next tick, rather than during the tick logic. Thus,
there will be no cases where a chunk is unloaded unexpectedly.
- Do not use fastFloor for calculating chunk position from block position
It is not going to return a good value outside of [-1024, 1024]
- Always perform mid tick update for ticking regionised player chunk loader
If no entities were loaded, no chunks were loaded, and nothing else -
the logic would not have otherwise ran. This fixed some rare cases of
chunks never loading for players after logging in.
2023-03-02 04:12:31 +01:00
+ long currTime = System.nanoTime();
2023-02-23 17:13:45 +01:00
+ for (final ServerPlayer player : this.world.players()) {
+ player.chunkLoader.update();
Fix several issues, mostly saving pending teleporting entities
The place/portal async function now track entities that have been
removed from the world but have not teleported. When the server
shuts down, these entities will have their passenger tree restored
and re-added to the entity slices at the location they were teleporting to,
or in the case of portals that did not run placeAsync yet,
the location they entered the portal on. This should ensure that
for regular teleports that the entity is placed at its correct
target location, and for portalling to ensure that either
the entity is placed at the portal entrace location (where
they entered) or the portal destination. In any case,
the entity is preserved in a location and will survive
the shutdown.
Additionally, move player saving until after the worlds save. This
is to ensure that the save logic is performed only after
all teleportations have completed.
Fix some other misc issues as well:
- Fix double nether portal creation by checking if a portal exists again
before creating it, fixing a race condition where two entites would portal
and neither would see that the other created a portal.
- Make all remove ticket add an unknown ticket.
In general this behavior is better since it means that unloads will only
ever occur at the next tick, rather than during the tick logic. Thus,
there will be no cases where a chunk is unloaded unexpectedly.
- Do not use fastFloor for calculating chunk position from block position
It is not going to return a good value outside of [-1024, 1024]
- Always perform mid tick update for ticking regionised player chunk loader
If no entities were loaded, no chunks were loaded, and nothing else -
the logic would not have otherwise ran. This fixed some rare cases of
chunks never loading for players after logging in.
2023-03-02 04:12:31 +01:00
+ player.chunkLoader.midTickUpdate(currTime);
2023-02-23 17:13:45 +01:00
+ }
+ }
+
+ public void tickMidTick() {
+ final long time = System.nanoTime();
+ for (final ServerPlayer player : this.world.players()) {
+ player.chunkLoader.midTickUpdate(time);
+ }
+ }
+
+ private static long[] generateBFSOrder(final int radius) {
+ final LongArrayList chunks = new LongArrayList();
+ final LongArrayFIFOQueue queue = new LongArrayFIFOQueue();
+ final LongOpenHashSet seen = new LongOpenHashSet();
+
+ seen.add(CoordinateUtils.getChunkKey(0, 0));
+ queue.enqueue(CoordinateUtils.getChunkKey(0, 0));
+ while (!queue.isEmpty()) {
+ final long chunk = queue.dequeueLong();
+ final int chunkX = CoordinateUtils.getChunkX(chunk);
+ final int chunkZ = CoordinateUtils.getChunkZ(chunk);
+
+ // important that the addition to the list is here, rather than when enqueueing neighbours
+ // ensures the order is actually kept
+ chunks.add(chunk);
+
+ // -x
+ final long n1 = CoordinateUtils.getChunkKey(chunkX - 1, chunkZ);
+ // -z
+ final long n2 = CoordinateUtils.getChunkKey(chunkX, chunkZ - 1);
+ // +x
+ final long n3 = CoordinateUtils.getChunkKey(chunkX + 1, chunkZ);
+ // +z
+ final long n4 = CoordinateUtils.getChunkKey(chunkX, chunkZ + 1);
+
+ final long[] list = new long[] {n1, n2, n3, n4};
+
+ for (final long neighbour : list) {
+ final int neighbourX = CoordinateUtils.getChunkX(neighbour);
+ final int neighbourZ = CoordinateUtils.getChunkZ(neighbour);
+ if (Math.max(Math.abs(neighbourX), Math.abs(neighbourZ)) > radius) {
+ // don't enqueue out of range
+ continue;
+ }
+ if (!seen.add(neighbour)) {
+ continue;
+ }
+ queue.enqueue(neighbour);
+ }
+ }
+
+ return chunks.toLongArray();
+ }
+
+ public static final class PlayerChunkLoaderData {
+
+ private static final AtomicLong ID_GENERATOR = new AtomicLong();
+ private final long id = ID_GENERATOR.incrementAndGet();
+ private final Long idBoxed = Long.valueOf(this.id);
+
+ // expected that this list returns for a given radius, the set of chunks ordered
+ // by manhattan distance
+ private static final long[][] SEARCH_RADIUS_ITERATION_LIST = new long[65][];
+ static {
+ for (int i = 0; i < SEARCH_RADIUS_ITERATION_LIST.length; ++i) {
+ // a BFS around -x, -z, +x, +z will give increasing manhatten distance
+ SEARCH_RADIUS_ITERATION_LIST[i] = generateBFSOrder(i);
+ }
+ }
+
+ private final ServerPlayer player;
+ private final ServerLevel world;
+
+ private int lastChunkX = Integer.MIN_VALUE;
+ private int lastChunkZ = Integer.MIN_VALUE;
+
+ private int lastSendDistance = Integer.MIN_VALUE;
+ private int lastLoadDistance = Integer.MIN_VALUE;
+ private int lastTickDistance = Integer.MIN_VALUE;
+
+ private int lastSentChunkCenterX = Integer.MIN_VALUE;
+ private int lastSentChunkCenterZ = Integer.MIN_VALUE;
+
+ private int lastSentChunkRadius = Integer.MIN_VALUE;
+ private int lastSentSimulationDistance = Integer.MIN_VALUE;
+
+ private boolean canGenerateChunks = true;
+
+ private final ArrayDeque<ChunkHolderManager.TicketOperation<?, ?>> delayedTicketOps = new ArrayDeque<>();
+ private final LongOpenHashSet sentChunks = new LongOpenHashSet();
+
+ private static final byte CHUNK_TICKET_STAGE_NONE = 0;
+ private static final byte CHUNK_TICKET_STAGE_LOADING = 1;
+ private static final byte CHUNK_TICKET_STAGE_LOADED = 2;
+ private static final byte CHUNK_TICKET_STAGE_GENERATING = 3;
+ private static final byte CHUNK_TICKET_STAGE_GENERATED = 4;
+ private static final byte CHUNK_TICKET_STAGE_TICK = 5;
+ private static final int[] TICKET_STAGE_TO_LEVEL = new int[] {
+ ChunkHolderManager.MAX_TICKET_LEVEL + 1,
+ LOADED_TICKET_LEVEL,
+ LOADED_TICKET_LEVEL,
+ GENERATED_TICKET_LEVEL,
+ GENERATED_TICKET_LEVEL,
+ TICK_TICKET_LEVEL
+ };
+ private final Long2ByteOpenHashMap chunkTicketStage = new Long2ByteOpenHashMap();
+ {
+ this.chunkTicketStage.defaultReturnValue(CHUNK_TICKET_STAGE_NONE);
+ }
+
+ // rate limiting
+ private final MultiIntervalledCounter chunkSendCounter = new MultiIntervalledCounter(
+ TimeUnit.MILLISECONDS.toNanos(50L), TimeUnit.MILLISECONDS.toNanos(250L), TimeUnit.SECONDS.toNanos(1L)
+ );
+ private final MultiIntervalledCounter chunkLoadTicketCounter = new MultiIntervalledCounter(
+ TimeUnit.MILLISECONDS.toNanos(50L), TimeUnit.MILLISECONDS.toNanos(250L), TimeUnit.SECONDS.toNanos(1L)
+ );
+ private final MultiIntervalledCounter chunkGenerateTicketCounter = new MultiIntervalledCounter(
+ TimeUnit.MILLISECONDS.toNanos(50L), TimeUnit.MILLISECONDS.toNanos(250L), TimeUnit.SECONDS.toNanos(1L)
+ );
+
+ // queues
+ private final LongComparator CLOSEST_MANHATTAN_DIST = (final long c1, final long c2) -> {
+ final int c1x = CoordinateUtils.getChunkX(c1);
+ final int c1z = CoordinateUtils.getChunkZ(c1);
+
+ final int c2x = CoordinateUtils.getChunkX(c2);
+ final int c2z = CoordinateUtils.getChunkZ(c2);
+
+ final int centerX = PlayerChunkLoaderData.this.lastChunkX;
+ final int centerZ = PlayerChunkLoaderData.this.lastChunkZ;
+
+ return Integer.compare(
+ Math.abs(c1x - centerX) + Math.abs(c1z - centerZ),
+ Math.abs(c2x - centerX) + Math.abs(c2z - centerZ)
+ );
+ };
+ private final LongHeapPriorityQueue sendQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST);
+ private final LongHeapPriorityQueue tickingQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST);
+ private final LongHeapPriorityQueue generatingQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST);
+ private final LongHeapPriorityQueue genQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST);
+ private final LongHeapPriorityQueue loadingQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST);
+ private final LongHeapPriorityQueue loadQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST);
+
+ public PlayerChunkLoaderData(final ServerLevel world, final ServerPlayer player) {
+ this.world = world;
+ this.player = player;
+ }
+
+ private void flushDelayedTicketOps() {
+ if (this.delayedTicketOps.isEmpty()) {
+ return;
+ }
+ this.world.chunkTaskScheduler.chunkHolderManager.pushDelayedTicketUpdates(this.delayedTicketOps);
+ this.delayedTicketOps.clear();
+ this.world.chunkTaskScheduler.chunkHolderManager.tryDrainTicketUpdates();
+ }
+
+ private void pushDelayedTicketOp(final ChunkHolderManager.TicketOperation<?, ?> op) {
+ this.delayedTicketOps.addLast(op);
+ }
+
+ private void sendChunk(final int chunkX, final int chunkZ) {
+ if (this.sentChunks.add(CoordinateUtils.getChunkKey(chunkX, chunkZ))) {
+ this.world.getChunkSource().chunkMap.updateChunkTracking(this.player,
+ new ChunkPos(chunkX, chunkZ), new MutableObject<>(), false, true); // unloaded, loaded
+ return;
+ }
+ throw new IllegalStateException();
+ }
+
+ private void sendUnloadChunk(final int chunkX, final int chunkZ) {
+ if (!this.sentChunks.remove(CoordinateUtils.getChunkKey(chunkX, chunkZ))) {
+ return;
+ }
+ this.sendUnloadChunkRaw(chunkX, chunkZ);
+ }
+
+ private void sendUnloadChunkRaw(final int chunkX, final int chunkZ) {
+ this.player.getLevel().getChunkSource().chunkMap.updateChunkTracking(this.player,
+ new ChunkPos(chunkX, chunkZ), null, true, false); // unloaded, loaded
+ }
+
+ private final SingleUserAreaMap<PlayerChunkLoaderData> broadcastMap = new SingleUserAreaMap<>(this) {
+ @Override
+ protected void addCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) {
+ // do nothing, we only care about remove
+ }
+
+ @Override
+ protected void removeCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) {
+ parameter.sendUnloadChunk(chunkX, chunkZ);
+ }
+ };
+ private final SingleUserAreaMap<PlayerChunkLoaderData> loadTicketCleanup = new SingleUserAreaMap<>(this) {
+ @Override
+ protected void addCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) {
+ // do nothing, we only care about remove
+ }
+
+ @Override
+ protected void removeCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) {
+ final long chunk = CoordinateUtils.getChunkKey(chunkX, chunkZ);
+ final byte ticketStage = parameter.chunkTicketStage.remove(chunk);
+ final int level = TICKET_STAGE_TO_LEVEL[ticketStage];
+ if (level > ChunkHolderManager.MAX_TICKET_LEVEL) {
+ return;
+ }
+
+ parameter.pushDelayedTicketOp(ChunkHolderManager.TicketOperation.addAndRemove(
+ chunk,
+ TicketType.UNKNOWN, level, new ChunkPos(chunkX, chunkZ),
+ REGION_PLAYER_TICKET, level, parameter.idBoxed
+ ));
+ }
+ };
+ private final SingleUserAreaMap<PlayerChunkLoaderData> tickMap = new SingleUserAreaMap<>(this) {
+ @Override
+ protected void addCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) {
+ // do nothing, we will detect ticking chunks when we try to load them
+ }
+
+ @Override
+ protected void removeCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) {
+ final long chunk = CoordinateUtils.getChunkKey(chunkX, chunkZ);
+ // note: by the time this is called, the tick cleanup should have ran - so, if the chunk is at
+ // the tick stage it was deemed in range for loading. Thus, we need to move it to generated
+ if (!parameter.chunkTicketStage.replace(chunk, CHUNK_TICKET_STAGE_TICK, CHUNK_TICKET_STAGE_GENERATED)) {
+ return;
+ }
+
+ // Since we are possibly downgrading the ticket level, we add an unknown ticket so that
+ // the level is kept until tick().
+ parameter.pushDelayedTicketOp(ChunkHolderManager.TicketOperation.addAndRemove(
+ chunk,
+ TicketType.UNKNOWN, TICK_TICKET_LEVEL, new ChunkPos(chunkX, chunkZ),
+ REGION_PLAYER_TICKET, TICK_TICKET_LEVEL, parameter.idBoxed
+ ));
+ // keep chunk at new generated level
+ parameter.pushDelayedTicketOp(ChunkHolderManager.TicketOperation.addOp(
+ chunk,
+ REGION_PLAYER_TICKET, GENERATED_TICKET_LEVEL, parameter.idBoxed
+ ));
+ }
+ };
+
+ private static boolean wantChunkLoaded(final int centerX, final int centerZ, final int chunkX, final int chunkZ,
+ final int sendRadius) {
+ // expect sendRadius to be = 1 + target viewable radius
+ return ChunkMap.isChunkInRange(chunkX, chunkZ, centerX, centerZ, sendRadius);
+ }
+
+ private static int getClientViewDistance(final ServerPlayer player) {
+ final Integer vd = player.clientViewDistance;
+ return vd == null ? -1 : Math.max(0, vd.intValue());
+ }
+
+ private static int getTickDistance(final int playerTickViewDistance, final int worldTickViewDistance) {
+ return playerTickViewDistance < 0 ? worldTickViewDistance : playerTickViewDistance;
+ }
+
+ private static int getLoadViewDistance(final int tickViewDistance, final int playerLoadViewDistance,
+ final int worldLoadViewDistance) {
+ return Math.max(tickViewDistance + 1, playerLoadViewDistance < 0 ? worldLoadViewDistance : playerLoadViewDistance);
+ }
+
+ private static int getSendViewDistance(final int loadViewDistance, final int clientViewDistance,
+ final int playerSendViewDistance, final int worldSendViewDistance) {
+ return Math.min(
+ loadViewDistance,
+ playerSendViewDistance < 0 ? (!GlobalConfiguration.get().chunkLoadingAdvanced.autoConfigSendDistance || clientViewDistance < 0 ? (worldSendViewDistance < 0 ? loadViewDistance : worldSendViewDistance) : clientViewDistance + 1) : playerSendViewDistance
+ );
+ }
+
+ private Packet<?> updateClientChunkRadius(final int radius) {
+ this.lastSentChunkRadius = radius;
+ return new ClientboundSetChunkCacheRadiusPacket(radius);
+ }
+
+ private Packet<?> updateClientSimulationDistance(final int distance) {
+ this.lastSentSimulationDistance = distance;
+ return new ClientboundSetSimulationDistancePacket(distance);
+ }
+
+ private Packet<?> updateClientChunkCenter(final int chunkX, final int chunkZ) {
+ this.lastSentChunkCenterX = chunkX;
+ this.lastSentChunkCenterZ = chunkZ;
+ return new ClientboundSetChunkCacheCenterPacket(chunkX, chunkZ);
+ }
+
+ private boolean canPlayerGenerateChunks() {
+ return !this.player.isSpectator() || this.world.getGameRules().getBoolean(GameRules.RULE_SPECTATORSGENERATECHUNKS);
+ }
+
+ private int getMaxChunkLoads() {
+ final int radiusChunks = (2 * this.lastLoadDistance + 1) * (2 * this.lastLoadDistance + 1);
+ int configLimit = GlobalConfiguration.get().chunkLoadingAdvanced.playerMaxConcurrentChunkLoads;
+ if (configLimit == 0) {
+ // by default, only allow 1/10th of the chunks in the view distance to be concurrently active
+ configLimit = Math.max(5, radiusChunks / 10);
+ } else if (configLimit < 0) {
+ configLimit = Integer.MAX_VALUE;
+ } // else: use the value configured
+ configLimit = configLimit - this.loadingQueue.size();
+
+ int rateLimit;
+ double configRate = GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkLoadRate;
+ if (configRate < 0.0 || configRate > (1000.0 * (double)radiusChunks)) {
+ // getMaxCountBeforeViolation may not work with large config rates, so by checking against the load count we ensure
+ // there are no issues with the cast to integer
+ rateLimit = Integer.MAX_VALUE;
+ } else {
+ rateLimit = (int)this.chunkLoadTicketCounter.getMaxCountBeforeViolation(configRate);
+ }
+
+ return Math.min(configLimit, rateLimit);
+ }
+
+ private int getMaxChunkGenerates() {
+ final int radiusChunks = (2 * this.lastLoadDistance + 1) * (2 * this.lastLoadDistance + 1);
+ int configLimit = GlobalConfiguration.get().chunkLoadingAdvanced.playerMaxConcurrentChunkGenerates;
+ if (configLimit == 0) {
+ // by default, only allow 1/10th of the chunks in the view distance to be concurrently active
+ configLimit = Math.max(5, radiusChunks / 10);
+ } else if (configLimit < 0) {
+ configLimit = Integer.MAX_VALUE;
+ } // else: use the value configured
+ configLimit = configLimit - this.generatingQueue.size();
+
+ int rateLimit;
+ double configRate = GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkGenerateRate;
+ if (configRate < 0.0 || configRate > (1000.0 * (double)radiusChunks)) {
+ // getMaxCountBeforeViolation may not work with large config rates, so by checking against the load count we ensure
+ // there are no issues with the cast to integer
+ rateLimit = Integer.MAX_VALUE;
+ } else {
+ rateLimit = (int)this.chunkGenerateTicketCounter.getMaxCountBeforeViolation(configRate);
+ }
+
+ return Math.min(configLimit, rateLimit);
+ }
+
+ private int getMaxChunkSends() {
+ final int radiusChunks = (2 * this.lastSendDistance + 1) * (2 * this.lastSendDistance + 1);
+
+ int rateLimit;
+ double configRate = GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkSendRate;
+ if (configRate < 0.0 || configRate > (1000.0 * (double)radiusChunks)) {
+ // getMaxCountBeforeViolation may not work with large config rates, so by checking against the load count we ensure
+ // there are no issues with the cast to integer
+ rateLimit = Integer.MAX_VALUE;
+ } else {
+ rateLimit = (int)this.chunkSendCounter.getMaxCountBeforeViolation(configRate);
+ }
+
+ return rateLimit;
+ }
+
+ private boolean wantChunkSent(final int chunkX, final int chunkZ) {
+ final int dx = this.lastChunkX - chunkX;
+ final int dz = this.lastChunkZ - chunkZ;
+ return Math.max(Math.abs(dx), Math.abs(dz)) <= this.lastSendDistance && wantChunkLoaded(
+ this.lastChunkX, this.lastChunkZ, chunkX, chunkZ, this.lastSendDistance
+ );
+ }
+
+ private boolean wantChunkTicked(final int chunkX, final int chunkZ) {
+ final int dx = this.lastChunkX - chunkX;
+ final int dz = this.lastChunkZ - chunkZ;
+ return Math.max(Math.abs(dx), Math.abs(dz)) <= this.lastTickDistance;
+ }
+
+ void midTickUpdate(final long time) {
+ TickThread.ensureTickThread(this.player, "Cannot tick player chunk loader async");
+ // update rate limits
+ this.chunkSendCounter.update(time);
+ this.chunkGenerateTicketCounter.update(time);
+ this.chunkLoadTicketCounter.update(time);
+
+ // try to progress chunk loads
+ while (!this.loadingQueue.isEmpty()) {
+ final long pendingLoadChunk = this.loadingQueue.firstLong();
+ final int pendingChunkX = CoordinateUtils.getChunkX(pendingLoadChunk);
+ final int pendingChunkZ = CoordinateUtils.getChunkZ(pendingLoadChunk);
+ final ChunkAccess pending = this.world.chunkSource.getChunkAtImmediately(pendingChunkX, pendingChunkZ);
+ if (pending == null) {
+ // nothing to do here
+ break;
+ }
+ // chunk has loaded, so we can take it out of the queue
+ this.loadingQueue.dequeueLong();
+
+ // try to move to generate queue
+ final byte prev = this.chunkTicketStage.put(pendingLoadChunk, CHUNK_TICKET_STAGE_LOADED);
+ if (prev != CHUNK_TICKET_STAGE_LOADING) {
+ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_LOADING + ", not " + prev);
+ }
+
+ if (this.canGenerateChunks || this.isLoadedChunkGeneratable(pending)) {
+ this.genQueue.enqueue(pendingLoadChunk);
+ } // else: don't want to generate, so just leave it loaded
+ }
+
+ // try to push more chunk loads
+ int loadSlots;
+ while ((loadSlots = Math.min(this.getMaxChunkLoads(), this.loadQueue.size())) > 0) {
+ final LongArrayList chunks = new LongArrayList(loadSlots);
+ int actualLoadsQueued = 0;
+ for (int i = 0; i < loadSlots; ++i) {
+ final long chunk = this.loadQueue.dequeueLong();
+ final byte prev = this.chunkTicketStage.put(chunk, CHUNK_TICKET_STAGE_LOADING);
+ if (prev != CHUNK_TICKET_STAGE_NONE) {
+ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_NONE + ", not " + prev);
+ }
+ this.pushDelayedTicketOp(
+ ChunkHolderManager.TicketOperation.addOp(
+ chunk,
+ REGION_PLAYER_TICKET, LOADED_TICKET_LEVEL, this.idBoxed
+ )
+ );
+ chunks.add(chunk);
+ this.loadingQueue.enqueue(chunk);
+
+ if (this.world.chunkSource.getChunkAtImmediately(CoordinateUtils.getChunkX(chunk), CoordinateUtils.getChunkZ(chunk)) == null) {
+ // this is a good enough approximation for counting, but NOT for actual state management
+ ++actualLoadsQueued;
+ }
+ }
+ if (actualLoadsQueued > 0) {
+ this.chunkLoadTicketCounter.addTime(time, (long)actualLoadsQueued);
+ }
+
+ // here we need to flush tickets, as scheduleChunkLoad requires tickets to be propagated with addTicket = false
+ this.flushDelayedTicketOps();
+ // we only need to call scheduleChunkLoad because the loaded ticket level is not enough to start the chunk
+ // load - only generate ticket levels start anything, but they start generation...
+ // propagate levels
+ // Note: this CAN call plugin logic, so it is VITAL that our bookkeeping logic is completely done by the time this is invoked
+ this.world.chunkTaskScheduler.chunkHolderManager.processTicketUpdates();
+
+ for (int i = 0; i < loadSlots; ++i) {
+ final long queuedLoadChunk = chunks.getLong(i);
+ final int queuedChunkX = CoordinateUtils.getChunkX(queuedLoadChunk);
+ final int queuedChunkZ = CoordinateUtils.getChunkZ(queuedLoadChunk);
+ this.world.chunkTaskScheduler.scheduleChunkLoad(
+ queuedChunkX, queuedChunkZ, ChunkStatus.EMPTY, false, PrioritisedExecutor.Priority.NORMAL, null
+ );
+ }
+ }
+
+ // try to progress chunk generations
+ while (!this.generatingQueue.isEmpty()) {
+ final long pendingGenChunk = this.generatingQueue.firstLong();
+ final int pendingChunkX = CoordinateUtils.getChunkX(pendingGenChunk);
+ final int pendingChunkZ = CoordinateUtils.getChunkZ(pendingGenChunk);
+ final LevelChunk pending = this.world.chunkSource.getChunkAtIfLoadedMainThreadNoCache(pendingChunkX, pendingChunkZ);
+ if (pending == null) {
+ // nothing to do here
+ break;
+ }
+
+ // chunk has generated, so we can take it out of queue
+ this.generatingQueue.dequeueLong();
+
+ final byte prev = this.chunkTicketStage.put(pendingGenChunk, CHUNK_TICKET_STAGE_GENERATED);
+ if (prev != CHUNK_TICKET_STAGE_GENERATING) {
+ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_GENERATING + ", not " + prev);
+ }
+
+ // try to move to send queue
+ if (this.wantChunkSent(pendingChunkX, pendingChunkZ)) {
+ this.sendQueue.enqueue(pendingGenChunk);
+ }
+ // try to move to tick queue
+ if (this.wantChunkTicked(pendingChunkX, pendingChunkZ)) {
+ this.tickingQueue.enqueue(pendingGenChunk);
+ }
+ }
+
+ // try to push more chunk generations
+ int genSlots;
+ while ((genSlots = Math.min(this.getMaxChunkGenerates(), this.genQueue.size())) > 0) {
+ int actualGenerationsQueued = 0;
+ for (int i = 0; i < genSlots; ++i) {
+ final long chunk = this.genQueue.dequeueLong();
+ final byte prev = this.chunkTicketStage.put(chunk, CHUNK_TICKET_STAGE_GENERATING);
+ if (prev != CHUNK_TICKET_STAGE_LOADED) {
+ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_LOADED + ", not " + prev);
+ }
+ this.pushDelayedTicketOp(
+ ChunkHolderManager.TicketOperation.addAndRemove(
+ chunk,
+ REGION_PLAYER_TICKET, GENERATED_TICKET_LEVEL, this.idBoxed,
+ REGION_PLAYER_TICKET, LOADED_TICKET_LEVEL, this.idBoxed
+ )
+ );
+ this.generatingQueue.enqueue(chunk);
+ final ChunkAccess existingChunk = this.world.chunkSource.getChunkAtImmediately(CoordinateUtils.getChunkX(chunk), CoordinateUtils.getChunkZ(chunk));
+ if (existingChunk == null || !existingChunk.getStatus().isOrAfter(ChunkStatus.FULL)) {
+ // this is a good enough approximation for counting, but NOT for actual state management
+ ++actualGenerationsQueued;
+ }
+ }
+ if (actualGenerationsQueued > 0) {
+ this.chunkGenerateTicketCounter.addTime(time, (long)actualGenerationsQueued);
+ }
+ }
+
+ // try to pull ticking chunks
+ tick_check_outer:
+ while (!this.tickingQueue.isEmpty()) {
+ final long pendingTicking = this.tickingQueue.firstLong();
+ final int pendingChunkX = CoordinateUtils.getChunkX(pendingTicking);
+ final int pendingChunkZ = CoordinateUtils.getChunkZ(pendingTicking);
+
+ final int tickingReq = 2;
+ for (int dz = -tickingReq; dz <= tickingReq; ++dz) {
+ for (int dx = -tickingReq; dx <= tickingReq; ++dx) {
+ if ((dx | dz) == 0) {
+ continue;
+ }
+ final long neighbour = CoordinateUtils.getChunkKey(dx + pendingChunkX, dz + pendingChunkZ);
+ final byte stage = this.chunkTicketStage.get(neighbour);
+ if (stage != CHUNK_TICKET_STAGE_GENERATED && stage != CHUNK_TICKET_STAGE_TICK) {
+ break tick_check_outer;
+ }
+ }
+ }
+ // only gets here if all neighbours were marked as generated or ticking themselves
+ this.tickingQueue.dequeueLong();
+ this.pushDelayedTicketOp(
+ ChunkHolderManager.TicketOperation.addAndRemove(
+ pendingTicking,
+ REGION_PLAYER_TICKET, TICK_TICKET_LEVEL, this.idBoxed,
+ REGION_PLAYER_TICKET, GENERATED_TICKET_LEVEL, this.idBoxed
+ )
+ );
+ // there is no queue to add after ticking
+ final byte prev = this.chunkTicketStage.put(pendingTicking, CHUNK_TICKET_STAGE_TICK);
+ if (prev != CHUNK_TICKET_STAGE_GENERATED) {
+ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_GENERATED + ", not " + prev);
+ }
+ }
+
+ // try to pull sending chunks
+ final int maxSends = this.getMaxChunkSends();
+ final int sendSlots = Math.min(maxSends, this.sendQueue.size());
+ for (int i = 0; i < sendSlots; ++i) {
+ final long pendingSend = this.sendQueue.firstLong();
+ final int pendingSendX = CoordinateUtils.getChunkX(pendingSend);
+ final int pendingSendZ = CoordinateUtils.getChunkZ(pendingSend);
+ final LevelChunk chunk = this.world.chunkSource.getChunkAtIfLoadedMainThreadNoCache(pendingSendX, pendingSendZ);
+ if (!chunk.areNeighboursLoaded(1)) {
+ // nothing to do
+ break;
+ }
+ this.sendQueue.dequeueLong();
+
+ this.sendChunk(pendingSendX, pendingSendZ);
+ }
+ if (sendSlots > 0) {
+ this.chunkSendCounter.addTime(time, sendSlots);
+ }
+
+ this.flushDelayedTicketOps();
+ // we assume propagate ticket levels happens after this call
+ }
+
+ void add() {
+ final ViewDistances playerDistances = this.player.getViewDistances();
+ final ViewDistances worldDistances = this.world.getViewDistances();
+ final int chunkX = this.player.chunkPosition().x;
+ final int chunkZ = this.player.chunkPosition().z;
+
+ final int tickViewDistance = getTickDistance(playerDistances.tickViewDistance, worldDistances.tickViewDistance);
+ // load view cannot be less-than tick view + 1
+ final int loadViewDistance = getLoadViewDistance(tickViewDistance, playerDistances.loadViewDistance, worldDistances.loadViewDistance);
+ // send view cannot be greater-than load view
+ final int clientViewDistance = getClientViewDistance(this.player);
+ final int sendViewDistance = getSendViewDistance(loadViewDistance, clientViewDistance, playerDistances.sendViewDistance, worldDistances.sendViewDistance);
+
+ // send view distances
+ this.player.connection.send(this.updateClientChunkRadius(sendViewDistance));
+ this.player.connection.send(this.updateClientSimulationDistance(tickViewDistance));
+
+ // add to distance maps
+ this.broadcastMap.add(chunkX, chunkZ, sendViewDistance);
+ this.loadTicketCleanup.add(chunkX, chunkZ, loadViewDistance + 1);
+ this.tickMap.add(chunkX, chunkZ, tickViewDistance);
+
+ // update chunk center
+ this.player.connection.send(this.updateClientChunkCenter(chunkX, chunkZ));
+
+ // now we can update
+ this.update();
+ }
+
+ private boolean isLoadedChunkGeneratable(final int chunkX, final int chunkZ) {
+ return this.isLoadedChunkGeneratable(this.world.chunkSource.getChunkAtImmediately(chunkX, chunkZ));
+ }
+
+ private boolean isLoadedChunkGeneratable(final ChunkAccess chunkAccess) {
+ final BelowZeroRetrogen belowZeroRetrogen;
+ return chunkAccess != null && (
+ chunkAccess.getStatus() == ChunkStatus.FULL ||
+ ((belowZeroRetrogen = chunkAccess.getBelowZeroRetrogen()) != null && belowZeroRetrogen.targetStatus().isOrAfter(ChunkStatus.FULL))
+ );
+ }
+
+ void update() {
+ final ViewDistances playerDistances = this.player.getViewDistances();
+ final ViewDistances worldDistances = this.world.getViewDistances();
+
+ final int tickViewDistance = getTickDistance(playerDistances.tickViewDistance, worldDistances.tickViewDistance);
+ // load view cannot be less-than tick view + 1
+ final int loadViewDistance = getLoadViewDistance(tickViewDistance, playerDistances.loadViewDistance, worldDistances.loadViewDistance);
+ // send view cannot be greater-than load view
+ final int clientViewDistance = getClientViewDistance(this.player);
+ final int sendViewDistance = getSendViewDistance(loadViewDistance, clientViewDistance, playerDistances.sendViewDistance, worldDistances.sendViewDistance);
+
+ final ChunkPos playerPos = this.player.chunkPosition();
+ final boolean canGenerateChunks = this.canPlayerGenerateChunks();
+ final int currentChunkX = playerPos.x;
+ final int currentChunkZ = playerPos.z;
+
+ final int prevChunkX = this.lastChunkX;
+ final int prevChunkZ = this.lastChunkZ;
+
+ if (
+ // has view distance stayed the same?
+ sendViewDistance == this.lastSendDistance
+ && loadViewDistance == this.lastLoadDistance
+ && tickViewDistance == this.lastTickDistance
+
+ // has our chunk stayed the same?
+ && prevChunkX == currentChunkX
+ && prevChunkZ == currentChunkZ
+
+ // can we still generate chunks?
+ && this.canGenerateChunks == canGenerateChunks
+ ) {
+ // nothing we care about changed, so we're not re-calculating
+ return;
+ }
+
+ // update distance maps
+ this.broadcastMap.update(currentChunkX, currentChunkZ, sendViewDistance);
+ this.loadTicketCleanup.update(currentChunkX, currentChunkZ, loadViewDistance + 1);
+ this.tickMap.update(currentChunkX, currentChunkZ, tickViewDistance);
+ if (sendViewDistance > loadViewDistance || tickViewDistance > (loadViewDistance - 1)) {
+ throw new IllegalStateException();
+ }
+
+ // update VDs for client
+ // this should be after the distance map updates, as they will send unload packets
+ if (this.lastSentChunkRadius != sendViewDistance) {
+ this.player.connection.send(this.updateClientChunkRadius(sendViewDistance));
+ }
+ if (this.lastSentSimulationDistance != tickViewDistance) {
+ this.player.connection.send(this.updateClientSimulationDistance(tickViewDistance));
+ }
+
+ this.sendQueue.clear();
+ this.tickingQueue.clear();
+ this.generatingQueue.clear();
+ this.genQueue.clear();
+ this.loadingQueue.clear();
+ this.loadQueue.clear();
+
+ this.lastChunkX = currentChunkX;
+ this.lastChunkZ = currentChunkZ;
+ this.lastSendDistance = sendViewDistance;
+ this.lastLoadDistance = loadViewDistance;
+ this.lastTickDistance = tickViewDistance;
+ this.canGenerateChunks = canGenerateChunks;
+
+ // +1 since we need to load chunks +1 around the load view distance...
+ final long[] toIterate = SEARCH_RADIUS_ITERATION_LIST[loadViewDistance + 1];
+ // the iteration order is by increasing manhattan distance - so, we do NOT need to
+ // sort anything in the queue!
+ for (final long deltaChunk : toIterate) {
+ final int dx = CoordinateUtils.getChunkX(deltaChunk);
+ final int dz = CoordinateUtils.getChunkZ(deltaChunk);
+ final int chunkX = dx + currentChunkX;
+ final int chunkZ = dz + currentChunkZ;
+ final long chunk = CoordinateUtils.getChunkKey(chunkX, chunkZ);
+ final int squareDistance = Math.max(Math.abs(dx), Math.abs(dz));
+ final int manhattanDistance = Math.abs(dx) + Math.abs(dz);
+
+ // since chunk sending is not by radius alone, we need an extra check here to account for
+ // everything <= sendDistance
+ // Note: Vanilla may want to send chunks outside the send view distance, so we do need
+ // the dist <= view check
+ final boolean sendChunk = squareDistance <= sendViewDistance
+ && wantChunkLoaded(currentChunkX, currentChunkZ, chunkX, chunkZ, sendViewDistance);
+ final boolean sentChunk = sendChunk ? this.sentChunks.contains(chunk) : this.sentChunks.remove(chunk);
+
+ if (!sendChunk && sentChunk) {
+ // have sent the chunk, but don't want it anymore
+ // unload it now
+ this.sendUnloadChunkRaw(chunkX, chunkZ);
+ }
+
+ final byte stage = this.chunkTicketStage.get(chunk);
+ switch (stage) {
+ case CHUNK_TICKET_STAGE_NONE: {
+ // we want the chunk to be at least loaded
+ this.loadQueue.enqueue(chunk);
+ break;
+ }
+ case CHUNK_TICKET_STAGE_LOADING: {
+ this.loadingQueue.enqueue(chunk);
+ break;
+ }
+ case CHUNK_TICKET_STAGE_LOADED: {
+ if (canGenerateChunks || this.isLoadedChunkGeneratable(chunkX, chunkZ)) {
+ this.genQueue.enqueue(chunk);
+ }
+ break;
+ }
+ case CHUNK_TICKET_STAGE_GENERATING: {
+ this.generatingQueue.enqueue(chunk);
+ break;
+ }
+ case CHUNK_TICKET_STAGE_GENERATED: {
+ if (sendChunk && !sentChunk) {
+ this.sendQueue.enqueue(chunk);
+ }
+ if (squareDistance <= tickViewDistance) {
+ this.tickingQueue.enqueue(chunk);
+ }
+ break;
+ }
+ case CHUNK_TICKET_STAGE_TICK: {
+ if (sendChunk && !sentChunk) {
+ this.sendQueue.enqueue(chunk);
+ }
+ break;
+ }
+ default: {
+ throw new IllegalStateException("Unknown stage: " + stage);
+ }
+ }
+ }
+
+ // update the chunk center
+ // this must be done last so that the client does not ignore any of our unload chunk packets above
+ if (this.lastSentChunkCenterX != currentChunkX || this.lastSentChunkCenterZ != currentChunkZ) {
+ this.player.connection.send(this.updateClientChunkCenter(currentChunkX, currentChunkZ));
+ }
+
+ this.flushDelayedTicketOps();
+ }
+
+ void remove() {
+ // sends the chunk unload packets
+ this.broadcastMap.remove();
+ // cleans up loading/generating tickets
+ this.loadTicketCleanup.remove();
+ // cleans up ticking tickets
+ this.tickMap.remove();
+
+ // purge queues
+ this.sendQueue.clear();
+ this.tickingQueue.clear();
+ this.generatingQueue.clear();
+ this.genQueue.clear();
+ this.loadingQueue.clear();
+ this.loadQueue.clear();
+
+ // flush ticket changes
+ this.flushDelayedTicketOps();
+
+ // now all tickets should be removed, which is all of our external state
+ }
+ }
+
+ private static final class MultiIntervalledCounter {
+
+ private final IntervalledCounter[] counters;
+
+ public MultiIntervalledCounter(final long... intervals) {
+ final IntervalledCounter[] counters = new IntervalledCounter[intervals.length];
+ for (int i = 0; i < intervals.length; ++i) {
+ counters[i] = new IntervalledCounter(intervals[i]);
+ }
+ this.counters = counters;
+ }
+
+ public long getMaxCountBeforeViolation(final double rate) {
+ long count = Long.MAX_VALUE;
+ for (final IntervalledCounter counter : this.counters) {
+ final long sum = counter.getSum();
+ final long interval = counter.getInterval();
+ // rate = sum / interval
+ // so, sum = rate*interval
2023-02-24 09:03:00 +01:00
+ final long maxSum = (long)Math.ceil(rate * (1.0E-9 * (double)interval));
2023-02-23 17:13:45 +01:00
+ final long diff = maxSum - sum;
+ if (diff < count) {
+ count = diff;
+ }
+ }
+
+ return Math.max(0L, count);
+ }
+
+ public void update(final long time) {
+ for (final IntervalledCounter counter : this.counters) {
+ counter.updateCurrentTime(time);
+ }
+ }
+
+ public void updateAndAdd(final long count, final long time) {
+ for (final IntervalledCounter counter : this.counters) {
+ counter.updateAndAdd(count, time);
+ }
+ }
+
+ public void addTime(final long time, final long count) {
+ for (final IntervalledCounter counter : this.counters) {
+ counter.addTime(time, count);
+ }
+ }
+
+ public double getMaxRate() {
+ double ret = 0.0;
+
+ for (final IntervalledCounter counter : this.counters) {
+ final double counterRate = counter.getRate();
+ if (counterRate > ret) {
+ ret = counterRate;
+ }
+ }
+
+ return ret;
+ }
+ }
+
+ // TODO rebase into util patch
+ public static abstract class SingleUserAreaMap<T> {
+
+ private static final int NOT_SET = Integer.MIN_VALUE;
+
+ private final T parameter;
+ private int lastChunkX = NOT_SET;
+ private int lastChunkZ = NOT_SET;
+ private int distance = NOT_SET;
+
+ public SingleUserAreaMap(final T parameter) {
+ this.parameter = parameter;
+ }
+
+ /* math sign function except 0 returns 1 */
+ protected static int sign(int val) {
+ return 1 | (val >> (Integer.SIZE - 1));
+ }
+
+ protected abstract void addCallback(final T parameter, final int chunkX, final int chunkZ);
+
+ protected abstract void removeCallback(final T parameter, final int chunkX, final int chunkZ);
+
+ private void addToNew(final T parameter, final int chunkX, final int chunkZ, final int distance) {
+ final int maxX = chunkX + distance;
+ final int maxZ = chunkZ + distance;
+
+ for (int cx = chunkX - distance; cx <= maxX; ++cx) {
+ for (int cz = chunkZ - distance; cz <= maxZ; ++cz) {
+ this.addCallback(parameter, cx, cz);
+ }
+ }
+ }
+
+ private void removeFromOld(final T parameter, final int chunkX, final int chunkZ, final int distance) {
+ final int maxX = chunkX + distance;
+ final int maxZ = chunkZ + distance;
+
+ for (int cx = chunkX - distance; cx <= maxX; ++cx) {
+ for (int cz = chunkZ - distance; cz <= maxZ; ++cz) {
+ this.removeCallback(parameter, cx, cz);
+ }
+ }
+ }
+
+ public final boolean add(final int chunkX, final int chunkZ, final int distance) {
+ if (distance < 0) {
+ throw new IllegalArgumentException(Integer.toString(distance));
+ }
+ if (this.lastChunkX != NOT_SET) {
+ return false;
+ }
+ this.lastChunkX = chunkX;
+ this.lastChunkZ = chunkZ;
+ this.distance = distance;
+
+ this.addToNew(this.parameter, chunkX, chunkZ, distance);
+
+ return true;
+ }
+
+ public final boolean update(final int toX, final int toZ, final int newViewDistance) {
+ if (newViewDistance < 0) {
+ throw new IllegalArgumentException(Integer.toString(newViewDistance));
+ }
+ final int fromX = this.lastChunkX;
+ final int fromZ = this.lastChunkZ;
+ final int oldViewDistance = this.distance;
+ if (fromX == NOT_SET) {
+ return false;
+ }
+
+ this.lastChunkX = toX;
+ this.lastChunkZ = toZ;
+
+ final T parameter = this.parameter;
+
+
+ final int dx = toX - fromX;
+ final int dz = toZ - fromZ;
+
+ final int totalX = IntegerUtil.branchlessAbs(fromX - toX);
+ final int totalZ = IntegerUtil.branchlessAbs(fromZ - toZ);
+
+ if (Math.max(totalX, totalZ) > (2 * Math.max(newViewDistance, oldViewDistance))) {
+ // teleported?
+ this.removeFromOld(parameter, fromX, fromZ, oldViewDistance);
+ this.addToNew(parameter, toX, toZ, newViewDistance);
+ return true;
+ }
+
+ if (oldViewDistance != newViewDistance) {
+ // remove loop
+
+ final int oldMinX = fromX - oldViewDistance;
+ final int oldMinZ = fromZ - oldViewDistance;
+ final int oldMaxX = fromX + oldViewDistance;
+ final int oldMaxZ = fromZ + oldViewDistance;
+ for (int currX = oldMinX; currX <= oldMaxX; ++currX) {
+ for (int currZ = oldMinZ; currZ <= oldMaxZ; ++currZ) {
+
+ // only remove if we're outside the new view distance...
+ if (Math.max(IntegerUtil.branchlessAbs(currX - toX), IntegerUtil.branchlessAbs(currZ - toZ)) > newViewDistance) {
+ this.removeCallback(parameter, currX, currZ);
+ }
+ }
+ }
+
+ // add loop
+
+ final int newMinX = toX - newViewDistance;
+ final int newMinZ = toZ - newViewDistance;
+ final int newMaxX = toX + newViewDistance;
+ final int newMaxZ = toZ + newViewDistance;
+ for (int currX = newMinX; currX <= newMaxX; ++currX) {
+ for (int currZ = newMinZ; currZ <= newMaxZ; ++currZ) {
+
+ // only add if we're outside the old view distance...
+ if (Math.max(IntegerUtil.branchlessAbs(currX - fromX), IntegerUtil.branchlessAbs(currZ - fromZ)) > oldViewDistance) {
+ this.addCallback(parameter, currX, currZ);
+ }
+ }
+ }
+
+ return true;
+ }
+
+ // x axis is width
+ // z axis is height
+ // right refers to the x axis of where we moved
+ // top refers to the z axis of where we moved
+
+ // same view distance
+
+ // used for relative positioning
+ final int up = sign(dz); // 1 if dz >= 0, -1 otherwise
+ final int right = sign(dx); // 1 if dx >= 0, -1 otherwise
+
+ // The area excluded by overlapping the two view distance squares creates four rectangles:
+ // Two on the left, and two on the right. The ones on the left we consider the "removed" section
+ // and on the right the "added" section.
+ // https://i.imgur.com/MrnOBgI.png is a reference image. Note that the outside border is not actually
+ // exclusive to the regions they surround.
+
+ // 4 points of the rectangle
+ int maxX; // exclusive
+ int minX; // inclusive
+ int maxZ; // exclusive
+ int minZ; // inclusive
+
+ if (dx != 0) {
+ // handle right addition
+
+ maxX = toX + (oldViewDistance * right) + right; // exclusive
+ minX = fromX + (oldViewDistance * right) + right; // inclusive
+ maxZ = fromZ + (oldViewDistance * up) + up; // exclusive
+ minZ = toZ - (oldViewDistance * up); // inclusive
+
+ for (int currX = minX; currX != maxX; currX += right) {
+ for (int currZ = minZ; currZ != maxZ; currZ += up) {
+ this.addCallback(parameter, currX, currZ);
+ }
+ }
+ }
+
+ if (dz != 0) {
+ // handle up addition
+
+ maxX = toX + (oldViewDistance * right) + right; // exclusive
+ minX = toX - (oldViewDistance * right); // inclusive
+ maxZ = toZ + (oldViewDistance * up) + up; // exclusive
+ minZ = fromZ + (oldViewDistance * up) + up; // inclusive
+
+ for (int currX = minX; currX != maxX; currX += right) {
+ for (int currZ = minZ; currZ != maxZ; currZ += up) {
+ this.addCallback(parameter, currX, currZ);
+ }
+ }
+ }
+
+ if (dx != 0) {
+ // handle left removal
+
+ maxX = toX - (oldViewDistance * right); // exclusive
+ minX = fromX - (oldViewDistance * right); // inclusive
+ maxZ = fromZ + (oldViewDistance * up) + up; // exclusive
+ minZ = toZ - (oldViewDistance * up); // inclusive
+
+ for (int currX = minX; currX != maxX; currX += right) {
+ for (int currZ = minZ; currZ != maxZ; currZ += up) {
+ this.removeCallback(parameter, currX, currZ);
+ }
+ }
+ }
+
+ if (dz != 0) {
+ // handle down removal
+
+ maxX = fromX + (oldViewDistance * right) + right; // exclusive
+ minX = fromX - (oldViewDistance * right); // inclusive
+ maxZ = toZ - (oldViewDistance * up); // exclusive
+ minZ = fromZ - (oldViewDistance * up); // inclusive
+
+ for (int currX = minX; currX != maxX; currX += right) {
+ for (int currZ = minZ; currZ != maxZ; currZ += up) {
+ this.removeCallback(parameter, currX, currZ);
+ }
+ }
+ }
+
+ return true;
+ }
+
+ public final boolean remove() {
+ final int chunkX = this.lastChunkX;
+ final int chunkZ = this.lastChunkZ;
+ final int distance = this.distance;
+ if (chunkX == NOT_SET) {
+ return false;
+ }
+
+ this.lastChunkX = this.lastChunkZ = this.distance = NOT_SET;
+
+ this.removeFromOld(this.parameter, chunkX, chunkZ, distance);
+
+ return true;
+ }
+ }
+}
diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java
index e5d9c6f2cbe11c2ded6d8ad111fa6a8b2086dfba..c6d20bc2f0eab737338db6b88dacb63f0decb66c 100644
--- a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java
+++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java
@@ -1,5 +1,6 @@
package io.papermc.paper.chunk.system.scheduling;
+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
import ca.spottedleaf.concurrentutil.map.SWMRLong2ObjectHashTable;
import co.aikar.timings.Timing;
@@ -493,6 +494,21 @@ public final class ChunkHolderManager {
}
}
+ // atomic with respect to all add/remove/addandremove ticket calls for the given chunk
+ public <T, V> boolean addIfRemovedTicket(final long chunk, final TicketType<T> addType, final int addLevel, final T addIdentifier,
+ final TicketType<V> removeType, final int removeLevel, final V removeIdentifier) {
+ this.ticketLock.lock();
+ try {
+ if (this.removeTicketAtLevel(removeType, chunk, removeLevel, removeIdentifier)) {
+ this.addTicketAtLevel(addType, chunk, addLevel, addIdentifier);
+ return true;
+ }
+ return false;
+ } finally {
+ this.ticketLock.unlock();
+ }
+ }
+
public <T> void removeAllTicketsFor(final TicketType<T> ticketType, final int ticketLevel, final T ticketIdentifier) {
if (ticketLevel > MAX_TICKET_LEVEL) {
return;
@@ -900,6 +916,142 @@ public final class ChunkHolderManager {
}
}
+ public enum TicketOperationType {
+ ADD, REMOVE, ADD_IF_REMOVED, ADD_AND_REMOVE
+ }
+
+ public static record TicketOperation<T, V> (
+ TicketOperationType op, long chunkCoord,
+ TicketType<T> ticketType, int ticketLevel, T identifier,
+ TicketType<V> ticketType2, int ticketLevel2, V identifier2
+ ) {
+
+ private TicketOperation(TicketOperationType op, long chunkCoord,
+ TicketType<T> ticketType, int ticketLevel, T identifier) {
+ this(op, chunkCoord, ticketType, ticketLevel, identifier, null, 0, null);
+ }
+
+ public static <T> TicketOperation<T, T> addOp(final ChunkPos chunk, final TicketType<T> type, final int ticketLevel, final T identifier) {
+ return addOp(CoordinateUtils.getChunkKey(chunk), type, ticketLevel, identifier);
+ }
+
+ public static <T> TicketOperation<T, T> addOp(final int chunkX, final int chunkZ, final TicketType<T> type, final int ticketLevel, final T identifier) {
+ return addOp(CoordinateUtils.getChunkKey(chunkX, chunkZ), type, ticketLevel, identifier);
+ }
+
+ public static <T> TicketOperation<T, T> addOp(final long chunk, final TicketType<T> type, final int ticketLevel, final T identifier) {
+ return new TicketOperation<>(TicketOperationType.ADD, chunk, type, ticketLevel, identifier);
+ }
+
+ public static <T> TicketOperation<T, T> removeOp(final ChunkPos chunk, final TicketType<T> type, final int ticketLevel, final T identifier) {
+ return removeOp(CoordinateUtils.getChunkKey(chunk), type, ticketLevel, identifier);
+ }
+
+ public static <T> TicketOperation<T, T> removeOp(final int chunkX, final int chunkZ, final TicketType<T> type, final int ticketLevel, final T identifier) {
+ return removeOp(CoordinateUtils.getChunkKey(chunkX, chunkZ), type, ticketLevel, identifier);
+ }
+
+ public static <T> TicketOperation<T, T> removeOp(final long chunk, final TicketType<T> type, final int ticketLevel, final T identifier) {
+ return new TicketOperation<>(TicketOperationType.REMOVE, chunk, type, ticketLevel, identifier);
+ }
+
+ public static <T, V> TicketOperation<T, V> addIfRemovedOp(final long chunk,
+ final TicketType<T> addType, final int addLevel, final T addIdentifier,
+ final TicketType<V> removeType, final int removeLevel, final V removeIdentifier) {
+ return new TicketOperation<>(
+ TicketOperationType.ADD_IF_REMOVED, chunk, addType, addLevel, addIdentifier,
+ removeType, removeLevel, removeIdentifier
+ );
+ }
+
+ public static <T, V> TicketOperation<T, V> addAndRemove(final long chunk,
+ final TicketType<T> addType, final int addLevel, final T addIdentifier,
+ final TicketType<V> removeType, final int removeLevel, final V removeIdentifier) {
+ return new TicketOperation<>(
+ TicketOperationType.ADD_AND_REMOVE, chunk, addType, addLevel, addIdentifier,
+ removeType, removeLevel, removeIdentifier
+ );
+ }
+ }
+
+ private final MultiThreadedQueue<TicketOperation<?, ?>> delayedTicketUpdates = new MultiThreadedQueue<>();
+
+ // note: MUST hold ticket lock, otherwise operation ordering is lost
+ private boolean drainTicketUpdates() {
+ boolean ret = false;
+
+ TicketOperation operation;
+ while ((operation = this.delayedTicketUpdates.poll()) != null) {
+ switch (operation.op) {
+ case ADD: {
+ ret |= this.addTicketAtLevel(operation.ticketType, operation.chunkCoord, operation.ticketLevel, operation.identifier);
+ break;
+ }
+ case REMOVE: {
+ ret |= this.removeTicketAtLevel(operation.ticketType, operation.chunkCoord, operation.ticketLevel, operation.identifier);
+ break;
+ }
+ case ADD_IF_REMOVED: {
+ ret |= this.addIfRemovedTicket(
+ operation.chunkCoord,
+ operation.ticketType, operation.ticketLevel, operation.identifier,
+ operation.ticketType2, operation.ticketLevel2, operation.identifier2
+ );
+ break;
+ }
+ case ADD_AND_REMOVE: {
+ ret = true;
+ this.addAndRemoveTickets(
+ operation.chunkCoord,
+ operation.ticketType, operation.ticketLevel, operation.identifier,
+ operation.ticketType2, operation.ticketLevel2, operation.identifier2
+ );
+ break;
+ }
+ }
+ }
+
+ return ret;
+ }
+
+ public Boolean tryDrainTicketUpdates() {
+ final boolean acquired = this.ticketLock.tryLock();
+ try {
+ if (!acquired) {
+ return null;
+ }
+
+ return Boolean.valueOf(this.drainTicketUpdates());
+ } finally {
+ if (acquired) {
+ this.ticketLock.unlock();
+ }
+ }
+ }
+
+ public void pushDelayedTicketUpdate(final TicketOperation<?, ?> operation) {
+ this.delayedTicketUpdates.add(operation);
+ }
+
+ public void pushDelayedTicketUpdates(final Collection<TicketOperation<?, ?>> operations) {
+ this.delayedTicketUpdates.addAll(operations);
+ }
+
+ public Boolean tryProcessTicketUpdates() {
+ final boolean acquired = this.ticketLock.tryLock();
+ try {
+ if (!acquired) {
+ return null;
+ }
+
+ return Boolean.valueOf(this.processTicketUpdates(false, true, null));
+ } finally {
+ if (acquired) {
+ this.ticketLock.unlock();
+ }
+ }
+ }
+
private final ThreadLocal<Boolean> BLOCK_TICKET_UPDATES = ThreadLocal.withInitial(() -> {
return Boolean.FALSE;
});
@@ -948,6 +1100,8 @@ public final class ChunkHolderManager {
this.ticketLock.lock();
try {
+ this.drainTicketUpdates();
+
final boolean levelsUpdated = this.ticketLevelPropagator.propagateUpdates();
if (levelsUpdated) {
// Unlike CB, ticket level updates cannot happen recursively. Thank god.
diff --git a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
index 8d442c5a498ecf288a0cc0c54889c6e2fda849ce..9f5f0d8ddc8f480b48079c70e38c9c08eff403f6 100644
--- a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
+++ b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
@@ -287,4 +287,43 @@ public class GlobalConfiguration extends ConfigurationPart {
public boolean useDimensionTypeForCustomSpawners = false;
public boolean strictAdvancementDimensionCheck = false;
}
+
+ public ChunkLoadingBasic chunkLoadingBasic;
+
+ public class ChunkLoadingBasic extends ConfigurationPart {
+ @Comment("The maximum rate in chunks per second that the server will send to any individual player. Set to -1 to disable this limit.")
+ public double playerMaxChunkSendRate = 75.0;
+
+ @Comment(
+ "The maximum rate at which chunks will load for any individual player. " +
+ "Note that this setting also affects chunk generations, since a chunk load is always first issued to test if a" +
+ "chunk is already generated. Set to -1 to disable this limit."
+ )
+ public double playerMaxChunkLoadRate = 100.0;
+
+ @Comment("The maximum rate at which chunks will generate for any individual player. Set to -1 to disable this limit.")
+ public double playerMaxChunkGenerateRate = -1.0;
+ }
+
+ public ChunkLoadingAdvanced chunkLoadingAdvanced;
+
+ public class ChunkLoadingAdvanced extends ConfigurationPart {
+ @Comment(
+ "Set to true if the server will match the chunk send radius that clients have configured" +
+ "in their view distance settings if the client is less-than the server's send distance."
+ )
+ public boolean autoConfigSendDistance = true;
+
+ @Comment(
+ "Specifies the maximum amount of concurrent chunk loads that an individual player can have." +
+ "Set to 0 to let the server configure it automatically per player, or set it to -1 to disable the limit."
+ )
+ public int playerMaxConcurrentChunkLoads = 0;
+
+ @Comment(
+ "Specifies the maximum amount of concurrent chunk generations that an individual player can have." +
+ "Set to 0 to let the server configure it automatically per player, or set it to -1 to disable the limit."
+ )
+ public int playerMaxConcurrentChunkGenerates = 0;
+ }
}
diff --git a/src/main/java/io/papermc/paper/util/IntervalledCounter.java b/src/main/java/io/papermc/paper/util/IntervalledCounter.java
index cea9c098ade00ee87b8efc8164ab72f5279758f0..197224e31175252d8438a8df585bbb65f2288d7f 100644
--- a/src/main/java/io/papermc/paper/util/IntervalledCounter.java
+++ b/src/main/java/io/papermc/paper/util/IntervalledCounter.java
@@ -2,6 +2,8 @@ package io.papermc.paper.util;
public final class IntervalledCounter {
+ private static final int INITIAL_SIZE = 8;
+
protected long[] times;
protected long[] counts;
protected final long interval;
@@ -11,8 +13,8 @@ public final class IntervalledCounter {
protected int tail; // exclusive
public IntervalledCounter(final long interval) {
- this.times = new long[8];
- this.counts = new long[8];
+ this.times = new long[INITIAL_SIZE];
+ this.counts = new long[INITIAL_SIZE];
this.interval = interval;
}
@@ -67,13 +69,13 @@ public final class IntervalledCounter {
this.tail = nextTail;
}
- public void updateAndAdd(final int count) {
+ public void updateAndAdd(final long count) {
final long currTime = System.nanoTime();
this.updateCurrentTime(currTime);
this.addTime(currTime, count);
}
- public void updateAndAdd(final int count, final long currTime) {
+ public void updateAndAdd(final long count, final long currTime) {
this.updateCurrentTime(currTime);
this.addTime(currTime, count);
}
@@ -93,9 +95,13 @@ public final class IntervalledCounter {
this.tail = size;
if (tail >= head) {
+ // sequentially ordered from [head, tail)
System.arraycopy(oldElements, head, newElements, 0, size);
System.arraycopy(oldCounts, head, newCounts, 0, size);
} else {
+ // ordered from [head, length)
+ // then followed by [0, tail)
+
System.arraycopy(oldElements, head, newElements, 0, oldElements.length - head);
System.arraycopy(oldElements, 0, newElements, oldElements.length - head, tail);
@@ -106,10 +112,18 @@ public final class IntervalledCounter {
// returns in units per second
public double getRate() {
- return this.size() / (this.interval * 1.0e-9);
+ return (double)this.sum / ((double)this.interval * 1.0E-9);
+ }
+
+ public long getInterval() {
+ return this.interval;
}
- public long size() {
+ public long getSum() {
return this.sum;
}
+
+ public int totalDataPoints() {
+ return this.tail >= this.head ? (this.tail - this.head) : (this.tail + (this.counts.length - this.head));
+ }
}
diff --git a/src/main/java/io/papermc/paper/util/MCUtil.java b/src/main/java/io/papermc/paper/util/MCUtil.java
2023-03-23 14:55:09 +01:00
index 6efb8b10f17c70b05128039376d254e6beda3841..c856a9a0d085b278da416c59996fc131811f790c 100644
2023-02-23 17:13:45 +01:00
--- a/src/main/java/io/papermc/paper/util/MCUtil.java
+++ b/src/main/java/io/papermc/paper/util/MCUtil.java
2023-03-23 14:55:09 +01:00
@@ -607,8 +607,8 @@ public final class MCUtil {
2023-02-23 17:13:45 +01:00
worldData.addProperty("is-loaded", loadedWorlds.contains(bukkitWorld));
worldData.addProperty("name", world.getWorld().getName());
- worldData.addProperty("view-distance", world.getChunkSource().chunkMap.playerChunkManager.getTargetNoTickViewDistance()); // Paper - replace chunk loader system
- worldData.addProperty("tick-view-distance", world.getChunkSource().chunkMap.playerChunkManager.getTargetTickViewDistance()); // Paper - replace chunk loader system
+ worldData.addProperty("view-distance", world.getWorld().getViewDistance()); // Paper - replace chunk loader system
+ worldData.addProperty("tick-view-distance", world.getWorld().getSimulationDistance()); // Paper - replace chunk loader system
worldData.addProperty("keep-spawn-loaded", world.keepSpawnInMemory);
worldData.addProperty("keep-spawn-loaded-range", world.paperConfig().spawn.keepSpawnLoadedRange * 16);
diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java
2023-03-23 14:55:09 +01:00
index 904fcdeb7937d36208cc9a8d5eca9ef3a5b2cd9e..6fce2a9bce051e21eba8f331007a9752607f69f2 100644
2023-02-23 17:13:45 +01:00
--- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
+++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java
@@ -128,6 +128,26 @@ public class ChunkHolder {
com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> playersInChunkTickRange;
// Paper end - optimise anyPlayerCloseEnoughForSpawning
+ // Paper start - replace player chunk loader
+ private final com.destroystokyo.paper.util.maplist.ReferenceList<ServerPlayer> playersSentChunkTo = new com.destroystokyo.paper.util.maplist.ReferenceList<>();
+
+ public void addPlayer(ServerPlayer player) {
+ if (!this.playersSentChunkTo.add(player)) {
+ throw new IllegalStateException("Already sent chunk " + this.pos + " to player " + player);
+ }
+ }
+
+ public void removePlayer(ServerPlayer player) {
+ if (!this.playersSentChunkTo.remove(player)) {
+ throw new IllegalStateException("Have not sent chunk " + this.pos + " to player " + player);
+ }
+ }
+
+ public boolean hasChunkBeenSent() {
+ return this.playersSentChunkTo.size() != 0;
+ }
+ // Paper end - replace player chunk loader
+
public ChunkHolder(ChunkPos pos, LevelHeightAccessor world, LevelLightEngine lightingProvider, ChunkHolder.PlayerProvider playersWatchingChunkProvider, io.papermc.paper.chunk.system.scheduling.NewChunkHolder newChunkHolder) { // Paper - rewrite chunk system
this.newChunkHolder = newChunkHolder; // Paper - rewrite chunk system
this.chunkToSaveHistory = null;
@@ -225,6 +245,11 @@ public class ChunkHolder {
// Paper - rewrite chunk system
public void blockChanged(BlockPos pos) {
+ // Paper start - replace player chunk loader
+ if (this.playersSentChunkTo.size() == 0) {
+ return;
+ }
+ // Paper end - replace player chunk loader
LevelChunk chunk = this.getSendingChunk(); // Paper - no-tick view distance
if (chunk != null) {
@@ -251,7 +276,7 @@ public class ChunkHolder {
LevelChunk chunk = this.getSendingChunk();
// Paper end - no-tick view distance
- if (chunk != null) {
+ if (this.playersSentChunkTo.size() != 0 && chunk != null) { // Paper - replace player chunk loader
int j = this.lightEngine.getMinLightSection();
int k = this.lightEngine.getMaxLightSection();
@@ -351,27 +376,32 @@ public class ChunkHolder {
}
- public void broadcast(Packet<?> packet, boolean onlyOnWatchDistanceEdge) {
- // Paper 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.PlayerAreaMap viewDistanceMap = this.chunkMap.playerChunkManager.broadcastMap; // Paper - replace old player chunk manager
- com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> players = viewDistanceMap.getObjectsInRange(this.pos);
- if (players == null) {
- return;
- }
+ // Paper start - rewrite player chunk loader
+ public List<ServerPlayer> getPlayers(boolean onlyOnWatchDistanceEdge) {
+ List<ServerPlayer> ret = new java.util.ArrayList<>();
- Object[] backingSet = players.getBackingSet();
- for (int i = 0, len = backingSet.length; i < len; ++i) {
- if (!(backingSet[i] instanceof ServerPlayer player)) {
+ for (int i = 0, len = this.playersSentChunkTo.size(); i < len; ++i) {
+ ServerPlayer player = this.playersSentChunkTo.getUnchecked(i);
+ if (onlyOnWatchDistanceEdge && !this.chunkMap.level.playerChunkLoader.isChunkSent(player, this.pos.x, this.pos.z, onlyOnWatchDistanceEdge)) {
continue;
}
- if (!this.chunkMap.playerChunkManager.isChunkSent(player, this.pos.x, this.pos.z, onlyOnWatchDistanceEdge)) {
+ ret.add(player);
+ }
+
+ return ret;
+ }
+ // Paper end - rewrite player chunk loader
+
+ public void broadcast(Packet<?> packet, boolean onlyOnWatchDistanceEdge) {
+ // Paper start - rewrite player chunk loader - modeled after the above
+ for (int i = 0, len = this.playersSentChunkTo.size(); i < len; ++i) {
+ ServerPlayer player = this.playersSentChunkTo.getUnchecked(i);
+ if (onlyOnWatchDistanceEdge && !this.chunkMap.level.playerChunkLoader.isChunkSent(player, this.pos.x, this.pos.z, onlyOnWatchDistanceEdge)) {
continue;
}
player.connection.send(packet);
}
- // Paper end - per player view distance
+ // Paper end - rewrite player chunk loader
}
// Paper - rewrite chunk system
diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
2023-03-23 14:55:09 +01:00
index fbe209a66c77c47935ad026dd3e45e682af91fd8..2d133ae656f7420d6ceec14bc591721cff815479 100644
2023-02-23 17:13:45 +01:00
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
2023-03-23 14:55:09 +01:00
@@ -199,7 +199,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
2023-02-23 17:13:45 +01:00
// Paper end - use distance map to optimise tracker
void addPlayerToDistanceMaps(ServerPlayer player) {
- this.playerChunkManager.addPlayer(player); // Paper - replace chunk loader
+ this.level.playerChunkLoader.addPlayer(player); // Paper - replace chunk loader
int chunkX = MCUtil.getChunkCoordinate(player.getX());
int chunkZ = MCUtil.getChunkCoordinate(player.getZ());
// Note: players need to be explicitly added to distance maps before they can be updated
2023-03-23 14:55:09 +01:00
@@ -221,7 +221,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
2023-02-23 17:13:45 +01:00
}
void removePlayerFromDistanceMaps(ServerPlayer player) {
- this.playerChunkManager.removePlayer(player); // Paper - replace chunk loader
+ this.level.playerChunkLoader.removePlayer(player); // Paper - replace chunk loader
// Paper start - optimise ChunkMap#anyPlayerCloseEnoughForSpawning
this.playerMobSpawnMap.remove(player);
2023-03-23 14:55:09 +01:00
@@ -244,7 +244,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
2023-02-23 17:13:45 +01:00
int chunkX = MCUtil.getChunkCoordinate(player.getX());
int chunkZ = MCUtil.getChunkCoordinate(player.getZ());
// Note: players need to be explicitly added to distance maps before they can be updated
- this.playerChunkManager.updatePlayer(player); // Paper - replace chunk loader
+ this.level.playerChunkLoader.updatePlayer(player); // Paper - replace chunk loader
this.playerChunkTickRangeMap.update(player, chunkX, chunkZ, DistanceManager.MOB_SPAWN_RANGE); // Paper - optimise ChunkMap#anyPlayerCloseEnoughForSpawning
// Paper start - per player mob spawning
if (this.playerMobDistanceMap != null) {
2023-03-23 14:55:09 +01:00
@@ -816,7 +816,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
2023-02-23 17:13:45 +01:00
// Paper start - replace player loader system
public void setTickViewDistance(int distance) {
- this.playerChunkManager.setTickDistance(distance);
+ this.level.playerChunkLoader.setTickDistance(distance);
+ }
+
+ public void setSendViewDistance(int distance) {
+ this.level.playerChunkLoader.setSendDistance(distance);
}
// Paper end - replace player loader system
public void setViewDistance(int watchDistance) {
2023-03-23 14:55:09 +01:00
@@ -826,20 +830,22 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
2023-02-23 17:13:45 +01:00
int k = this.viewDistance;
this.viewDistance = j;
- this.playerChunkManager.setLoadDistance(this.viewDistance); // Paper - replace player loader system
+ this.level.playerChunkLoader.setLoadDistance(this.viewDistance); // Paper - replace player loader system
}
}
public void updateChunkTracking(ServerPlayer player, ChunkPos pos, MutableObject<java.util.Map<Object, ClientboundLevelChunkWithLightPacket>> packet, boolean oldWithinViewDistance, boolean newWithinViewDistance) { // Paper - public // Paper - Anti-Xray - Bypass
if (player.level == this.level) {
+ ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos.toLong()); // Paper - replace chunk loader system - move up
if (newWithinViewDistance && !oldWithinViewDistance) {
- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos.toLong());
+ // Paper - replace chunk loader system - move up
if (playerchunk != null) {
LevelChunk chunk = playerchunk.getSendingChunk(); // Paper - replace chunk loader system
if (chunk != null) {
+ playerchunk.addPlayer(player); // Paper - replace chunk loader system
this.playerLoadedChunk(player, packet, chunk);
}
2023-03-23 14:55:09 +01:00
@@ -848,6 +854,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
2023-02-23 17:13:45 +01:00
}
if (!newWithinViewDistance && oldWithinViewDistance) {
+ // Paper start - replace chunk loader system
+ if (playerchunk != null) {
+ playerchunk.removePlayer(player);
+ }
+ // Paper end - replace chunk loader system
player.untrackChunk(pos);
}
2023-03-23 14:55:09 +01:00
@@ -1151,34 +1162,18 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
2023-02-23 17:13:45 +01:00
// Paper - replaced by PlayerChunkLoader
this.updateMaps(player); // Paper - distance maps
- this.playerChunkManager.updatePlayer(player); // Paper - respond to movement immediately
}
@Override
public List<ServerPlayer> getPlayers(ChunkPos chunkPos, boolean onlyOnWatchDistanceEdge) {
// Paper 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<ServerPlayer> players = this.playerChunkManager.broadcastMap.getObjectsInRange(chunkPos);
- if (players == null) {
- return java.util.Collections.emptyList();
- }
-
- List<ServerPlayer> ret = new java.util.ArrayList<>(players.size());
-
- Object[] backingSet = players.getBackingSet();
- for (int i = 0, len = backingSet.length; i < len; ++i) {
- if (!(backingSet[i] instanceof ServerPlayer player)) {
- continue;
- }
- if (!this.playerChunkManager.isChunkSent(player, chunkPos.x, chunkPos.z, onlyOnWatchDistanceEdge)) {
- continue;
- }
- ret.add(player);
+ ChunkHolder holder = this.getVisibleChunkIfPresent(chunkPos.toLong());
+ if (holder == null) {
+ return new java.util.ArrayList<>();
+ } else {
+ return holder.getPlayers(onlyOnWatchDistanceEdge);
}
-
- return ret;
// Paper end - per player view distance
}
2023-03-23 14:55:09 +01:00
@@ -1612,7 +1607,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
2023-02-23 17:13:45 +01:00
double vec3d_dx = player.getX() - this.entity.getX();
double vec3d_dz = player.getZ() - this.entity.getZ();
// Paper end - remove allocation of Vec3D here
- double d0 = (double) Math.min(this.getEffectiveRange(), io.papermc.paper.chunk.PlayerChunkLoader.getSendViewDistance(player) * 16); // Paper - per player view distance
+ double d0 = (double) Math.min(this.getEffectiveRange(), io.papermc.paper.chunk.system.ChunkSystem.getSendViewDistance(player) * 16); // Paper - per player view distance
double d1 = vec3d_dx * vec3d_dx + vec3d_dz * vec3d_dz; // Paper
double d2 = d0 * d0;
boolean flag = d1 <= d2 && this.entity.broadcastToPlayer(player);
diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java
index 52cba8f68d274cce106304aef1249a95474d3238..88fca8b160df6804f30ed2cf8cf1f645085434e2 100644
--- a/src/main/java/net/minecraft/server/level/DistanceManager.java
+++ b/src/main/java/net/minecraft/server/level/DistanceManager.java
@@ -184,17 +184,17 @@ public abstract class DistanceManager {
}
protected void updatePlayerTickets(int viewDistance) {
- this.chunkMap.playerChunkManager.setTargetNoTickViewDistance(viewDistance); // Paper - route to player chunk manager
+ this.chunkMap.setViewDistance(viewDistance);// Paper - route to player chunk manager
}
// Paper start
public int getSimulationDistance() {
- return this.chunkMap.playerChunkManager.getTargetTickViewDistance(); // Paper - route to player chunk manager
+ return this.chunkMap.level.playerChunkLoader.getAPITickDistance();
}
// Paper end
public void updateSimulationDistance(int simulationDistance) {
- this.chunkMap.playerChunkManager.setTargetTickViewDistance(simulationDistance); // Paper - route to player chunk manager
+ this.chunkMap.level.playerChunkLoader.setTickDistance(simulationDistance); // Paper - route to player chunk manager
}
public int getNaturalSpawnChunkCount() {
diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
index ca84eddbdb1e198b899750e5f6b3eafd25ce970f..736f37979c882e41e7571202df38eb6a2923fcb0 100644
--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
@@ -645,7 +645,7 @@ public class ServerChunkCache extends ChunkSource {
this.level.getProfiler().popPush("chunks");
if (tickChunks) {
this.level.timings.chunks.startTiming(); // Paper - timings
- this.chunkMap.playerChunkManager.tick(); // Paper - this is mostly is to account for view distance changes
+ this.chunkMap.level.playerChunkLoader.tick(); // Paper - replace player chunk loader - this is mostly required to account for view distance changes
this.tickChunks();
this.level.timings.chunks.stopTiming(); // Paper - timings
}
@@ -1001,7 +1001,7 @@ public class ServerChunkCache extends ChunkSource {
@Override
// CraftBukkit start - process pending Chunk loadCallback() and unloadCallback() after each run task
public boolean pollTask() {
- ServerChunkCache.this.chunkMap.playerChunkManager.tickMidTick();
+ ServerChunkCache.this.chunkMap.level.playerChunkLoader.tickMidTick(); // Paper - replace player chunk loader
if (ServerChunkCache.this.runDistanceManagerUpdates()) {
return true;
}
diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
2023-03-25 00:22:47 +01:00
index 5a5ff40df37db9cbd53c584ed26a3ce4888b29c0..bf1a77cf9bbea4e2104b2a8c61309e740f28d51b 100644
2023-02-23 17:13:45 +01:00
--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
2023-03-23 14:55:09 +01:00
@@ -521,6 +521,48 @@ public class ServerLevel extends Level implements WorldGenLevel {
2023-02-23 17:13:45 +01:00
}
// Paper end - optimise get nearest players for entity AI
2023-03-25 00:22:47 +01:00
+ public final io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader playerChunkLoader = new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader(this);
+ private final java.util.concurrent.atomic.AtomicReference<io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances> viewDistances = new java.util.concurrent.atomic.AtomicReference<>(new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances(-1, -1, -1));
2023-02-23 17:13:45 +01:00
+
2023-03-25 00:22:47 +01:00
+ public io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances getViewDistances() {
2023-02-23 17:13:45 +01:00
+ return this.viewDistances.get();
+ }
+
2023-03-25 00:22:47 +01:00
+ private void updateViewDistance(final java.util.function.Function<io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances, io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances> update) {
+ for (io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances curr = this.viewDistances.get();;) {
2023-02-23 17:13:45 +01:00
+ if (this.viewDistances.compareAndSet(curr, update.apply(curr))) {
+ return;
+ }
+ }
+ }
+
+ public void setTickViewDistance(final int distance) {
2023-03-25 00:22:47 +01:00
+ if ((distance < io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE || distance > io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE)) {
+ throw new IllegalArgumentException("Tick view distance must be a number between " + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE + " and " + (io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE) + ", got: " + distance);
2023-02-23 17:13:45 +01:00
+ }
+ this.updateViewDistance((input) -> {
+ return input.setTickViewDistance(distance);
+ });
+ }
+
+ public void setLoadViewDistance(final int distance) {
2023-03-25 00:22:47 +01:00
+ if (distance != -1 && (distance < io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE || distance > io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1)) {
+ throw new IllegalArgumentException("Load view distance must be a number between " + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE + " and " + (io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1) + " or -1, got: " + distance);
2023-02-23 17:13:45 +01:00
+ }
+ this.updateViewDistance((input) -> {
+ return input.setLoadViewDistance(distance);
+ });
+ }
+
+ public void setSendViewDistance(final int distance) {
2023-03-25 00:22:47 +01:00
+ if (distance != -1 && (distance < io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE || distance > io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1)) {
+ throw new IllegalArgumentException("Send view distance must be a number between " + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE + " and " + (io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1) + " or -1, got: " + distance);
2023-02-23 17:13:45 +01:00
+ }
+ this.updateViewDistance((input) -> {
+ return input.setSendViewDistance(distance);
+ });
+ }
+
// Add env and gen to constructor, IWorldDataServer -> WorldDataServer
public ServerLevel(MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PrimaryLevelData iworlddataserver, ResourceKey<Level> resourcekey, LevelStem worlddimension, ChunkProgressListener worldloadlistener, boolean flag, long i, List<CustomSpawner> list, boolean flag1, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider) {
2023-03-23 14:55:09 +01:00
// IRegistryCustom.Dimension iregistrycustom_dimension = minecraftserver.registryAccess(); // CraftBukkit - decompile error
2023-02-23 17:13:45 +01:00
diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java
2023-03-30 03:26:03 +02:00
index 1d4d02f26391ac55c7631817f09d05e2769b0d29..4231c3fe14a621d237bdc3d56c593f0adc0a7bc6 100644
2023-02-23 17:13:45 +01:00
--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
2023-03-30 03:26:03 +02:00
@@ -276,6 +276,48 @@ public class ServerPlayer extends Player {
2023-02-23 17:13:45 +01:00
public PlayerNaturallySpawnCreaturesEvent playerNaturallySpawnedEvent; // Paper
public org.bukkit.event.player.PlayerQuitEvent.QuitReason quitReason = null; // Paper - there are a lot of changes to do if we change all methods leading to the event
2023-03-25 00:22:47 +01:00
+ private final java.util.concurrent.atomic.AtomicReference<io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances> viewDistances = new java.util.concurrent.atomic.AtomicReference<>(new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances(-1, -1, -1));
+ public io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.PlayerChunkLoaderData chunkLoader;
2023-02-23 17:13:45 +01:00
+
2023-03-25 00:22:47 +01:00
+ public io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances getViewDistances() {
2023-02-23 17:13:45 +01:00
+ return this.viewDistances.get();
+ }
+
2023-03-25 00:22:47 +01:00
+ private void updateViewDistance(final java.util.function.Function<io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances, io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances> update) {
+ for (io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances curr = this.viewDistances.get();;) {
2023-02-23 17:13:45 +01:00
+ if (this.viewDistances.compareAndSet(curr, update.apply(curr))) {
+ return;
+ }
+ }
+ }
+
+ public void setTickViewDistance(final int distance) {
2023-03-25 00:22:47 +01:00
+ if ((distance < io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE || distance > io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE)) {
+ throw new IllegalArgumentException("Tick view distance must be a number between " + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE + " and " + (io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE) + ", got: " + distance);
2023-02-23 17:13:45 +01:00
+ }
+ this.updateViewDistance((input) -> {
+ return input.setTickViewDistance(distance);
+ });
+ }
+
+ public void setLoadViewDistance(final int distance) {
2023-03-25 00:22:47 +01:00
+ if (distance != -1 && (distance < io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE || distance > io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1)) {
+ throw new IllegalArgumentException("Load view distance must be a number between " + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE + " and " + (io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1) + " or -1, got: " + distance);
2023-02-23 17:13:45 +01:00
+ }
+ this.updateViewDistance((input) -> {
+ return input.setLoadViewDistance(distance);
+ });
+ }
+
+ public void setSendViewDistance(final int distance) {
2023-03-25 00:22:47 +01:00
+ if (distance != -1 && (distance < io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE || distance > io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1)) {
+ throw new IllegalArgumentException("Send view distance must be a number between " + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE + " and " + (io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1) + " or -1, got: " + distance);
2023-02-23 17:13:45 +01:00
+ }
+ this.updateViewDistance((input) -> {
+ return input.setSendViewDistance(distance);
+ });
+ }
+
public ServerPlayer(MinecraftServer server, ServerLevel world, GameProfile profile) {
super(world, world.getSharedSpawnPos(), world.getSharedSpawnAngle(), profile);
this.chatVisibility = ChatVisiblity.FULL;
diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java
2023-03-23 14:55:09 +01:00
index c0c14766adaac855112f85a203a6163b8adfdded..0dbe182fbae5ce5ba182176eb5d5e3f1897e77f2 100644
2023-02-23 17:13:45 +01:00
--- a/src/main/java/net/minecraft/server/players/PlayerList.java
+++ b/src/main/java/net/minecraft/server/players/PlayerList.java
2023-03-23 14:55:09 +01:00
@@ -271,7 +271,7 @@ public abstract class PlayerList {
2023-02-23 17:13:45 +01:00
boolean flag1 = gamerules.getBoolean(GameRules.RULE_REDUCEDDEBUGINFO);
// Spigot - view distance
- playerconnection.send(new ClientboundLoginPacket(player.getId(), worlddata.isHardcore(), player.gameMode.getGameModeForPlayer(), player.gameMode.getPreviousGameModeForPlayer(), this.server.levelKeys(), this.synchronizedRegistries, worldserver1.dimensionTypeId(), worldserver1.dimension(), BiomeManager.obfuscateSeed(worldserver1.getSeed()), this.getMaxPlayers(), worldserver1.getChunkSource().chunkMap.playerChunkManager.getTargetSendDistance(), worldserver1.getChunkSource().chunkMap.playerChunkManager.getTargetTickViewDistance(), flag1, !flag, worldserver1.isDebug(), worldserver1.isFlat(), player.getLastDeathLocation())); // Paper - replace old player chunk management
+ playerconnection.send(new ClientboundLoginPacket(player.getId(), worlddata.isHardcore(), player.gameMode.getGameModeForPlayer(), player.gameMode.getPreviousGameModeForPlayer(), this.server.levelKeys(), this.synchronizedRegistries, worldserver1.dimensionTypeId(), worldserver1.dimension(), BiomeManager.obfuscateSeed(worldserver1.getSeed()), this.getMaxPlayers(), worldserver1.getWorld().getSendViewDistance(), worldserver1.getWorld().getSimulationDistance(), flag1, !flag, worldserver1.isDebug(), worldserver1.isFlat(), player.getLastDeathLocation())); // Paper - replace old player chunk management
player.getBukkitEntity().sendSupportedChannels(); // CraftBukkit
playerconnection.send(new ClientboundUpdateEnabledFeaturesPacket(FeatureFlags.REGISTRY.toNames(worldserver1.enabledFeatures())));
playerconnection.send(new ClientboundCustomPayloadPacket(ClientboundCustomPayloadPacket.BRAND, (new FriendlyByteBuf(Unpooled.buffer())).writeUtf(this.getServer().getServerModName())));
2023-03-23 14:55:09 +01:00
@@ -905,8 +905,8 @@ public abstract class PlayerList {
2023-02-23 17:13:45 +01:00
// CraftBukkit start
LevelData worlddata = worldserver1.getLevelData();
entityplayer1.connection.send(new ClientboundRespawnPacket(worldserver1.dimensionTypeId(), worldserver1.dimension(), BiomeManager.obfuscateSeed(worldserver1.getSeed()), entityplayer1.gameMode.getGameModeForPlayer(), entityplayer1.gameMode.getPreviousGameModeForPlayer(), worldserver1.isDebug(), worldserver1.isFlat(), (byte) i, entityplayer1.getLastDeathLocation()));
- entityplayer1.connection.send(new ClientboundSetChunkCacheRadiusPacket(worldserver1.getChunkSource().chunkMap.playerChunkManager.getTargetSendDistance())); // Spigot // Paper - replace old player chunk management
- entityplayer1.connection.send(new ClientboundSetSimulationDistancePacket(worldserver1.getChunkSource().chunkMap.playerChunkManager.getTargetTickViewDistance())); // Spigot // Paper - replace old player chunk management
+ entityplayer1.connection.send(new ClientboundSetChunkCacheRadiusPacket(worldserver1.getWorld().getSendViewDistance())); // Spigot // Paper - replace old player chunk management
+ entityplayer1.connection.send(new ClientboundSetSimulationDistancePacket(worldserver1.getWorld().getSimulationDistance())); // Spigot // Paper - replace old player chunk management
entityplayer1.spawnIn(worldserver1);
entityplayer1.unsetRemoved();
entityplayer1.connection.teleport(new Location(worldserver1.getWorld(), entityplayer1.getX(), entityplayer1.getY(), entityplayer1.getZ(), entityplayer1.getYRot(), entityplayer1.getXRot()));
diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java
2023-03-23 14:55:09 +01:00
index 973ecd50f9cb6b86c353586e84d15dcb118ccb60..944da18bcc993ab0488a34cbbe9df134c355301a 100644
2023-02-23 17:13:45 +01:00
--- a/src/main/java/net/minecraft/world/level/Level.java
+++ b/src/main/java/net/minecraft/world/level/Level.java
2023-03-23 14:55:09 +01:00
@@ -635,7 +635,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
2023-02-23 17:13:45 +01:00
this.sendBlockUpdated(blockposition, iblockdata1, iblockdata, i);
// Paper 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 || ((ServerLevel)this).getChunkSource().chunkMap.playerChunkManager.broadcastMap.getObjectsInRange(io.papermc.paper.util.MCUtil.getCoordinateKey(blockposition)) != null)) { // Paper - replace old player chunk management
+ } else if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0)) { // Paper - replace old player chunk management
((ServerLevel)this).getChunkSource().blockChanged(blockposition);
// Paper end - per player view distance
}
diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
2023-03-23 14:55:09 +01:00
index 206dc04086a218b510930739a6c573f2653ab0fa..9599af33c683ec47e28b1c8e4dc965d30d9081a7 100644
2023-02-23 17:13:45 +01:00
--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
@@ -184,43 +184,6 @@ public class LevelChunk extends ChunkAccess {
protected void onNeighbourChange(final long bitsetBefore, final long bitsetAfter) {
- // Paper start - no-tick view distance
- ServerChunkCache chunkProviderServer = ((ServerLevel)this.level).getChunkSource();
- net.minecraft.server.level.ChunkMap chunkMap = chunkProviderServer.chunkMap;
- // this code handles the addition of ticking tickets - the distance map handles the removal
- if (!areNeighboursLoaded(bitsetBefore, 2) && areNeighboursLoaded(bitsetAfter, 2)) {
- if (chunkMap.playerChunkManager.tickMap.getObjectsInRange(this.coordinateKey) != null) { // Paper - replace old player chunk loading system
- // now we're ready for entity ticking
- chunkProviderServer.mainThreadProcessor.execute(() -> {
- // double check that this condition still holds.
- if (LevelChunk.this.areNeighboursLoaded(2) && chunkMap.playerChunkManager.tickMap.getObjectsInRange(LevelChunk.this.coordinateKey) != null) { // Paper - replace old player chunk loading system
- chunkMap.playerChunkManager.onChunkPlayerTickReady(this.chunkPos.x, this.chunkPos.z); // Paper - replace old player chunk
- chunkProviderServer.addTicketAtLevel(net.minecraft.server.level.TicketType.PLAYER, LevelChunk.this.chunkPos, 31, LevelChunk.this.chunkPos); // 31 -> entity ticking, TODO check on update
- }
- });
- }
- }
-
- // this code handles the chunk sending
- if (!areNeighboursLoaded(bitsetBefore, 1) && areNeighboursLoaded(bitsetAfter, 1)) {
- // Paper start - replace old player chunk loading system
- if (chunkMap.playerChunkManager.isChunkNearPlayers(this.chunkPos.x, this.chunkPos.z)) {
- // the post processing is expensive, so we don't want to run it unless we're actually near
- // a player.
- chunkProviderServer.mainThreadProcessor.execute(() -> {
- if (!LevelChunk.this.areNeighboursLoaded(1)) {
- return;
- }
- LevelChunk.this.postProcessGeneration();
- if (!LevelChunk.this.areNeighboursLoaded(1)) {
- return;
- }
- chunkMap.playerChunkManager.onChunkSendReady(this.chunkPos.x, this.chunkPos.z);
- });
- }
- // Paper end - replace old player chunk loading system
- }
- // Paper end - no-tick view distance
}
public final boolean isAnyNeighborsLoaded() {
@@ -906,7 +869,6 @@ public class LevelChunk extends ChunkAccess {
// Paper - rewrite chunk system - move into separate callback
org.bukkit.Server server = this.level.getCraftServer();
// Paper - rewrite chunk system - move into separate callback
- ((ServerLevel)this.level).getChunkSource().chunkMap.playerChunkManager.onChunkLoad(this.chunkPos.x, this.chunkPos.z); // Paper - rewrite player chunk management
if (server != null) {
/*
* If it's a new world, the first few chunks are generated inside
2023-03-23 14:55:09 +01:00
@@ -1086,6 +1048,7 @@ public class LevelChunk extends ChunkAccess {
2023-02-23 17:13:45 +01:00
BlockState iblockdata1 = Block.updateFromNeighbourShapes(iblockdata, this.level, blockposition);
this.level.setBlock(blockposition, iblockdata1, 20);
+ if (iblockdata1 != iblockdata) this.level.chunkSource.blockChanged(blockposition); // Paper - replace player chunk loader - notify since we send before processing full updates
}
}
2023-03-23 14:55:09 +01:00
@@ -1105,7 +1068,6 @@ public class LevelChunk extends ChunkAccess {
2023-02-23 17:13:45 +01:00
this.upgradeData.upgrade(this);
} finally { // Paper start - replace chunk loader system
this.isPostProcessingDone = true;
- this.level.getChunkSource().chunkMap.playerChunkManager.onChunkPostProcessing(this.chunkPos.x, this.chunkPos.z);
}
// Paper end - replace chunk loader system
}
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
2023-03-23 14:55:09 +01:00
index 5e9055fdf411029ea2fed91acd6b981f79156418..ff6559bf563f2fdcc0f2843d4f4aa24d7ddfb6db 100644
2023-02-23 17:13:45 +01:00
--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
2023-03-23 14:55:09 +01:00
@@ -2273,12 +2273,12 @@ public class CraftWorld extends CraftRegionAccessor implements World {
2023-02-23 17:13:45 +01:00
// Spigot start
@Override
public int getViewDistance() {
- return getHandle().getChunkSource().chunkMap.playerChunkManager.getTargetNoTickViewDistance(); // Paper - replace old player chunk management
+ return this.getHandle().playerChunkLoader.getAPIViewDistance(); // Paper - replace player chunk loader
}
@Override
public int getSimulationDistance() {
- return getHandle().getChunkSource().chunkMap.playerChunkManager.getTargetTickViewDistance(); // Paper - replace old player chunk management
+ return this.getHandle().playerChunkLoader.getAPITickDistance(); // Paper - replace player chunk loader
}
// Spigot end
// Paper start - view distance api
2023-03-23 14:55:09 +01:00
@@ -2312,12 +2312,12 @@ public class CraftWorld extends CraftRegionAccessor implements World {
2023-02-23 17:13:45 +01:00
@Override
public int getSendViewDistance() {
- return getHandle().getChunkSource().chunkMap.playerChunkManager.getTargetSendDistance();
+ return this.getHandle().playerChunkLoader.getAPISendViewDistance(); // Paper - replace player chunk loader
}
@Override
public void setSendViewDistance(int viewDistance) {
- getHandle().getChunkSource().chunkMap.playerChunkManager.setSendDistance(viewDistance);
+ this.getHandle().chunkSource.chunkMap.setSendViewDistance(viewDistance); // Paper - replace player chunk loader
}
// Paper end - view distance api
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
2023-03-30 03:26:03 +02:00
index 0ae1fce0c1a2e3bfbbab756a088fc76545e263fa..fec408416aa63e1022802a644bb95131601f0c3b 100644
2023-02-23 17:13:45 +01:00
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
2023-03-23 14:55:09 +01:00
@@ -189,44 +189,22 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
2023-02-23 17:13:45 +01:00
// Paper start - implement view distances
@Override
public int getViewDistance() {
- net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap;
- io.papermc.paper.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle());
- if (data == null) {
- return chunkMap.playerChunkManager.getTargetNoTickViewDistance();
- }
- return data.getTargetNoTickViewDistance();
2023-03-25 00:22:47 +01:00
+ return io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.getAPIViewDistance(this);
2023-02-23 17:13:45 +01:00
}
@Override
public void setViewDistance(int viewDistance) {
- net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap;
- io.papermc.paper.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle());
- if (data == null) {
- throw new IllegalStateException("Player is not attached to world");
- }
-
- data.setTargetNoTickViewDistance(viewDistance);
+ this.getHandle().setLoadViewDistance(viewDistance < 0 ? viewDistance : viewDistance + 1);
}
@Override
public int getSimulationDistance() {
- net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap;
- io.papermc.paper.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle());
- if (data == null) {
- return chunkMap.playerChunkManager.getTargetTickViewDistance();
- }
- return data.getTargetTickViewDistance();
2023-03-25 00:22:47 +01:00
+ return io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.getAPITickViewDistance(this);
2023-02-23 17:13:45 +01:00
}
@Override
public void setSimulationDistance(int simulationDistance) {
- net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap;
- io.papermc.paper.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle());
- if (data == null) {
- throw new IllegalStateException("Player is not attached to world");
- }
-
- data.setTargetTickViewDistance(simulationDistance);
+ this.getHandle().setTickViewDistance(simulationDistance);
}
@Override
2023-03-23 14:55:09 +01:00
@@ -241,23 +219,12 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
2023-02-23 17:13:45 +01:00
@Override
public int getSendViewDistance() {
- net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap;
- io.papermc.paper.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle());
- if (data == null) {
- return chunkMap.playerChunkManager.getTargetSendDistance();
- }
- return data.getTargetSendViewDistance();
2023-03-25 00:22:47 +01:00
+ return io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.getAPISendViewDistance(this);
2023-02-23 17:13:45 +01:00
}
@Override
public void setSendViewDistance(int viewDistance) {
- net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap;
- io.papermc.paper.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle());
- if (data == null) {
- throw new IllegalStateException("Player is not attached to world");
- }
-
- data.setTargetSendViewDistance(viewDistance);
+ this.getHandle().setSendViewDistance(viewDistance);
}
// Paper end - implement view distances