From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Spottedleaf Date: Wed, 1 Feb 2023 21:06:31 -0800 Subject: [PATCH] New player chunk loader system diff --git a/src/main/java/co/aikar/timings/TimingsExport.java b/src/main/java/co/aikar/timings/TimingsExport.java index 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..99dc2038aa4fb0f70dc3670e451018ff541d3a85 --- /dev/null +++ b/src/main/java/io/papermc/paper/chunk/system/RegionizedPlayerChunkLoader.java @@ -0,0 +1,1304 @@ +package io.papermc.paper.chunk.system; + +import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; +import io.papermc.paper.chunk.system.scheduling.ChunkHolderManager; +import io.papermc.paper.configuration.GlobalConfiguration; +import io.papermc.paper.util.CoordinateUtils; +import io.papermc.paper.util.IntegerUtil; +import io.papermc.paper.util.IntervalledCounter; +import io.papermc.paper.util.TickThread; +import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue; +import it.unimi.dsi.fastutil.longs.LongArrayList; +import it.unimi.dsi.fastutil.longs.LongComparator; +import it.unimi.dsi.fastutil.longs.LongHeapPriorityQueue; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ClientboundSetChunkCacheCenterPacket; +import net.minecraft.network.protocol.game.ClientboundSetChunkCacheRadiusPacket; +import net.minecraft.network.protocol.game.ClientboundSetSimulationDistancePacket; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.TicketType; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.GameRules; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkStatus; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.levelgen.BelowZeroRetrogen; +import org.apache.commons.lang3.mutable.MutableObject; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.bukkit.entity.Player; + +import java.util.ArrayDeque; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +public class RegionizedPlayerChunkLoader { + + public static final TicketType 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 : this.world.players()) { + player.chunkLoader.update(); + player.chunkLoader.midTickUpdate(currTime); + } + } + + public void tickMidTick() { + final long time = System.nanoTime(); + for (final ServerPlayer player : this.world.players()) { + player.chunkLoader.midTickUpdate(time); + } + } + + private static long[] generateBFSOrder(final int radius) { + final LongArrayList chunks = new LongArrayList(); + final LongArrayFIFOQueue queue = new LongArrayFIFOQueue(); + final LongOpenHashSet seen = new LongOpenHashSet(); + + seen.add(CoordinateUtils.getChunkKey(0, 0)); + queue.enqueue(CoordinateUtils.getChunkKey(0, 0)); + while (!queue.isEmpty()) { + final long chunk = queue.dequeueLong(); + final int chunkX = CoordinateUtils.getChunkX(chunk); + final int chunkZ = CoordinateUtils.getChunkZ(chunk); + + // important that the addition to the list is here, rather than when enqueueing neighbours + // ensures the order is actually kept + chunks.add(chunk); + + // -x + final long n1 = CoordinateUtils.getChunkKey(chunkX - 1, chunkZ); + // -z + final long n2 = CoordinateUtils.getChunkKey(chunkX, chunkZ - 1); + // +x + final long n3 = CoordinateUtils.getChunkKey(chunkX + 1, chunkZ); + // +z + final long n4 = CoordinateUtils.getChunkKey(chunkX, chunkZ + 1); + + final long[] list = new long[] {n1, n2, n3, n4}; + + for (final long neighbour : list) { + final int neighbourX = CoordinateUtils.getChunkX(neighbour); + final int neighbourZ = CoordinateUtils.getChunkZ(neighbour); + if (Math.max(Math.abs(neighbourX), Math.abs(neighbourZ)) > radius) { + // don't enqueue out of range + continue; + } + if (!seen.add(neighbour)) { + continue; + } + queue.enqueue(neighbour); + } + } + + return chunks.toLongArray(); + } + + public static final class PlayerChunkLoaderData { + + private static final AtomicLong ID_GENERATOR = new AtomicLong(); + private final long id = ID_GENERATOR.incrementAndGet(); + private final Long idBoxed = Long.valueOf(this.id); + + // expected that this list returns for a given radius, the set of chunks ordered + // by manhattan distance + private static final long[][] SEARCH_RADIUS_ITERATION_LIST = new long[65][]; + static { + for (int i = 0; i < SEARCH_RADIUS_ITERATION_LIST.length; ++i) { + // a BFS around -x, -z, +x, +z will give increasing manhatten distance + SEARCH_RADIUS_ITERATION_LIST[i] = generateBFSOrder(i); + } + } + + private final ServerPlayer player; + private final ServerLevel world; + + private int lastChunkX = Integer.MIN_VALUE; + private int lastChunkZ = Integer.MIN_VALUE; + + private int lastSendDistance = Integer.MIN_VALUE; + private int lastLoadDistance = Integer.MIN_VALUE; + private int lastTickDistance = Integer.MIN_VALUE; + + private int lastSentChunkCenterX = Integer.MIN_VALUE; + private int lastSentChunkCenterZ = Integer.MIN_VALUE; + + private int lastSentChunkRadius = Integer.MIN_VALUE; + private int lastSentSimulationDistance = Integer.MIN_VALUE; + + private boolean canGenerateChunks = true; + + private final ArrayDeque> delayedTicketOps = new ArrayDeque<>(); + private final LongOpenHashSet sentChunks = new LongOpenHashSet(); + + private static final byte CHUNK_TICKET_STAGE_NONE = 0; + private static final byte CHUNK_TICKET_STAGE_LOADING = 1; + private static final byte CHUNK_TICKET_STAGE_LOADED = 2; + private static final byte CHUNK_TICKET_STAGE_GENERATING = 3; + private static final byte CHUNK_TICKET_STAGE_GENERATED = 4; + private static final byte CHUNK_TICKET_STAGE_TICK = 5; + private static final int[] TICKET_STAGE_TO_LEVEL = new int[] { + ChunkHolderManager.MAX_TICKET_LEVEL + 1, + LOADED_TICKET_LEVEL, + LOADED_TICKET_LEVEL, + GENERATED_TICKET_LEVEL, + GENERATED_TICKET_LEVEL, + TICK_TICKET_LEVEL + }; + private final Long2ByteOpenHashMap chunkTicketStage = new Long2ByteOpenHashMap(); + { + this.chunkTicketStage.defaultReturnValue(CHUNK_TICKET_STAGE_NONE); + } + + // rate limiting + private final MultiIntervalledCounter chunkSendCounter = new MultiIntervalledCounter( + TimeUnit.MILLISECONDS.toNanos(50L), TimeUnit.MILLISECONDS.toNanos(250L), TimeUnit.SECONDS.toNanos(1L) + ); + private final MultiIntervalledCounter chunkLoadTicketCounter = new MultiIntervalledCounter( + TimeUnit.MILLISECONDS.toNanos(50L), TimeUnit.MILLISECONDS.toNanos(250L), TimeUnit.SECONDS.toNanos(1L) + ); + private final MultiIntervalledCounter chunkGenerateTicketCounter = new MultiIntervalledCounter( + TimeUnit.MILLISECONDS.toNanos(50L), TimeUnit.MILLISECONDS.toNanos(250L), TimeUnit.SECONDS.toNanos(1L) + ); + + // queues + private final LongComparator CLOSEST_MANHATTAN_DIST = (final long c1, final long c2) -> { + final int c1x = CoordinateUtils.getChunkX(c1); + final int c1z = CoordinateUtils.getChunkZ(c1); + + final int c2x = CoordinateUtils.getChunkX(c2); + final int c2z = CoordinateUtils.getChunkZ(c2); + + final int centerX = PlayerChunkLoaderData.this.lastChunkX; + final int centerZ = PlayerChunkLoaderData.this.lastChunkZ; + + return Integer.compare( + Math.abs(c1x - centerX) + Math.abs(c1z - centerZ), + Math.abs(c2x - centerX) + Math.abs(c2z - centerZ) + ); + }; + private final LongHeapPriorityQueue sendQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); + private final LongHeapPriorityQueue tickingQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); + private final LongHeapPriorityQueue generatingQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); + private final LongHeapPriorityQueue genQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); + private final LongHeapPriorityQueue loadingQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); + private final LongHeapPriorityQueue loadQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); + + public PlayerChunkLoaderData(final ServerLevel world, final ServerPlayer player) { + this.world = world; + this.player = player; + } + + private void flushDelayedTicketOps() { + if (this.delayedTicketOps.isEmpty()) { + return; + } + this.world.chunkTaskScheduler.chunkHolderManager.pushDelayedTicketUpdates(this.delayedTicketOps); + this.delayedTicketOps.clear(); + this.world.chunkTaskScheduler.chunkHolderManager.tryDrainTicketUpdates(); + } + + private void pushDelayedTicketOp(final ChunkHolderManager.TicketOperation op) { + this.delayedTicketOps.addLast(op); + } + + private void sendChunk(final int chunkX, final int chunkZ) { + if (this.sentChunks.add(CoordinateUtils.getChunkKey(chunkX, chunkZ))) { + this.world.getChunkSource().chunkMap.updateChunkTracking(this.player, + new ChunkPos(chunkX, chunkZ), new MutableObject<>(), false, true); // unloaded, loaded + return; + } + throw new IllegalStateException(); + } + + private void sendUnloadChunk(final int chunkX, final int chunkZ) { + if (!this.sentChunks.remove(CoordinateUtils.getChunkKey(chunkX, chunkZ))) { + return; + } + this.sendUnloadChunkRaw(chunkX, chunkZ); + } + + private void sendUnloadChunkRaw(final int chunkX, final int chunkZ) { + this.player.getLevel().getChunkSource().chunkMap.updateChunkTracking(this.player, + new ChunkPos(chunkX, chunkZ), null, true, false); // unloaded, loaded + } + + private final SingleUserAreaMap 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 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 tickMap = new SingleUserAreaMap<>(this) { + @Override + protected void addCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { + // do nothing, we will detect ticking chunks when we try to load them + } + + @Override + protected void removeCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { + final long chunk = CoordinateUtils.getChunkKey(chunkX, chunkZ); + // note: by the time this is called, the tick cleanup should have ran - so, if the chunk is at + // the tick stage it was deemed in range for loading. Thus, we need to move it to generated + if (!parameter.chunkTicketStage.replace(chunk, CHUNK_TICKET_STAGE_TICK, CHUNK_TICKET_STAGE_GENERATED)) { + return; + } + + // Since we are possibly downgrading the ticket level, we add an unknown ticket so that + // the level is kept until tick(). + parameter.pushDelayedTicketOp(ChunkHolderManager.TicketOperation.addAndRemove( + chunk, + TicketType.UNKNOWN, TICK_TICKET_LEVEL, new ChunkPos(chunkX, chunkZ), + REGION_PLAYER_TICKET, TICK_TICKET_LEVEL, parameter.idBoxed + )); + // keep chunk at new generated level + parameter.pushDelayedTicketOp(ChunkHolderManager.TicketOperation.addOp( + chunk, + REGION_PLAYER_TICKET, GENERATED_TICKET_LEVEL, parameter.idBoxed + )); + } + }; + + private static boolean wantChunkLoaded(final int centerX, final int centerZ, final int chunkX, final int chunkZ, + final int sendRadius) { + // expect sendRadius to be = 1 + target viewable radius + return ChunkMap.isChunkInRange(chunkX, chunkZ, centerX, centerZ, sendRadius); + } + + private static int getClientViewDistance(final ServerPlayer player) { + final Integer vd = player.clientViewDistance; + return vd == null ? -1 : Math.max(0, vd.intValue()); + } + + private static int getTickDistance(final int playerTickViewDistance, final int worldTickViewDistance) { + return playerTickViewDistance < 0 ? worldTickViewDistance : playerTickViewDistance; + } + + private static int getLoadViewDistance(final int tickViewDistance, final int playerLoadViewDistance, + final int worldLoadViewDistance) { + return Math.max(tickViewDistance + 1, playerLoadViewDistance < 0 ? worldLoadViewDistance : playerLoadViewDistance); + } + + private static int getSendViewDistance(final int loadViewDistance, final int clientViewDistance, + final int playerSendViewDistance, final int worldSendViewDistance) { + return Math.min( + loadViewDistance, + playerSendViewDistance < 0 ? (!GlobalConfiguration.get().chunkLoadingAdvanced.autoConfigSendDistance || clientViewDistance < 0 ? (worldSendViewDistance < 0 ? loadViewDistance : worldSendViewDistance) : clientViewDistance + 1) : playerSendViewDistance + ); + } + + private Packet updateClientChunkRadius(final int radius) { + this.lastSentChunkRadius = radius; + return new ClientboundSetChunkCacheRadiusPacket(radius); + } + + private Packet updateClientSimulationDistance(final int distance) { + this.lastSentSimulationDistance = distance; + return new ClientboundSetSimulationDistancePacket(distance); + } + + private Packet updateClientChunkCenter(final int chunkX, final int chunkZ) { + this.lastSentChunkCenterX = chunkX; + this.lastSentChunkCenterZ = chunkZ; + return new ClientboundSetChunkCacheCenterPacket(chunkX, chunkZ); + } + + private boolean canPlayerGenerateChunks() { + return !this.player.isSpectator() || this.world.getGameRules().getBoolean(GameRules.RULE_SPECTATORSGENERATECHUNKS); + } + + private int getMaxChunkLoads() { + final int radiusChunks = (2 * this.lastLoadDistance + 1) * (2 * this.lastLoadDistance + 1); + int configLimit = GlobalConfiguration.get().chunkLoadingAdvanced.playerMaxConcurrentChunkLoads; + if (configLimit == 0) { + // by default, only allow 1/10th of the chunks in the view distance to be concurrently active + configLimit = Math.max(5, radiusChunks / 10); + } else if (configLimit < 0) { + configLimit = Integer.MAX_VALUE; + } // else: use the value configured + configLimit = configLimit - this.loadingQueue.size(); + + int rateLimit; + double configRate = GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkLoadRate; + if (configRate < 0.0 || configRate > (1000.0 * (double)radiusChunks)) { + // getMaxCountBeforeViolation may not work with large config rates, so by checking against the load count we ensure + // there are no issues with the cast to integer + rateLimit = Integer.MAX_VALUE; + } else { + rateLimit = (int)this.chunkLoadTicketCounter.getMaxCountBeforeViolation(configRate); + } + + return Math.min(configLimit, rateLimit); + } + + private int getMaxChunkGenerates() { + final int radiusChunks = (2 * this.lastLoadDistance + 1) * (2 * this.lastLoadDistance + 1); + int configLimit = GlobalConfiguration.get().chunkLoadingAdvanced.playerMaxConcurrentChunkGenerates; + if (configLimit == 0) { + // by default, only allow 1/10th of the chunks in the view distance to be concurrently active + configLimit = Math.max(5, radiusChunks / 10); + } else if (configLimit < 0) { + configLimit = Integer.MAX_VALUE; + } // else: use the value configured + configLimit = configLimit - this.generatingQueue.size(); + + int rateLimit; + double configRate = GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkGenerateRate; + if (configRate < 0.0 || configRate > (1000.0 * (double)radiusChunks)) { + // getMaxCountBeforeViolation may not work with large config rates, so by checking against the load count we ensure + // there are no issues with the cast to integer + rateLimit = Integer.MAX_VALUE; + } else { + rateLimit = (int)this.chunkGenerateTicketCounter.getMaxCountBeforeViolation(configRate); + } + + return Math.min(configLimit, rateLimit); + } + + private int getMaxChunkSends() { + final int radiusChunks = (2 * this.lastSendDistance + 1) * (2 * this.lastSendDistance + 1); + + int rateLimit; + double configRate = GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkSendRate; + if (configRate < 0.0 || configRate > (1000.0 * (double)radiusChunks)) { + // getMaxCountBeforeViolation may not work with large config rates, so by checking against the load count we ensure + // there are no issues with the cast to integer + rateLimit = Integer.MAX_VALUE; + } else { + rateLimit = (int)this.chunkSendCounter.getMaxCountBeforeViolation(configRate); + } + + return rateLimit; + } + + private boolean wantChunkSent(final int chunkX, final int chunkZ) { + final int dx = this.lastChunkX - chunkX; + final int dz = this.lastChunkZ - chunkZ; + return Math.max(Math.abs(dx), Math.abs(dz)) <= this.lastSendDistance && wantChunkLoaded( + this.lastChunkX, this.lastChunkZ, chunkX, chunkZ, this.lastSendDistance + ); + } + + private boolean wantChunkTicked(final int chunkX, final int chunkZ) { + final int dx = this.lastChunkX - chunkX; + final int dz = this.lastChunkZ - chunkZ; + return Math.max(Math.abs(dx), Math.abs(dz)) <= this.lastTickDistance; + } + + void midTickUpdate(final long time) { + TickThread.ensureTickThread(this.player, "Cannot tick player chunk loader async"); + // update rate limits + this.chunkSendCounter.update(time); + this.chunkGenerateTicketCounter.update(time); + this.chunkLoadTicketCounter.update(time); + + // try to progress chunk loads + while (!this.loadingQueue.isEmpty()) { + final long pendingLoadChunk = this.loadingQueue.firstLong(); + final int pendingChunkX = CoordinateUtils.getChunkX(pendingLoadChunk); + final int pendingChunkZ = CoordinateUtils.getChunkZ(pendingLoadChunk); + final ChunkAccess pending = this.world.chunkSource.getChunkAtImmediately(pendingChunkX, pendingChunkZ); + if (pending == null) { + // nothing to do here + break; + } + // chunk has loaded, so we can take it out of the queue + this.loadingQueue.dequeueLong(); + + // try to move to generate queue + final byte prev = this.chunkTicketStage.put(pendingLoadChunk, CHUNK_TICKET_STAGE_LOADED); + if (prev != CHUNK_TICKET_STAGE_LOADING) { + throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_LOADING + ", not " + prev); + } + + if (this.canGenerateChunks || this.isLoadedChunkGeneratable(pending)) { + this.genQueue.enqueue(pendingLoadChunk); + } // else: don't want to generate, so just leave it loaded + } + + // try to push more chunk loads + int loadSlots; + while ((loadSlots = Math.min(this.getMaxChunkLoads(), this.loadQueue.size())) > 0) { + final LongArrayList chunks = new LongArrayList(loadSlots); + int actualLoadsQueued = 0; + for (int i = 0; i < loadSlots; ++i) { + final long chunk = this.loadQueue.dequeueLong(); + final byte prev = this.chunkTicketStage.put(chunk, CHUNK_TICKET_STAGE_LOADING); + if (prev != CHUNK_TICKET_STAGE_NONE) { + throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_NONE + ", not " + prev); + } + this.pushDelayedTicketOp( + ChunkHolderManager.TicketOperation.addOp( + chunk, + REGION_PLAYER_TICKET, LOADED_TICKET_LEVEL, this.idBoxed + ) + ); + chunks.add(chunk); + this.loadingQueue.enqueue(chunk); + + if (this.world.chunkSource.getChunkAtImmediately(CoordinateUtils.getChunkX(chunk), CoordinateUtils.getChunkZ(chunk)) == null) { + // this is a good enough approximation for counting, but NOT for actual state management + ++actualLoadsQueued; + } + } + if (actualLoadsQueued > 0) { + this.chunkLoadTicketCounter.addTime(time, (long)actualLoadsQueued); + } + + // here we need to flush tickets, as scheduleChunkLoad requires tickets to be propagated with addTicket = false + this.flushDelayedTicketOps(); + // we only need to call scheduleChunkLoad because the loaded ticket level is not enough to start the chunk + // load - only generate ticket levels start anything, but they start generation... + // propagate levels + // Note: this CAN call plugin logic, so it is VITAL that our bookkeeping logic is completely done by the time this is invoked + this.world.chunkTaskScheduler.chunkHolderManager.processTicketUpdates(); + + for (int i = 0; i < loadSlots; ++i) { + final long queuedLoadChunk = chunks.getLong(i); + final int queuedChunkX = CoordinateUtils.getChunkX(queuedLoadChunk); + final int queuedChunkZ = CoordinateUtils.getChunkZ(queuedLoadChunk); + this.world.chunkTaskScheduler.scheduleChunkLoad( + queuedChunkX, queuedChunkZ, ChunkStatus.EMPTY, false, PrioritisedExecutor.Priority.NORMAL, null + ); + } + } + + // try to progress chunk generations + while (!this.generatingQueue.isEmpty()) { + final long pendingGenChunk = this.generatingQueue.firstLong(); + final int pendingChunkX = CoordinateUtils.getChunkX(pendingGenChunk); + final int pendingChunkZ = CoordinateUtils.getChunkZ(pendingGenChunk); + final LevelChunk pending = this.world.chunkSource.getChunkAtIfLoadedMainThreadNoCache(pendingChunkX, pendingChunkZ); + if (pending == null) { + // nothing to do here + break; + } + + // chunk has generated, so we can take it out of queue + this.generatingQueue.dequeueLong(); + + final byte prev = this.chunkTicketStage.put(pendingGenChunk, CHUNK_TICKET_STAGE_GENERATED); + if (prev != CHUNK_TICKET_STAGE_GENERATING) { + throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_GENERATING + ", not " + prev); + } + + // try to move to send queue + if (this.wantChunkSent(pendingChunkX, pendingChunkZ)) { + this.sendQueue.enqueue(pendingGenChunk); + } + // try to move to tick queue + if (this.wantChunkTicked(pendingChunkX, pendingChunkZ)) { + this.tickingQueue.enqueue(pendingGenChunk); + } + } + + // try to push more chunk generations + int genSlots; + while ((genSlots = Math.min(this.getMaxChunkGenerates(), this.genQueue.size())) > 0) { + int actualGenerationsQueued = 0; + for (int i = 0; i < genSlots; ++i) { + final long chunk = this.genQueue.dequeueLong(); + final byte prev = this.chunkTicketStage.put(chunk, CHUNK_TICKET_STAGE_GENERATING); + if (prev != CHUNK_TICKET_STAGE_LOADED) { + throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_LOADED + ", not " + prev); + } + this.pushDelayedTicketOp( + ChunkHolderManager.TicketOperation.addAndRemove( + chunk, + REGION_PLAYER_TICKET, GENERATED_TICKET_LEVEL, this.idBoxed, + REGION_PLAYER_TICKET, LOADED_TICKET_LEVEL, this.idBoxed + ) + ); + this.generatingQueue.enqueue(chunk); + final ChunkAccess existingChunk = this.world.chunkSource.getChunkAtImmediately(CoordinateUtils.getChunkX(chunk), CoordinateUtils.getChunkZ(chunk)); + if (existingChunk == null || !existingChunk.getStatus().isOrAfter(ChunkStatus.FULL)) { + // this is a good enough approximation for counting, but NOT for actual state management + ++actualGenerationsQueued; + } + } + if (actualGenerationsQueued > 0) { + this.chunkGenerateTicketCounter.addTime(time, (long)actualGenerationsQueued); + } + } + + // try to pull ticking chunks + tick_check_outer: + while (!this.tickingQueue.isEmpty()) { + final long pendingTicking = this.tickingQueue.firstLong(); + final int pendingChunkX = CoordinateUtils.getChunkX(pendingTicking); + final int pendingChunkZ = CoordinateUtils.getChunkZ(pendingTicking); + + final int tickingReq = 2; + for (int dz = -tickingReq; dz <= tickingReq; ++dz) { + for (int dx = -tickingReq; dx <= tickingReq; ++dx) { + if ((dx | dz) == 0) { + continue; + } + final long neighbour = CoordinateUtils.getChunkKey(dx + pendingChunkX, dz + pendingChunkZ); + final byte stage = this.chunkTicketStage.get(neighbour); + if (stage != CHUNK_TICKET_STAGE_GENERATED && stage != CHUNK_TICKET_STAGE_TICK) { + break tick_check_outer; + } + } + } + // only gets here if all neighbours were marked as generated or ticking themselves + this.tickingQueue.dequeueLong(); + this.pushDelayedTicketOp( + ChunkHolderManager.TicketOperation.addAndRemove( + pendingTicking, + REGION_PLAYER_TICKET, TICK_TICKET_LEVEL, this.idBoxed, + REGION_PLAYER_TICKET, GENERATED_TICKET_LEVEL, this.idBoxed + ) + ); + // there is no queue to add after ticking + final byte prev = this.chunkTicketStage.put(pendingTicking, CHUNK_TICKET_STAGE_TICK); + if (prev != CHUNK_TICKET_STAGE_GENERATED) { + throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_GENERATED + ", not " + prev); + } + } + + // try to pull sending chunks + final int maxSends = this.getMaxChunkSends(); + final int sendSlots = Math.min(maxSends, this.sendQueue.size()); + for (int i = 0; i < sendSlots; ++i) { + final long pendingSend = this.sendQueue.firstLong(); + final int pendingSendX = CoordinateUtils.getChunkX(pendingSend); + final int pendingSendZ = CoordinateUtils.getChunkZ(pendingSend); + final LevelChunk chunk = this.world.chunkSource.getChunkAtIfLoadedMainThreadNoCache(pendingSendX, pendingSendZ); + if (!chunk.areNeighboursLoaded(1)) { + // nothing to do + break; + } + this.sendQueue.dequeueLong(); + + this.sendChunk(pendingSendX, pendingSendZ); + } + if (sendSlots > 0) { + this.chunkSendCounter.addTime(time, sendSlots); + } + + this.flushDelayedTicketOps(); + // we assume propagate ticket levels happens after this call + } + + void add() { + final ViewDistances playerDistances = this.player.getViewDistances(); + final ViewDistances worldDistances = this.world.getViewDistances(); + final int chunkX = this.player.chunkPosition().x; + final int chunkZ = this.player.chunkPosition().z; + + final int tickViewDistance = getTickDistance(playerDistances.tickViewDistance, worldDistances.tickViewDistance); + // load view cannot be less-than tick view + 1 + final int loadViewDistance = getLoadViewDistance(tickViewDistance, playerDistances.loadViewDistance, worldDistances.loadViewDistance); + // send view cannot be greater-than load view + final int clientViewDistance = getClientViewDistance(this.player); + final int sendViewDistance = getSendViewDistance(loadViewDistance, clientViewDistance, playerDistances.sendViewDistance, worldDistances.sendViewDistance); + + // send view distances + this.player.connection.send(this.updateClientChunkRadius(sendViewDistance)); + this.player.connection.send(this.updateClientSimulationDistance(tickViewDistance)); + + // add to distance maps + this.broadcastMap.add(chunkX, chunkZ, sendViewDistance); + this.loadTicketCleanup.add(chunkX, chunkZ, loadViewDistance + 1); + this.tickMap.add(chunkX, chunkZ, tickViewDistance); + + // update chunk center + this.player.connection.send(this.updateClientChunkCenter(chunkX, chunkZ)); + + // now we can update + this.update(); + } + + private boolean isLoadedChunkGeneratable(final int chunkX, final int chunkZ) { + return this.isLoadedChunkGeneratable(this.world.chunkSource.getChunkAtImmediately(chunkX, chunkZ)); + } + + private boolean isLoadedChunkGeneratable(final ChunkAccess chunkAccess) { + final BelowZeroRetrogen belowZeroRetrogen; + return chunkAccess != null && ( + chunkAccess.getStatus() == ChunkStatus.FULL || + ((belowZeroRetrogen = chunkAccess.getBelowZeroRetrogen()) != null && belowZeroRetrogen.targetStatus().isOrAfter(ChunkStatus.FULL)) + ); + } + + void update() { + final ViewDistances playerDistances = this.player.getViewDistances(); + final ViewDistances worldDistances = this.world.getViewDistances(); + + final int tickViewDistance = getTickDistance(playerDistances.tickViewDistance, worldDistances.tickViewDistance); + // load view cannot be less-than tick view + 1 + final int loadViewDistance = getLoadViewDistance(tickViewDistance, playerDistances.loadViewDistance, worldDistances.loadViewDistance); + // send view cannot be greater-than load view + final int clientViewDistance = getClientViewDistance(this.player); + final int sendViewDistance = getSendViewDistance(loadViewDistance, clientViewDistance, playerDistances.sendViewDistance, worldDistances.sendViewDistance); + + final ChunkPos playerPos = this.player.chunkPosition(); + final boolean canGenerateChunks = this.canPlayerGenerateChunks(); + final int currentChunkX = playerPos.x; + final int currentChunkZ = playerPos.z; + + final int prevChunkX = this.lastChunkX; + final int prevChunkZ = this.lastChunkZ; + + if ( + // has view distance stayed the same? + sendViewDistance == this.lastSendDistance + && loadViewDistance == this.lastLoadDistance + && tickViewDistance == this.lastTickDistance + + // has our chunk stayed the same? + && prevChunkX == currentChunkX + && prevChunkZ == currentChunkZ + + // can we still generate chunks? + && this.canGenerateChunks == canGenerateChunks + ) { + // nothing we care about changed, so we're not re-calculating + return; + } + + // update distance maps + this.broadcastMap.update(currentChunkX, currentChunkZ, sendViewDistance); + this.loadTicketCleanup.update(currentChunkX, currentChunkZ, loadViewDistance + 1); + this.tickMap.update(currentChunkX, currentChunkZ, tickViewDistance); + if (sendViewDistance > loadViewDistance || tickViewDistance > (loadViewDistance - 1)) { + throw new IllegalStateException(); + } + + // update VDs for client + // this should be after the distance map updates, as they will send unload packets + if (this.lastSentChunkRadius != sendViewDistance) { + this.player.connection.send(this.updateClientChunkRadius(sendViewDistance)); + } + if (this.lastSentSimulationDistance != tickViewDistance) { + this.player.connection.send(this.updateClientSimulationDistance(tickViewDistance)); + } + + this.sendQueue.clear(); + this.tickingQueue.clear(); + this.generatingQueue.clear(); + this.genQueue.clear(); + this.loadingQueue.clear(); + this.loadQueue.clear(); + + this.lastChunkX = currentChunkX; + this.lastChunkZ = currentChunkZ; + this.lastSendDistance = sendViewDistance; + this.lastLoadDistance = loadViewDistance; + this.lastTickDistance = tickViewDistance; + this.canGenerateChunks = canGenerateChunks; + + // +1 since we need to load chunks +1 around the load view distance... + final long[] toIterate = SEARCH_RADIUS_ITERATION_LIST[loadViewDistance + 1]; + // the iteration order is by increasing manhattan distance - so, we do NOT need to + // sort anything in the queue! + for (final long deltaChunk : toIterate) { + final int dx = CoordinateUtils.getChunkX(deltaChunk); + final int dz = CoordinateUtils.getChunkZ(deltaChunk); + final int chunkX = dx + currentChunkX; + final int chunkZ = dz + currentChunkZ; + final long chunk = CoordinateUtils.getChunkKey(chunkX, chunkZ); + final int squareDistance = Math.max(Math.abs(dx), Math.abs(dz)); + final int manhattanDistance = Math.abs(dx) + Math.abs(dz); + + // since chunk sending is not by radius alone, we need an extra check here to account for + // everything <= sendDistance + // Note: Vanilla may want to send chunks outside the send view distance, so we do need + // the dist <= view check + final boolean sendChunk = squareDistance <= sendViewDistance + && wantChunkLoaded(currentChunkX, currentChunkZ, chunkX, chunkZ, sendViewDistance); + final boolean sentChunk = sendChunk ? this.sentChunks.contains(chunk) : this.sentChunks.remove(chunk); + + if (!sendChunk && sentChunk) { + // have sent the chunk, but don't want it anymore + // unload it now + this.sendUnloadChunkRaw(chunkX, chunkZ); + } + + final byte stage = this.chunkTicketStage.get(chunk); + switch (stage) { + case CHUNK_TICKET_STAGE_NONE: { + // we want the chunk to be at least loaded + this.loadQueue.enqueue(chunk); + break; + } + case CHUNK_TICKET_STAGE_LOADING: { + this.loadingQueue.enqueue(chunk); + break; + } + case CHUNK_TICKET_STAGE_LOADED: { + if (canGenerateChunks || this.isLoadedChunkGeneratable(chunkX, chunkZ)) { + this.genQueue.enqueue(chunk); + } + break; + } + case CHUNK_TICKET_STAGE_GENERATING: { + this.generatingQueue.enqueue(chunk); + break; + } + case CHUNK_TICKET_STAGE_GENERATED: { + if (sendChunk && !sentChunk) { + this.sendQueue.enqueue(chunk); + } + if (squareDistance <= tickViewDistance) { + this.tickingQueue.enqueue(chunk); + } + break; + } + case CHUNK_TICKET_STAGE_TICK: { + if (sendChunk && !sentChunk) { + this.sendQueue.enqueue(chunk); + } + break; + } + default: { + throw new IllegalStateException("Unknown stage: " + stage); + } + } + } + + // update the chunk center + // this must be done last so that the client does not ignore any of our unload chunk packets above + if (this.lastSentChunkCenterX != currentChunkX || this.lastSentChunkCenterZ != currentChunkZ) { + this.player.connection.send(this.updateClientChunkCenter(currentChunkX, currentChunkZ)); + } + + this.flushDelayedTicketOps(); + } + + void remove() { + // sends the chunk unload packets + this.broadcastMap.remove(); + // cleans up loading/generating tickets + this.loadTicketCleanup.remove(); + // cleans up ticking tickets + this.tickMap.remove(); + + // purge queues + this.sendQueue.clear(); + this.tickingQueue.clear(); + this.generatingQueue.clear(); + this.genQueue.clear(); + this.loadingQueue.clear(); + this.loadQueue.clear(); + + // flush ticket changes + this.flushDelayedTicketOps(); + + // now all tickets should be removed, which is all of our external state + } + } + + private static final class MultiIntervalledCounter { + + private final IntervalledCounter[] counters; + + public MultiIntervalledCounter(final long... intervals) { + final IntervalledCounter[] counters = new IntervalledCounter[intervals.length]; + for (int i = 0; i < intervals.length; ++i) { + counters[i] = new IntervalledCounter(intervals[i]); + } + this.counters = counters; + } + + public long getMaxCountBeforeViolation(final double rate) { + long count = Long.MAX_VALUE; + for (final IntervalledCounter counter : this.counters) { + final long sum = counter.getSum(); + final long interval = counter.getInterval(); + // rate = sum / interval + // so, sum = rate*interval + final long maxSum = (long)Math.ceil(rate * (1.0E-9 * (double)interval)); + final long diff = maxSum - sum; + if (diff < count) { + count = diff; + } + } + + return Math.max(0L, count); + } + + public void update(final long time) { + for (final IntervalledCounter counter : this.counters) { + counter.updateCurrentTime(time); + } + } + + public void updateAndAdd(final long count, final long time) { + for (final IntervalledCounter counter : this.counters) { + counter.updateAndAdd(count, time); + } + } + + public void addTime(final long time, final long count) { + for (final IntervalledCounter counter : this.counters) { + counter.addTime(time, count); + } + } + + public double getMaxRate() { + double ret = 0.0; + + for (final IntervalledCounter counter : this.counters) { + final double counterRate = counter.getRate(); + if (counterRate > ret) { + ret = counterRate; + } + } + + return ret; + } + } + + // TODO rebase into util patch + public static abstract class SingleUserAreaMap { + + private static final int NOT_SET = Integer.MIN_VALUE; + + private final T parameter; + private int lastChunkX = NOT_SET; + private int lastChunkZ = NOT_SET; + private int distance = NOT_SET; + + public SingleUserAreaMap(final T parameter) { + this.parameter = parameter; + } + + /* math sign function except 0 returns 1 */ + protected static int sign(int val) { + return 1 | (val >> (Integer.SIZE - 1)); + } + + protected abstract void addCallback(final T parameter, final int chunkX, final int chunkZ); + + protected abstract void removeCallback(final T parameter, final int chunkX, final int chunkZ); + + private void addToNew(final T parameter, final int chunkX, final int chunkZ, final int distance) { + final int maxX = chunkX + distance; + final int maxZ = chunkZ + distance; + + for (int cx = chunkX - distance; cx <= maxX; ++cx) { + for (int cz = chunkZ - distance; cz <= maxZ; ++cz) { + this.addCallback(parameter, cx, cz); + } + } + } + + private void removeFromOld(final T parameter, final int chunkX, final int chunkZ, final int distance) { + final int maxX = chunkX + distance; + final int maxZ = chunkZ + distance; + + for (int cx = chunkX - distance; cx <= maxX; ++cx) { + for (int cz = chunkZ - distance; cz <= maxZ; ++cz) { + this.removeCallback(parameter, cx, cz); + } + } + } + + public final boolean add(final int chunkX, final int chunkZ, final int distance) { + if (distance < 0) { + throw new IllegalArgumentException(Integer.toString(distance)); + } + if (this.lastChunkX != NOT_SET) { + return false; + } + this.lastChunkX = chunkX; + this.lastChunkZ = chunkZ; + this.distance = distance; + + this.addToNew(this.parameter, chunkX, chunkZ, distance); + + return true; + } + + public final boolean update(final int toX, final int toZ, final int newViewDistance) { + if (newViewDistance < 0) { + throw new IllegalArgumentException(Integer.toString(newViewDistance)); + } + final int fromX = this.lastChunkX; + final int fromZ = this.lastChunkZ; + final int oldViewDistance = this.distance; + if (fromX == NOT_SET) { + return false; + } + + this.lastChunkX = toX; + this.lastChunkZ = toZ; + + final T parameter = this.parameter; + + + final int dx = toX - fromX; + final int dz = toZ - fromZ; + + final int totalX = IntegerUtil.branchlessAbs(fromX - toX); + final int totalZ = IntegerUtil.branchlessAbs(fromZ - toZ); + + if (Math.max(totalX, totalZ) > (2 * Math.max(newViewDistance, oldViewDistance))) { + // teleported? + this.removeFromOld(parameter, fromX, fromZ, oldViewDistance); + this.addToNew(parameter, toX, toZ, newViewDistance); + return true; + } + + if (oldViewDistance != newViewDistance) { + // remove loop + + final int oldMinX = fromX - oldViewDistance; + final int oldMinZ = fromZ - oldViewDistance; + final int oldMaxX = fromX + oldViewDistance; + final int oldMaxZ = fromZ + oldViewDistance; + for (int currX = oldMinX; currX <= oldMaxX; ++currX) { + for (int currZ = oldMinZ; currZ <= oldMaxZ; ++currZ) { + + // only remove if we're outside the new view distance... + if (Math.max(IntegerUtil.branchlessAbs(currX - toX), IntegerUtil.branchlessAbs(currZ - toZ)) > newViewDistance) { + this.removeCallback(parameter, currX, currZ); + } + } + } + + // add loop + + final int newMinX = toX - newViewDistance; + final int newMinZ = toZ - newViewDistance; + final int newMaxX = toX + newViewDistance; + final int newMaxZ = toZ + newViewDistance; + for (int currX = newMinX; currX <= newMaxX; ++currX) { + for (int currZ = newMinZ; currZ <= newMaxZ; ++currZ) { + + // only add if we're outside the old view distance... + if (Math.max(IntegerUtil.branchlessAbs(currX - fromX), IntegerUtil.branchlessAbs(currZ - fromZ)) > oldViewDistance) { + this.addCallback(parameter, currX, currZ); + } + } + } + + return true; + } + + // x axis is width + // z axis is height + // right refers to the x axis of where we moved + // top refers to the z axis of where we moved + + // same view distance + + // used for relative positioning + final int up = sign(dz); // 1 if dz >= 0, -1 otherwise + final int right = sign(dx); // 1 if dx >= 0, -1 otherwise + + // The area excluded by overlapping the two view distance squares creates four rectangles: + // Two on the left, and two on the right. The ones on the left we consider the "removed" section + // and on the right the "added" section. + // https://i.imgur.com/MrnOBgI.png is a reference image. Note that the outside border is not actually + // exclusive to the regions they surround. + + // 4 points of the rectangle + int maxX; // exclusive + int minX; // inclusive + int maxZ; // exclusive + int minZ; // inclusive + + if (dx != 0) { + // handle right addition + + maxX = toX + (oldViewDistance * right) + right; // exclusive + minX = fromX + (oldViewDistance * right) + right; // inclusive + maxZ = fromZ + (oldViewDistance * up) + up; // exclusive + minZ = toZ - (oldViewDistance * up); // inclusive + + for (int currX = minX; currX != maxX; currX += right) { + for (int currZ = minZ; currZ != maxZ; currZ += up) { + this.addCallback(parameter, currX, currZ); + } + } + } + + if (dz != 0) { + // handle up addition + + maxX = toX + (oldViewDistance * right) + right; // exclusive + minX = toX - (oldViewDistance * right); // inclusive + maxZ = toZ + (oldViewDistance * up) + up; // exclusive + minZ = fromZ + (oldViewDistance * up) + up; // inclusive + + for (int currX = minX; currX != maxX; currX += right) { + for (int currZ = minZ; currZ != maxZ; currZ += up) { + this.addCallback(parameter, currX, currZ); + } + } + } + + if (dx != 0) { + // handle left removal + + maxX = toX - (oldViewDistance * right); // exclusive + minX = fromX - (oldViewDistance * right); // inclusive + maxZ = fromZ + (oldViewDistance * up) + up; // exclusive + minZ = toZ - (oldViewDistance * up); // inclusive + + for (int currX = minX; currX != maxX; currX += right) { + for (int currZ = minZ; currZ != maxZ; currZ += up) { + this.removeCallback(parameter, currX, currZ); + } + } + } + + if (dz != 0) { + // handle down removal + + maxX = fromX + (oldViewDistance * right) + right; // exclusive + minX = fromX - (oldViewDistance * right); // inclusive + maxZ = toZ - (oldViewDistance * up); // exclusive + minZ = fromZ - (oldViewDistance * up); // inclusive + + for (int currX = minX; currX != maxX; currX += right) { + for (int currZ = minZ; currZ != maxZ; currZ += up) { + this.removeCallback(parameter, currX, currZ); + } + } + } + + return true; + } + + public final boolean remove() { + final int chunkX = this.lastChunkX; + final int chunkZ = this.lastChunkZ; + final int distance = this.distance; + if (chunkX == NOT_SET) { + return false; + } + + this.lastChunkX = this.lastChunkZ = this.distance = NOT_SET; + + this.removeFromOld(this.parameter, chunkX, chunkZ, distance); + + return true; + } + } +} diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java index e5d9c6f2cbe11c2ded6d8ad111fa6a8b2086dfba..c6d20bc2f0eab737338db6b88dacb63f0decb66c 100644 --- a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java +++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java @@ -1,5 +1,6 @@ package io.papermc.paper.chunk.system.scheduling; +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; import ca.spottedleaf.concurrentutil.map.SWMRLong2ObjectHashTable; import co.aikar.timings.Timing; @@ -493,6 +494,21 @@ public final class ChunkHolderManager { } } + // atomic with respect to all add/remove/addandremove ticket calls for the given chunk + public boolean addIfRemovedTicket(final long chunk, final TicketType addType, final int addLevel, final T addIdentifier, + final TicketType 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 void removeAllTicketsFor(final TicketType ticketType, final int ticketLevel, final T ticketIdentifier) { if (ticketLevel > MAX_TICKET_LEVEL) { return; @@ -900,6 +916,142 @@ public final class ChunkHolderManager { } } + public enum TicketOperationType { + ADD, REMOVE, ADD_IF_REMOVED, ADD_AND_REMOVE + } + + public static record TicketOperation ( + TicketOperationType op, long chunkCoord, + TicketType ticketType, int ticketLevel, T identifier, + TicketType ticketType2, int ticketLevel2, V identifier2 + ) { + + private TicketOperation(TicketOperationType op, long chunkCoord, + TicketType ticketType, int ticketLevel, T identifier) { + this(op, chunkCoord, ticketType, ticketLevel, identifier, null, 0, null); + } + + public static TicketOperation addOp(final ChunkPos chunk, final TicketType type, final int ticketLevel, final T identifier) { + return addOp(CoordinateUtils.getChunkKey(chunk), type, ticketLevel, identifier); + } + + public static TicketOperation addOp(final int chunkX, final int chunkZ, final TicketType type, final int ticketLevel, final T identifier) { + return addOp(CoordinateUtils.getChunkKey(chunkX, chunkZ), type, ticketLevel, identifier); + } + + public static TicketOperation addOp(final long chunk, final TicketType type, final int ticketLevel, final T identifier) { + return new TicketOperation<>(TicketOperationType.ADD, chunk, type, ticketLevel, identifier); + } + + public static TicketOperation removeOp(final ChunkPos chunk, final TicketType type, final int ticketLevel, final T identifier) { + return removeOp(CoordinateUtils.getChunkKey(chunk), type, ticketLevel, identifier); + } + + public static TicketOperation removeOp(final int chunkX, final int chunkZ, final TicketType type, final int ticketLevel, final T identifier) { + return removeOp(CoordinateUtils.getChunkKey(chunkX, chunkZ), type, ticketLevel, identifier); + } + + public static TicketOperation removeOp(final long chunk, final TicketType type, final int ticketLevel, final T identifier) { + return new TicketOperation<>(TicketOperationType.REMOVE, chunk, type, ticketLevel, identifier); + } + + public static TicketOperation addIfRemovedOp(final long chunk, + final TicketType addType, final int addLevel, final T addIdentifier, + final TicketType removeType, final int removeLevel, final V removeIdentifier) { + return new TicketOperation<>( + TicketOperationType.ADD_IF_REMOVED, chunk, addType, addLevel, addIdentifier, + removeType, removeLevel, removeIdentifier + ); + } + + public static TicketOperation addAndRemove(final long chunk, + final TicketType addType, final int addLevel, final T addIdentifier, + final TicketType removeType, final int removeLevel, final V removeIdentifier) { + return new TicketOperation<>( + TicketOperationType.ADD_AND_REMOVE, chunk, addType, addLevel, addIdentifier, + removeType, removeLevel, removeIdentifier + ); + } + } + + private final MultiThreadedQueue> 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> 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 BLOCK_TICKET_UPDATES = ThreadLocal.withInitial(() -> { return Boolean.FALSE; }); @@ -948,6 +1100,8 @@ public final class ChunkHolderManager { this.ticketLock.lock(); try { + this.drainTicketUpdates(); + final boolean levelsUpdated = this.ticketLevelPropagator.propagateUpdates(); if (levelsUpdated) { // Unlike CB, ticket level updates cannot happen recursively. Thank god. diff --git a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java index 8d442c5a498ecf288a0cc0c54889c6e2fda849ce..9f5f0d8ddc8f480b48079c70e38c9c08eff403f6 100644 --- a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java +++ b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java @@ -287,4 +287,43 @@ public class GlobalConfiguration extends ConfigurationPart { public boolean useDimensionTypeForCustomSpawners = false; public boolean strictAdvancementDimensionCheck = false; } + + public ChunkLoadingBasic chunkLoadingBasic; + + public class ChunkLoadingBasic extends ConfigurationPart { + @Comment("The maximum rate in chunks per second that the server will send to any individual player. Set to -1 to disable this limit.") + public double playerMaxChunkSendRate = 75.0; + + @Comment( + "The maximum rate at which chunks will load for any individual player. " + + "Note that this setting also affects chunk generations, since a chunk load is always first issued to test if a" + + "chunk is already generated. Set to -1 to disable this limit." + ) + public double playerMaxChunkLoadRate = 100.0; + + @Comment("The maximum rate at which chunks will generate for any individual player. Set to -1 to disable this limit.") + public double playerMaxChunkGenerateRate = -1.0; + } + + public ChunkLoadingAdvanced chunkLoadingAdvanced; + + public class ChunkLoadingAdvanced extends ConfigurationPart { + @Comment( + "Set to true if the server will match the chunk send radius that clients have configured" + + "in their view distance settings if the client is less-than the server's send distance." + ) + public boolean autoConfigSendDistance = true; + + @Comment( + "Specifies the maximum amount of concurrent chunk loads that an individual player can have." + + "Set to 0 to let the server configure it automatically per player, or set it to -1 to disable the limit." + ) + public int playerMaxConcurrentChunkLoads = 0; + + @Comment( + "Specifies the maximum amount of concurrent chunk generations that an individual player can have." + + "Set to 0 to let the server configure it automatically per player, or set it to -1 to disable the limit." + ) + public int playerMaxConcurrentChunkGenerates = 0; + } } diff --git a/src/main/java/io/papermc/paper/util/IntervalledCounter.java b/src/main/java/io/papermc/paper/util/IntervalledCounter.java index cea9c098ade00ee87b8efc8164ab72f5279758f0..197224e31175252d8438a8df585bbb65f2288d7f 100644 --- a/src/main/java/io/papermc/paper/util/IntervalledCounter.java +++ b/src/main/java/io/papermc/paper/util/IntervalledCounter.java @@ -2,6 +2,8 @@ package io.papermc.paper.util; public final class IntervalledCounter { + private static final int INITIAL_SIZE = 8; + protected long[] times; protected long[] counts; protected final long interval; @@ -11,8 +13,8 @@ public final class IntervalledCounter { protected int tail; // exclusive public IntervalledCounter(final long interval) { - this.times = new long[8]; - this.counts = new long[8]; + this.times = new long[INITIAL_SIZE]; + this.counts = new long[INITIAL_SIZE]; this.interval = interval; } @@ -67,13 +69,13 @@ public final class IntervalledCounter { this.tail = nextTail; } - public void updateAndAdd(final int count) { + public void updateAndAdd(final long count) { final long currTime = System.nanoTime(); this.updateCurrentTime(currTime); this.addTime(currTime, count); } - public void updateAndAdd(final int count, final long currTime) { + public void updateAndAdd(final long count, final long currTime) { this.updateCurrentTime(currTime); this.addTime(currTime, count); } @@ -93,9 +95,13 @@ public final class IntervalledCounter { this.tail = size; if (tail >= head) { + // sequentially ordered from [head, tail) System.arraycopy(oldElements, head, newElements, 0, size); System.arraycopy(oldCounts, head, newCounts, 0, size); } else { + // ordered from [head, length) + // then followed by [0, tail) + System.arraycopy(oldElements, head, newElements, 0, oldElements.length - head); System.arraycopy(oldElements, 0, newElements, oldElements.length - head, tail); @@ -106,10 +112,18 @@ public final class IntervalledCounter { // returns in units per second public double getRate() { - return this.size() / (this.interval * 1.0e-9); + return (double)this.sum / ((double)this.interval * 1.0E-9); + } + + public long getInterval() { + return this.interval; } - public long size() { + public long getSum() { return this.sum; } + + public int totalDataPoints() { + return this.tail >= this.head ? (this.tail - this.head) : (this.tail + (this.counts.length - this.head)); + } } diff --git a/src/main/java/io/papermc/paper/util/MCUtil.java b/src/main/java/io/papermc/paper/util/MCUtil.java index 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..6fce2a9bce051e21eba8f331007a9752607f69f2 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 playersInChunkTickRange; // Paper end - optimise anyPlayerCloseEnoughForSpawning + // Paper start - replace player chunk loader + private final com.destroystokyo.paper.util.maplist.ReferenceList playersSentChunkTo = new com.destroystokyo.paper.util.maplist.ReferenceList<>(); + + public void addPlayer(ServerPlayer player) { + if (!this.playersSentChunkTo.add(player)) { + throw new IllegalStateException("Already sent chunk " + this.pos + " to player " + player); + } + } + + public void removePlayer(ServerPlayer player) { + if (!this.playersSentChunkTo.remove(player)) { + throw new IllegalStateException("Have not sent chunk " + this.pos + " to player " + player); + } + } + + public boolean hasChunkBeenSent() { + return this.playersSentChunkTo.size() != 0; + } + // Paper end - replace player chunk loader + public ChunkHolder(ChunkPos pos, LevelHeightAccessor world, LevelLightEngine lightingProvider, ChunkHolder.PlayerProvider playersWatchingChunkProvider, io.papermc.paper.chunk.system.scheduling.NewChunkHolder newChunkHolder) { // Paper - rewrite chunk system this.newChunkHolder = newChunkHolder; // Paper - rewrite chunk system this.chunkToSaveHistory = null; @@ -225,6 +245,11 @@ public class ChunkHolder { // Paper - rewrite chunk system public void blockChanged(BlockPos pos) { + // Paper start - replace player chunk loader + if (this.playersSentChunkTo.size() == 0) { + return; + } + // Paper end - replace player chunk loader LevelChunk chunk = this.getSendingChunk(); // Paper - no-tick view distance if (chunk != null) { @@ -251,7 +276,7 @@ public class ChunkHolder { LevelChunk chunk = this.getSendingChunk(); // Paper end - no-tick view distance - if (chunk != null) { + if (this.playersSentChunkTo.size() != 0 && chunk != null) { // Paper - replace player chunk loader int j = this.lightEngine.getMinLightSection(); int k = this.lightEngine.getMaxLightSection(); @@ -351,27 +376,32 @@ public class ChunkHolder { } - public void broadcast(Packet packet, boolean onlyOnWatchDistanceEdge) { - // Paper start - per player view distance - // there can be potential desync with player's last mapped section and the view distance map, so use the - // view distance map here. - com.destroystokyo.paper.util.misc.PlayerAreaMap viewDistanceMap = this.chunkMap.playerChunkManager.broadcastMap; // Paper - replace old player chunk manager - com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet players = viewDistanceMap.getObjectsInRange(this.pos); - if (players == null) { - return; - } + // Paper start - rewrite player chunk loader + public List getPlayers(boolean onlyOnWatchDistanceEdge) { + List 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..2d133ae656f7420d6ceec14bc591721cff815479 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,22 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider int k = this.viewDistance; this.viewDistance = j; - this.playerChunkManager.setLoadDistance(this.viewDistance); // Paper - replace player loader system + this.level.playerChunkLoader.setLoadDistance(this.viewDistance); // Paper - replace player loader system } } public void updateChunkTracking(ServerPlayer player, ChunkPos pos, MutableObject> packet, boolean oldWithinViewDistance, boolean newWithinViewDistance) { // Paper - public // Paper - Anti-Xray - Bypass if (player.level == this.level) { + ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos.toLong()); // Paper - replace chunk loader system - move up if (newWithinViewDistance && !oldWithinViewDistance) { - ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos.toLong()); + // Paper - replace chunk loader system - move up if (playerchunk != null) { LevelChunk chunk = playerchunk.getSendingChunk(); // Paper - replace chunk loader system if (chunk != null) { + playerchunk.addPlayer(player); // Paper - replace chunk loader system this.playerLoadedChunk(player, packet, chunk); } @@ -848,6 +854,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } if (!newWithinViewDistance && oldWithinViewDistance) { + // Paper start - replace chunk loader system + if (playerchunk != null) { + playerchunk.removePlayer(player); + } + // Paper end - replace chunk loader system player.untrackChunk(pos); } @@ -1151,34 +1162,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 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 players = this.playerChunkManager.broadcastMap.getObjectsInRange(chunkPos); - if (players == null) { - return java.util.Collections.emptyList(); - } - - List 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 +1607,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider double vec3d_dx = player.getX() - this.entity.getX(); double vec3d_dz = player.getZ() - this.entity.getZ(); // Paper end - remove allocation of Vec3D here - double d0 = (double) Math.min(this.getEffectiveRange(), io.papermc.paper.chunk.PlayerChunkLoader.getSendViewDistance(player) * 16); // Paper - per player view distance + double d0 = (double) Math.min(this.getEffectiveRange(), io.papermc.paper.chunk.system.ChunkSystem.getSendViewDistance(player) * 16); // Paper - per player view distance double d1 = vec3d_dx * vec3d_dx + vec3d_dz * vec3d_dz; // Paper double d2 = d0 * d0; boolean flag = d1 <= d2 && this.entity.broadcastToPlayer(player); diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java index 52cba8f68d274cce106304aef1249a95474d3238..88fca8b160df6804f30ed2cf8cf1f645085434e2 100644 --- a/src/main/java/net/minecraft/server/level/DistanceManager.java +++ b/src/main/java/net/minecraft/server/level/DistanceManager.java @@ -184,17 +184,17 @@ public abstract class DistanceManager { } protected void updatePlayerTickets(int viewDistance) { - this.chunkMap.playerChunkManager.setTargetNoTickViewDistance(viewDistance); // Paper - route to player chunk manager + this.chunkMap.setViewDistance(viewDistance);// Paper - route to player chunk manager } // Paper start public int getSimulationDistance() { - return this.chunkMap.playerChunkManager.getTargetTickViewDistance(); // Paper - route to player chunk manager + return this.chunkMap.level.playerChunkLoader.getAPITickDistance(); } // Paper end public void updateSimulationDistance(int simulationDistance) { - this.chunkMap.playerChunkManager.setTargetTickViewDistance(simulationDistance); // Paper - route to player chunk manager + this.chunkMap.level.playerChunkLoader.setTickDistance(simulationDistance); // Paper - route to player chunk manager } public int getNaturalSpawnChunkCount() { diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java index ca84eddbdb1e198b899750e5f6b3eafd25ce970f..736f37979c882e41e7571202df38eb6a2923fcb0 100644 --- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java +++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java @@ -645,7 +645,7 @@ public class ServerChunkCache extends ChunkSource { this.level.getProfiler().popPush("chunks"); if (tickChunks) { this.level.timings.chunks.startTiming(); // Paper - timings - this.chunkMap.playerChunkManager.tick(); // Paper - this is mostly is to account for view distance changes + this.chunkMap.level.playerChunkLoader.tick(); // Paper - replace player chunk loader - this is mostly required to account for view distance changes this.tickChunks(); this.level.timings.chunks.stopTiming(); // Paper - timings } @@ -1001,7 +1001,7 @@ public class ServerChunkCache extends ChunkSource { @Override // CraftBukkit start - process pending Chunk loadCallback() and unloadCallback() after each run task public boolean pollTask() { - ServerChunkCache.this.chunkMap.playerChunkManager.tickMidTick(); + ServerChunkCache.this.chunkMap.level.playerChunkLoader.tickMidTick(); // Paper - replace player chunk loader if (ServerChunkCache.this.runDistanceManagerUpdates()) { return true; } diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java index 5a5ff40df37db9cbd53c584ed26a3ce4888b29c0..bf1a77cf9bbea4e2104b2a8c61309e740f28d51b 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 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 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 resourcekey, LevelStem worlddimension, ChunkProgressListener worldloadlistener, boolean flag, long i, List 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 1d4d02f26391ac55c7631817f09d05e2769b0d29..4231c3fe14a621d237bdc3d56c593f0adc0a7bc6 100644 --- a/src/main/java/net/minecraft/server/level/ServerPlayer.java +++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java @@ -276,6 +276,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 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 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 c0c14766adaac855112f85a203a6163b8adfdded..0dbe182fbae5ce5ba182176eb5d5e3f1897e77f2 100644 --- a/src/main/java/net/minecraft/server/players/PlayerList.java +++ b/src/main/java/net/minecraft/server/players/PlayerList.java @@ -271,7 +271,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()))); @@ -905,8 +905,8 @@ public abstract class PlayerList { // CraftBukkit start LevelData worlddata = worldserver1.getLevelData(); entityplayer1.connection.send(new ClientboundRespawnPacket(worldserver1.dimensionTypeId(), worldserver1.dimension(), BiomeManager.obfuscateSeed(worldserver1.getSeed()), entityplayer1.gameMode.getGameModeForPlayer(), entityplayer1.gameMode.getPreviousGameModeForPlayer(), worldserver1.isDebug(), worldserver1.isFlat(), (byte) i, entityplayer1.getLastDeathLocation())); - entityplayer1.connection.send(new ClientboundSetChunkCacheRadiusPacket(worldserver1.getChunkSource().chunkMap.playerChunkManager.getTargetSendDistance())); // Spigot // Paper - replace old player chunk management - entityplayer1.connection.send(new ClientboundSetSimulationDistancePacket(worldserver1.getChunkSource().chunkMap.playerChunkManager.getTargetTickViewDistance())); // Spigot // Paper - replace old player chunk management + entityplayer1.connection.send(new ClientboundSetChunkCacheRadiusPacket(worldserver1.getWorld().getSendViewDistance())); // Spigot // Paper - replace old player chunk management + entityplayer1.connection.send(new ClientboundSetSimulationDistancePacket(worldserver1.getWorld().getSimulationDistance())); // Spigot // Paper - replace old player chunk management entityplayer1.spawnIn(worldserver1); entityplayer1.unsetRemoved(); entityplayer1.connection.teleport(new Location(worldserver1.getWorld(), entityplayer1.getX(), entityplayer1.getY(), entityplayer1.getZ(), entityplayer1.getYRot(), entityplayer1.getXRot())); diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java index 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 206dc04086a218b510930739a6c573f2653ab0fa..9599af33c683ec47e28b1c8e4dc965d30d9081a7 100644 --- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java @@ -184,43 +184,6 @@ public class LevelChunk extends ChunkAccess { protected void onNeighbourChange(final long bitsetBefore, final long bitsetAfter) { - // Paper start - no-tick view distance - ServerChunkCache chunkProviderServer = ((ServerLevel)this.level).getChunkSource(); - net.minecraft.server.level.ChunkMap chunkMap = chunkProviderServer.chunkMap; - // this code handles the addition of ticking tickets - the distance map handles the removal - if (!areNeighboursLoaded(bitsetBefore, 2) && areNeighboursLoaded(bitsetAfter, 2)) { - if (chunkMap.playerChunkManager.tickMap.getObjectsInRange(this.coordinateKey) != null) { // Paper - replace old player chunk loading system - // now we're ready for entity ticking - chunkProviderServer.mainThreadProcessor.execute(() -> { - // double check that this condition still holds. - if (LevelChunk.this.areNeighboursLoaded(2) && chunkMap.playerChunkManager.tickMap.getObjectsInRange(LevelChunk.this.coordinateKey) != null) { // Paper - replace old player chunk loading system - chunkMap.playerChunkManager.onChunkPlayerTickReady(this.chunkPos.x, this.chunkPos.z); // Paper - replace old player chunk - chunkProviderServer.addTicketAtLevel(net.minecraft.server.level.TicketType.PLAYER, LevelChunk.this.chunkPos, 31, LevelChunk.this.chunkPos); // 31 -> entity ticking, TODO check on update - } - }); - } - } - - // this code handles the chunk sending - if (!areNeighboursLoaded(bitsetBefore, 1) && areNeighboursLoaded(bitsetAfter, 1)) { - // Paper start - replace old player chunk loading system - if (chunkMap.playerChunkManager.isChunkNearPlayers(this.chunkPos.x, this.chunkPos.z)) { - // the post processing is expensive, so we don't want to run it unless we're actually near - // a player. - chunkProviderServer.mainThreadProcessor.execute(() -> { - if (!LevelChunk.this.areNeighboursLoaded(1)) { - return; - } - LevelChunk.this.postProcessGeneration(); - if (!LevelChunk.this.areNeighboursLoaded(1)) { - return; - } - chunkMap.playerChunkManager.onChunkSendReady(this.chunkPos.x, this.chunkPos.z); - }); - } - // Paper end - replace old player chunk loading system - } - // Paper end - no-tick view distance } public final boolean isAnyNeighborsLoaded() { @@ -906,7 +869,6 @@ public class LevelChunk extends ChunkAccess { // Paper - rewrite chunk system - move into separate callback org.bukkit.Server server = this.level.getCraftServer(); // Paper - rewrite chunk system - move into separate callback - ((ServerLevel)this.level).getChunkSource().chunkMap.playerChunkManager.onChunkLoad(this.chunkPos.x, this.chunkPos.z); // Paper - rewrite player chunk management if (server != null) { /* * If it's a new world, the first few chunks are generated inside @@ -1086,6 +1048,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 } } @@ -1105,7 +1068,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 5e9055fdf411029ea2fed91acd6b981f79156418..ff6559bf563f2fdcc0f2843d4f4aa24d7ddfb6db 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java @@ -2273,12 +2273,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 @@ -2312,12 +2312,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 0ae1fce0c1a2e3bfbbab756a088fc76545e263fa..fec408416aa63e1022802a644bb95131601f0c3b 100644 --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java @@ -189,44 +189,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 @@ -241,23 +219,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