Folia/patches/server/0002-New-player-chunk-loader-system.patch
Spottedleaf 9b824e6406 Fix player chunk loader behaving poorly when limits are low
Need to use ceil() so that it always allows at least 1 addition
for the 50ms case.
2023-02-24 00:03:00 -08:00

2282 lines
116 KiB
Diff

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
index 06bff37e4c1fddd3be6343049a66787c63fb420c..1be1fe766401221b6adb417175312007d29d347e 100644
--- a/src/main/java/co/aikar/timings/TimingsExport.java
+++ b/src/main/java/co/aikar/timings/TimingsExport.java
@@ -163,9 +163,9 @@ public class TimingsExport extends Thread {
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
index 0e45a340ae534caf676b7f9d0adcbcee5829925e..6df1948b1204a7288ecb7238b6fc2a733f7d25b3 100644
--- 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);
+ return io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.getAPISendViewDistance(player);
}
public static int getLoadViewDistance(final ServerPlayer player) {
- return io.papermc.paper.chunk.PlayerChunkLoader.getLoadViewDistance(player);
+ return io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.getLoadViewDistance(player);
}
public static int getTickViewDistance(final ServerPlayer player) {
- return io.papermc.paper.chunk.PlayerChunkLoader.getTickViewDistance(player);
+ return io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.getAPITickViewDistance(player);
}
private ChunkSystem() {
diff --git a/src/main/java/io/papermc/paper/chunk/system/RegionisedPlayerChunkLoader.java b/src/main/java/io/papermc/paper/chunk/system/RegionisedPlayerChunkLoader.java
new file mode 100644
index 0000000000000000000000000000000000000000..cb170d1039fd9dadfbc27da0b181c00742e72025
--- /dev/null
+++ b/src/main/java/io/papermc/paper/chunk/system/RegionisedPlayerChunkLoader.java
@@ -0,0 +1,1302 @@
+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;
+
+public class RegionisedPlayerChunkLoader {
+
+ 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;
+
+ public RegionisedPlayerChunkLoader(final ServerLevel world) {
+ 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");
+ for (final ServerPlayer player : this.world.players()) {
+ player.chunkLoader.update();
+ }
+ }
+
+ 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
+ final long maxSum = (long)Math.ceil(rate * (1.0E-9 * (double)interval));
+ 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
index d1a59c2af0557a816c094983ec60097fb4de060c..6898c704e60d89d53c8ed114e5e12f73ed63605a 100644
--- a/src/main/java/io/papermc/paper/util/MCUtil.java
+++ b/src/main/java/io/papermc/paper/util/MCUtil.java
@@ -602,8 +602,8 @@ public final class MCUtil {
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
index bc46479fd0622a90fd98ac88f92b2840a22a2d04..0b9cb85c063f913ad9245bafb8587d2f06c0ac6e 100644
--- 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
index 2212f9f48636357265d8e44aba415ea4f09f1fe7..870f4d6fae8c14502b4653f246a2df9e345ccca3 100644
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
@@ -196,7 +196,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
// 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
@@ -218,7 +218,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
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);
@@ -241,7 +241,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
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) {
@@ -813,7 +813,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
// 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) {
@@ -823,20 +827,22 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
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);
}
@@ -845,6 +851,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
if (!newWithinViewDistance && oldWithinViewDistance) {
+ // Paper start - replace chunk loader system
+ if (playerchunk != null) {
+ playerchunk.removePlayer(player);
+ }
+ // Paper end - replace chunk loader system
player.untrackChunk(pos);
}
@@ -1148,34 +1159,18 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
// 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
}
@@ -1599,7 +1594,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
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
index 54c2b7fba83d6f06dba95b1bb5b487a02048d6e6..714637cdd9dcdbffa344b19e77944fb3c7541ff7 100644
--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
@@ -523,6 +523,48 @@ public class ServerLevel extends Level implements WorldGenLevel {
}
// Paper end - optimise get nearest players for entity AI
+ public final io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader playerChunkLoader = new io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader(this);
+ private final java.util.concurrent.atomic.AtomicReference<io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.ViewDistances> viewDistances = new java.util.concurrent.atomic.AtomicReference<>(new io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.ViewDistances(-1, -1, -1));
+
+ public io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.ViewDistances getViewDistances() {
+ return this.viewDistances.get();
+ }
+
+ private void updateViewDistance(final java.util.function.Function<io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.ViewDistances, io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.ViewDistances> update) {
+ for (io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.ViewDistances curr = this.viewDistances.get();;) {
+ if (this.viewDistances.compareAndSet(curr, update.apply(curr))) {
+ return;
+ }
+ }
+ }
+
+ public void setTickViewDistance(final int distance) {
+ if ((distance < io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MIN_VIEW_DISTANCE || distance > io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MAX_VIEW_DISTANCE)) {
+ throw new IllegalArgumentException("Tick view distance must be a number between " + io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MIN_VIEW_DISTANCE + " and " + (io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MAX_VIEW_DISTANCE) + ", got: " + distance);
+ }
+ this.updateViewDistance((input) -> {
+ return input.setTickViewDistance(distance);
+ });
+ }
+
+ public void setLoadViewDistance(final int distance) {
+ if (distance != -1 && (distance < io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MIN_VIEW_DISTANCE || distance > io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1)) {
+ throw new IllegalArgumentException("Load view distance must be a number between " + io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MIN_VIEW_DISTANCE + " and " + (io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1) + " or -1, got: " + distance);
+ }
+ this.updateViewDistance((input) -> {
+ return input.setLoadViewDistance(distance);
+ });
+ }
+
+ public void setSendViewDistance(final int distance) {
+ if (distance != -1 && (distance < io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MIN_VIEW_DISTANCE || distance > io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1)) {
+ throw new IllegalArgumentException("Send view distance must be a number between " + io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MIN_VIEW_DISTANCE + " and " + (io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1) + " or -1, got: " + distance);
+ }
+ 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) {
// Holder holder = worlddimension.type(); // CraftBukkit - decompile error
diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java
index 7d6d3c8556033d289fdadc489e73fba478fce41a..869daafbc236b3ff63f878e5fe28427fde75afe5 100644
--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
@@ -269,6 +269,48 @@ public class ServerPlayer extends Player {
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
+ private final java.util.concurrent.atomic.AtomicReference<io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.ViewDistances> viewDistances = new java.util.concurrent.atomic.AtomicReference<>(new io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.ViewDistances(-1, -1, -1));
+ public io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.PlayerChunkLoaderData chunkLoader;
+
+ public io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.ViewDistances getViewDistances() {
+ return this.viewDistances.get();
+ }
+
+ private void updateViewDistance(final java.util.function.Function<io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.ViewDistances, io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.ViewDistances> update) {
+ for (io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.ViewDistances curr = this.viewDistances.get();;) {
+ if (this.viewDistances.compareAndSet(curr, update.apply(curr))) {
+ return;
+ }
+ }
+ }
+
+ public void setTickViewDistance(final int distance) {
+ if ((distance < io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MIN_VIEW_DISTANCE || distance > io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MAX_VIEW_DISTANCE)) {
+ throw new IllegalArgumentException("Tick view distance must be a number between " + io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MIN_VIEW_DISTANCE + " and " + (io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MAX_VIEW_DISTANCE) + ", got: " + distance);
+ }
+ this.updateViewDistance((input) -> {
+ return input.setTickViewDistance(distance);
+ });
+ }
+
+ public void setLoadViewDistance(final int distance) {
+ if (distance != -1 && (distance < io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MIN_VIEW_DISTANCE || distance > io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1)) {
+ throw new IllegalArgumentException("Load view distance must be a number between " + io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MIN_VIEW_DISTANCE + " and " + (io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1) + " or -1, got: " + distance);
+ }
+ this.updateViewDistance((input) -> {
+ return input.setLoadViewDistance(distance);
+ });
+ }
+
+ public void setSendViewDistance(final int distance) {
+ if (distance != -1 && (distance < io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MIN_VIEW_DISTANCE || distance > io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1)) {
+ throw new IllegalArgumentException("Send view distance must be a number between " + io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MIN_VIEW_DISTANCE + " and " + (io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1) + " or -1, got: " + distance);
+ }
+ 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
index 4b754f6eae683248d7fe11d6d6cb168d5dd696a2..3c9d08c37a44a60bc70387d8d0dbd0a39ea98a26 100644
--- a/src/main/java/net/minecraft/server/players/PlayerList.java
+++ b/src/main/java/net/minecraft/server/players/PlayerList.java
@@ -270,7 +270,7 @@ public abstract class PlayerList {
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())));
@@ -898,8 +898,8 @@ public abstract class PlayerList {
// 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
index 3cbf801b2e5420c0e870f73788deb550e49ad54d..60003ff929f7ac6b34f9230c53ccbd54dc9e176b 100644
--- a/src/main/java/net/minecraft/world/level/Level.java
+++ b/src/main/java/net/minecraft/world/level/Level.java
@@ -627,7 +627,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
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
index 28e4b302284f955a73e75d0f4276d55fb51826f5..e776eb8afef978938da084f9ae29d611181b43fe 100644
--- 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
@@ -1074,6 +1036,7 @@ public class LevelChunk extends ChunkAccess {
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
}
}
@@ -1093,7 +1056,6 @@ public class LevelChunk extends ChunkAccess {
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
index 4cb0307935aa63d44aac55c80ee50be074d7913c..d33476ffa49d7f6388bb227f8a57cf115a74698f 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
@@ -2257,12 +2257,12 @@ public class CraftWorld extends CraftRegionAccessor implements World {
// 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
@@ -2296,12 +2296,12 @@ public class CraftWorld extends CraftRegionAccessor implements World {
@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
index 7c43de6ad6bd7259c6bcb2a55e312e8abfcf546b..0351eb67bac6ce257f820af60aa3bba9f45da687 100644
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
@@ -188,44 +188,22 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
// 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();
+ return io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.getAPIViewDistance(this);
}
@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();
+ return io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.getAPITickViewDistance(this);
}
@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
@@ -240,23 +218,12 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
@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();
+ return io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.getAPISendViewDistance(this);
}
@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