Folia/patches/server/0003-New-player-chunk-loade...

2351 lines
120 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/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadPool.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadPool.java
index 26fa2caa18a9194e57574a4a7fa9f7a4265740e0..5aa1c61a8a82c91514d35df62db7f4bd67aa27a1 100644
--- a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadPool.java
+++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadPool.java
@@ -71,6 +71,10 @@ public final class PrioritisedThreadPool {
return Arrays.copyOf(this.threads, this.threads.length, Thread[].class);
}
+ public int getThreadCount() {
+ return this.threads.length;
+ }
+
public PrioritisedPoolExecutor createExecutor(final String name, final int parallelism) {
synchronized (this.nonShutdownQueues) {
if (this.shutdown) {
diff --git a/src/main/java/co/aikar/timings/TimingsExport.java b/src/main/java/co/aikar/timings/TimingsExport.java
index c07eb451a576811a39021f6f97103c77488fd001..a2f71a6d1a9e98133dff6cd0f625da9435a8af14 100644
--- a/src/main/java/co/aikar/timings/TimingsExport.java
+++ b/src/main/java/co/aikar/timings/TimingsExport.java
@@ -164,9 +164,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..61d03808c8d1ab822d9b2f31fab0de14089a3b15 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.RegionizedPlayerChunkLoader.getAPISendViewDistance(player);
}
public static int getLoadViewDistance(final ServerPlayer player) {
- return io.papermc.paper.chunk.PlayerChunkLoader.getLoadViewDistance(player);
+ return io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.getLoadViewDistance(player);
}
public static int getTickViewDistance(final ServerPlayer player) {
- return io.papermc.paper.chunk.PlayerChunkLoader.getTickViewDistance(player);
+ return io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.getAPITickViewDistance(player);
}
private ChunkSystem() {
diff --git a/src/main/java/io/papermc/paper/chunk/system/RegionizedPlayerChunkLoader.java b/src/main/java/io/papermc/paper/chunk/system/RegionizedPlayerChunkLoader.java
new file mode 100644
index 0000000000000000000000000000000000000000..63c69c4da5fcbd5901c9fc3427f69626e16492ee
--- /dev/null
+++ b/src/main/java/io/papermc/paper/chunk/system/RegionizedPlayerChunkLoader.java
@@ -0,0 +1,1332 @@
+package io.papermc.paper.chunk.system;
+
+import ca.spottedleaf.concurrentutil.collection.SRSWLinkedQueue;
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
+import io.papermc.paper.chunk.system.io.RegionFileIOThread;
+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.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.lang.invoke.VarHandle;
+import java.util.ArrayDeque;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+public class RegionizedPlayerChunkLoader {
+
+ 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 RegionizedPlayerChunkLoader(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");
+ long currTime = System.nanoTime();
+ for (final ServerPlayer player : new java.util.ArrayList<>(this.world.players())) {
+ final PlayerChunkLoaderData loader = player.chunkLoader;
+ if (loader == null || loader.world != this.world) {
+ // not our problem anymore
+ continue;
+ }
+ loader.update(); // can't invoke plugin logic
+ loader.updateQueues(currTime);
+ }
+ }
+
+ 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 static final long MAX_RATE = 10_000L;
+
+ 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 AllocatingRateLimiter chunkSendLimiter = new AllocatingRateLimiter();
+ private final AllocatingRateLimiter chunkLoadTicketLimiter = new AllocatingRateLimiter();
+ private final AllocatingRateLimiter chunkGenerateTicketLimiter = new AllocatingRateLimiter();
+
+ // 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);
+
+ private volatile boolean removed;
+
+ 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 double getMaxChunkLoadRate() {
+ final double configRate = GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkLoadRate;
+
+ return configRate < 0.0 || configRate > (double)MAX_RATE ? (double)MAX_RATE : Math.max(1.0, configRate);
+ }
+
+ private double getMaxChunkGenRate() {
+ final double configRate = GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkGenerateRate;
+
+ return configRate < 0.0 || configRate > (double)MAX_RATE ? (double)MAX_RATE : Math.max(1.0, configRate);
+ }
+
+ private double getMaxChunkSendRate() {
+ final double configRate = GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkSendRate;
+
+ return configRate < 0.0 || configRate > (double)MAX_RATE ? (double)MAX_RATE : Math.max(1.0, configRate);
+ }
+
+ private long getMaxChunkLoads() {
+ final long radiusChunks = (2L * this.lastLoadDistance + 1L) * (2L * this.lastLoadDistance + 1L);
+ long configLimit = GlobalConfiguration.get().chunkLoadingAdvanced.playerMaxConcurrentChunkLoads;
+ if (configLimit == 0L) {
+ // by default, only allow 1/5th of the chunks in the view distance to be concurrently active
+ configLimit = Math.max(5L, radiusChunks / 5L);
+ } else if (configLimit < 0L) {
+ configLimit = Integer.MAX_VALUE;
+ } // else: use the value configured
+ configLimit = configLimit - this.loadingQueue.size();
+
+ return configLimit;
+ }
+
+ private long getMaxChunkGenerates() {
+ final long radiusChunks = (2L * this.lastLoadDistance + 1L) * (2L * this.lastLoadDistance + 1L);
+ long configLimit = GlobalConfiguration.get().chunkLoadingAdvanced.playerMaxConcurrentChunkGenerates;
+ if (configLimit == 0L) {
+ // by default, only allow 1/5th of the chunks in the view distance to be concurrently active
+ configLimit = Math.max(5L, radiusChunks / 5L);
+ } else if (configLimit < 0L) {
+ configLimit = Integer.MAX_VALUE;
+ } // else: use the value configured
+ configLimit = configLimit - this.generatingQueue.size();
+
+ return configLimit;
+ }
+
+ 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 updateQueues(final long time) {
+ TickThread.ensureTickThread(this.player, "Cannot tick player chunk loader async");
+ if (this.removed) {
+ throw new IllegalStateException("Ticking removed player chunk loader");
+ }
+ // update rate limits
+ final double loadRate = this.getMaxChunkLoadRate();
+ final double genRate = this.getMaxChunkGenRate();
+ final double sendRate = this.getMaxChunkSendRate();
+
+ this.chunkLoadTicketLimiter.tickAllocation(time, loadRate, loadRate);
+ this.chunkGenerateTicketLimiter.tickAllocation(time, genRate, genRate);
+ this.chunkSendLimiter.tickAllocation(time, sendRate, sendRate);
+
+ // 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
+ final long maxLoads = Math.max(0L, Math.min(MAX_RATE, Math.min(this.loadQueue.size(), this.getMaxChunkLoads())));
+ final int maxLoadsThisTick = (int)this.chunkLoadTicketLimiter.takeAllocation(time, loadRate, maxLoads);
+ if (maxLoadsThisTick > 0) {
+ final LongArrayList chunks = new LongArrayList(maxLoadsThisTick);
+ for (int i = 0; i < maxLoadsThisTick; ++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);
+ }
+
+ // 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();
+
+ if (this.removed) {
+ // process ticket updates may invoke plugin logic, which may remove this player
+ return;
+ }
+
+ for (int i = 0; i < maxLoadsThisTick; ++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
+ );
+ if (this.removed) {
+ return;
+ }
+ }
+ }
+
+ // 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
+ final long maxGens = Math.max(0L, Math.min(MAX_RATE, Math.min(this.genQueue.size(), this.getMaxChunkGenerates())));
+ final int maxGensThisTick = (int)this.chunkGenerateTicketLimiter.takeAllocation(time, genRate, maxGens);
+ for (int i = 0; i < maxGensThisTick; ++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);
+ }
+
+ // 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 long maxSends = Math.max(0L, Math.min(MAX_RATE, Integer.MAX_VALUE)); // no logic to track concurrent sends
+ final int maxSendsThisTick = Math.min((int)this.chunkSendLimiter.takeAllocation(time, sendRate, maxSends), this.sendQueue.size());
+ // we do not return sends that we took from the allocation back because we want to limit the max send rate, not target it
+ for (int i = 0; i < maxSendsThisTick; ++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) || !TickThread.isTickThreadFor(this.world, pendingSendX, pendingSendZ)) {
+ // nothing to do
+ // the target chunk may not be owned by this region, but this should be resolved in the future
+ break;
+ }
+ this.sendQueue.dequeueLong();
+
+ this.sendChunk(pendingSendX, pendingSendZ);
+ if (this.removed) {
+ // sendChunk may invoke plugin logic
+ return;
+ }
+ }
+
+ this.flushDelayedTicketOps();
+ // we assume propagate ticket levels happens after this call
+ }
+
+ void add() {
+ TickThread.ensureTickThread(this.player, "Cannot add player asynchronously");
+ if (this.removed) {
+ throw new IllegalStateException("Adding removed player chunk loader");
+ }
+ 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() {
+ TickThread.ensureTickThread(this.player, "Cannot update player asynchronously");
+ if (this.removed) {
+ throw new IllegalStateException("Updating removed player chunk loader");
+ }
+ 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() {
+ TickThread.ensureTickThread(this.player, "Cannot add player asynchronously");
+ if (this.removed) {
+ throw new IllegalStateException("Removing removed player chunk loader");
+ }
+ this.removed = true;
+ // 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
+ }
+ }
+
+ // TODO rebase into util patch
+ private static final class AllocatingRateLimiter {
+
+ // max difference granularity in ns
+ private static final long MAX_GRANULARITY = TimeUnit.SECONDS.toNanos(1L);
+
+ private double allocation;
+ private long lastAllocationUpdate;
+ private double takeCarry;
+ private long lastTakeUpdate;
+
+ // rate in units/s, and time in ns
+ public void tickAllocation(final long time, final double rate, final double maxAllocation) {
+ final long diff = Math.min(MAX_GRANULARITY, time - this.lastAllocationUpdate);
+ this.lastAllocationUpdate = time;
+
+ this.allocation = Math.min(maxAllocation - this.takeCarry, this.allocation + rate * (diff*1.0E-9D));
+ }
+
+ // rate in units/s, and time in ns
+ public long takeAllocation(final long time, final double rate, final long maxTake) {
+ if (maxTake < 1L) {
+ return 0L;
+ }
+
+ double ret = this.takeCarry;
+ final long diff = Math.min(MAX_GRANULARITY, time - this.lastTakeUpdate);
+ this.lastTakeUpdate = time;
+
+ // note: abs(takeCarry) <= 1.0
+ final double take = Math.min(Math.min((double)maxTake - this.takeCarry, this.allocation), rate * (diff*1.0E-9));
+
+ ret += take;
+ this.allocation -= take;
+
+ final long retInteger = (long)Math.floor(ret);
+ this.takeCarry = ret - (double)retInteger;
+
+ return retInteger;
+ }
+ }
+
+ 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;
+ }
+ }
+
+ static final class CountedSRSWLinkedQueue<E> {
+
+ private final SRSWLinkedQueue<E> queue = new SRSWLinkedQueue<>();
+ private volatile long countAdded;
+ private volatile long countRemoved;
+
+ private static final VarHandle COUNT_ADDED_HANDLE = ConcurrentUtil.getVarHandle(CountedSRSWLinkedQueue.class, "countAdded", long.class);
+ private static final VarHandle COUNT_REMOVED_HANDLE = ConcurrentUtil.getVarHandle(CountedSRSWLinkedQueue.class, "countRemoved", long.class);
+
+ private long getCountAddedPlain() {
+ return (long)COUNT_ADDED_HANDLE.get(this);
+ }
+
+ private long getCountAddedAcquire() {
+ return (long)COUNT_ADDED_HANDLE.getAcquire(this);
+ }
+
+ private void setCountAddedRelease(final long to) {
+ COUNT_ADDED_HANDLE.setRelease(this, to);
+ }
+
+ private long getCountRemovedPlain() {
+ return (long)COUNT_REMOVED_HANDLE.get(this);
+ }
+
+ private long getCountRemovedAcquire() {
+ return (long)COUNT_REMOVED_HANDLE.getAcquire(this);
+ }
+
+ private void setCountRemovedRelease(final long to) {
+ COUNT_REMOVED_HANDLE.setRelease(this, to);
+ }
+
+ public void add(final E element) {
+ this.setCountAddedRelease(this.getCountAddedPlain() + 1L);
+ this.queue.addLast(element);
+ }
+
+ public E poll() {
+ final E ret = this.queue.poll();
+ if (ret != null) {
+ this.setCountRemovedRelease(this.getCountRemovedPlain() + 1L);
+ }
+
+ return ret;
+ }
+
+ public long size() {
+ final long removed = this.getCountRemovedAcquire();
+ final long added = this.getCountAddedAcquire();
+
+ return added - removed;
+ }
+ }
+}
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 ad3560284ae79b9c6bbc8752be7d9d14b18e226e..cb12c31ffe014e17eb9f901ab0a273802e3e0245 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;
@@ -500,6 +501,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;
@@ -907,6 +923,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;
});
@@ -955,6 +1107,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/chunk/system/scheduling/ChunkTaskScheduler.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java
index 84cc9397237fa0c17aa1012dfb5683c90eb6d3b8..e944bd32df98a1dd7dde8c5ce3698b6d81862cbf 100644
--- a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java
+++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java
@@ -48,6 +48,10 @@ public final class ChunkTaskScheduler {
public static ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadPool workerThreads;
+ public static int getWorkerCount() {
+ return workerThreads.getThreadCount();
+ }
+
private static boolean initialised = false;
public static void init(final GlobalConfiguration.ChunkSystem config) {
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 6efb8b10f17c70b05128039376d254e6beda3841..c856a9a0d085b278da416c59996fc131811f790c 100644
--- a/src/main/java/io/papermc/paper/util/MCUtil.java
+++ b/src/main/java/io/papermc/paper/util/MCUtil.java
@@ -607,8 +607,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 904fcdeb7937d36208cc9a8d5eca9ef3a5b2cd9e..a7013fdd8d82f24cadd90ea7e9ebc7c1501042d3 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 + " in world '" + this.chunkMap.level.getWorld().getName() + "' to player " + player);
+ }
+ }
+
+ public void removePlayer(ServerPlayer player) {
+ if (!this.playersSentChunkTo.remove(player)) {
+ throw new IllegalStateException("Have not sent chunk " + this.pos + " in world '" + this.chunkMap.level.getWorld().getName() + "' 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 fbe209a66c77c47935ad026dd3e45e682af91fd8..78ccfe201f91b9d969766a6aed4ab033a8e1218f 100644
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
@@ -199,7 +199,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
@@ -221,7 +221,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);
@@ -244,7 +244,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) {
@@ -816,7 +816,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) {
@@ -826,20 +830,24 @@ 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
+ io.papermc.paper.util.TickThread.ensureTickThread(this.level, pos, "May not update chunk tracking for chunk async"); // Paper - replace chunk loader system
+ io.papermc.paper.util.TickThread.ensureTickThread(player, "May not update chunk tracking for player async"); // Paper - replace chunk loader system
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);
}
@@ -848,10 +856,17 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
if (!newWithinViewDistance && oldWithinViewDistance) {
+ // Paper start - replace chunk loader system
+ if (playerchunk != null) {
+ playerchunk.removePlayer(player);
+ } else {
+ LOGGER.warn("ChunkHolder at " + pos + " in world '" + this.level.getWorld().getName() + "' does not exist to untrack chunk for " + player, new Throwable());
+ }
+ // Paper end - replace chunk loader system
player.untrackChunk(pos);
}
- }
+ } else { LOGGER.warn("Mismatch in world for chunk " + pos + " in world '" + this.level.getWorld().getName() + "' for player " + player, new Throwable()); } // Paper - replace chunk loader system
}
public int size() {
@@ -1151,34 +1166,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
}
@@ -1612,7 +1611,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..5c7b8041e96ede9e662dfdb5285539bf51304650 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();
+ // 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 45804711255f04110e9509df8d60900314aa10b7..7b7e2edbcb6c8eb45029e6cbf4fc9d2e9052b668 100644
--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
@@ -521,6 +521,48 @@ public class ServerLevel extends Level implements WorldGenLevel {
}
// Paper end - optimise get nearest players for entity AI
+ public final io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader playerChunkLoader = new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader(this);
+ private final java.util.concurrent.atomic.AtomicReference<io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances> viewDistances = new java.util.concurrent.atomic.AtomicReference<>(new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances(-1, -1, -1));
+
+ public io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances getViewDistances() {
+ return this.viewDistances.get();
+ }
+
+ private void updateViewDistance(final java.util.function.Function<io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances, io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances> update) {
+ for (io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances curr = this.viewDistances.get();;) {
+ if (this.viewDistances.compareAndSet(curr, update.apply(curr))) {
+ return;
+ }
+ }
+ }
+
+ public void setTickViewDistance(final int distance) {
+ if ((distance < io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE || distance > io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE)) {
+ throw new IllegalArgumentException("Tick view distance must be a number between " + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE + " and " + (io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE) + ", got: " + distance);
+ }
+ this.updateViewDistance((input) -> {
+ return input.setTickViewDistance(distance);
+ });
+ }
+
+ public void setLoadViewDistance(final int distance) {
+ if (distance != -1 && (distance < io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE || distance > io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1)) {
+ throw new IllegalArgumentException("Load view distance must be a number between " + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE + " and " + (io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1) + " or -1, got: " + distance);
+ }
+ this.updateViewDistance((input) -> {
+ return input.setLoadViewDistance(distance);
+ });
+ }
+
+ public void setSendViewDistance(final int distance) {
+ if (distance != -1 && (distance < io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE || distance > io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1)) {
+ throw new IllegalArgumentException("Send view distance must be a number between " + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE + " and " + (io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1) + " or -1, got: " + distance);
+ }
+ 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) {
// IRegistryCustom.Dimension iregistrycustom_dimension = minecraftserver.registryAccess(); // 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 98df2463bf41fc736aa6a2b6ddf89e5abde6eb39..37dfb5f2e56d8d7bf114dac6be82ccf0fbfc8a09 100644
--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
@@ -278,6 +278,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.RegionizedPlayerChunkLoader.ViewDistances> viewDistances = new java.util.concurrent.atomic.AtomicReference<>(new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances(-1, -1, -1));
+ public io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.PlayerChunkLoaderData chunkLoader;
+
+ public io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances getViewDistances() {
+ return this.viewDistances.get();
+ }
+
+ private void updateViewDistance(final java.util.function.Function<io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances, io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances> update) {
+ for (io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances curr = this.viewDistances.get();;) {
+ if (this.viewDistances.compareAndSet(curr, update.apply(curr))) {
+ return;
+ }
+ }
+ }
+
+ public void setTickViewDistance(final int distance) {
+ if ((distance < io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE || distance > io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE)) {
+ throw new IllegalArgumentException("Tick view distance must be a number between " + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE + " and " + (io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE) + ", got: " + distance);
+ }
+ this.updateViewDistance((input) -> {
+ return input.setTickViewDistance(distance);
+ });
+ }
+
+ public void setLoadViewDistance(final int distance) {
+ if (distance != -1 && (distance < io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE || distance > io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1)) {
+ throw new IllegalArgumentException("Load view distance must be a number between " + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE + " and " + (io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1) + " or -1, got: " + distance);
+ }
+ this.updateViewDistance((input) -> {
+ return input.setLoadViewDistance(distance);
+ });
+ }
+
+ public void setSendViewDistance(final int distance) {
+ if (distance != -1 && (distance < io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE || distance > io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1)) {
+ throw new IllegalArgumentException("Send view distance must be a number between " + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE + " and " + (io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1) + " or -1, got: " + distance);
+ }
+ 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 92e758a286a5db079c32d53cc52c8a422457daef..7679fa8c905e6cee1906814a82f8ab4ab925b0fc 100644
--- a/src/main/java/net/minecraft/server/players/PlayerList.java
+++ b/src/main/java/net/minecraft/server/players/PlayerList.java
@@ -278,7 +278,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())));
@@ -912,8 +912,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(CraftLocation.toBukkit(entityplayer1.position(), worldserver1.getWorld(), 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 973ecd50f9cb6b86c353586e84d15dcb118ccb60..944da18bcc993ab0488a34cbbe9df134c355301a 100644
--- a/src/main/java/net/minecraft/world/level/Level.java
+++ b/src/main/java/net/minecraft/world/level/Level.java
@@ -635,7 +635,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 d190bad5d287766ed4165ed827d9901a9d878687..6143607ad1485c552309ac3df37b3ba9adb7c07c 100644
--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
@@ -177,43 +177,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() {
@@ -899,7 +862,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
@@ -1081,6 +1043,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
}
}
@@ -1100,7 +1063,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 8f0234296397ca2d4a607dcea6093c6c606dc7d2..4f0ca1621e4f1fc16d4a6601c858d74ed4d5a788 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
@@ -2309,12 +2309,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
@@ -2348,12 +2348,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 be64633c8bcee96f2ad5247525cac965b7b031b1..e88d7a6b4835bdac1a247545b49e4161ade148cb 100644
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
@@ -195,44 +195,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.RegionizedPlayerChunkLoader.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.RegionizedPlayerChunkLoader.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
@@ -247,23 +225,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.RegionizedPlayerChunkLoader.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