mirror of
https://github.com/PaperMC/Paper.git
synced 2024-11-09 04:09:54 +01:00
1549 lines
81 KiB
Diff
1549 lines
81 KiB
Diff
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
|
From: Aikar <aikar@aikar.co>
|
|
Date: Sat, 11 Apr 2020 03:56:07 -0400
|
|
Subject: [PATCH] Implement Chunk Priority / Urgency System for Chunks
|
|
|
|
Mark chunks that are blocking main thread for world generation as urgent
|
|
|
|
Implements a general priority system so that chunks that are sorted in
|
|
the generator queues can prioritize certain chunks over another.
|
|
|
|
Urgent chunks will jump to the front of the line, ensuring that a
|
|
sync chunk load on an ungenerated chunk does not lag the server for
|
|
a long period of time if the servers generator queues are filled with
|
|
lots of chunks already.
|
|
|
|
This massively reduces the lag spikes from sync chunk gens.
|
|
|
|
Then we further prioritize loading order so nearby chunks have higher
|
|
priority than distant chunks, reducing the pressure a high no tick
|
|
view distance holds on you.
|
|
|
|
Chunks in front of the player have higher priority, to help with
|
|
fast traveling players keep up with their movement.
|
|
|
|
diff --git a/src/main/java/com/destroystokyo/paper/io/chunk/ChunkTaskManager.java b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkTaskManager.java
|
|
index 18ae2e2b339d357fbe0f6f2b18bc14c0dfe4c222..3b7ba9c755c82a6f086d5542d32b3567c0f98b99 100644
|
|
--- a/src/main/java/com/destroystokyo/paper/io/chunk/ChunkTaskManager.java
|
|
+++ b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkTaskManager.java
|
|
@@ -108,7 +108,7 @@ public final class ChunkTaskManager {
|
|
}
|
|
|
|
static void dumpChunkInfo(Set<ChunkHolder> seenChunks, ChunkHolder chunkHolder, int x, int z) {
|
|
- dumpChunkInfo(seenChunks, chunkHolder, x, z, 0, 1);
|
|
+ dumpChunkInfo(seenChunks, chunkHolder, x, z, 0, 4); // Paper - 1->4
|
|
}
|
|
|
|
static void dumpChunkInfo(Set<ChunkHolder> seenChunks, ChunkHolder chunkHolder, int x, int z, int indent, int maxDepth) {
|
|
@@ -129,6 +129,31 @@ public final class ChunkTaskManager {
|
|
PaperFileIOThread.LOGGER.log(Level.ERROR, indentStr + "Chunk Status - " + ((chunk == null) ? "null chunk" : chunk.getStatus().toString()));
|
|
PaperFileIOThread.LOGGER.log(Level.ERROR, indentStr + "Chunk Ticket Status - " + ChunkHolder.getStatus(chunkHolder.getTicketLevel()));
|
|
PaperFileIOThread.LOGGER.log(Level.ERROR, indentStr + "Chunk Holder Status - " + ((holderStatus == null) ? "null" : holderStatus.toString()));
|
|
+ // Paper start
|
|
+ PaperFileIOThread.LOGGER.log(Level.ERROR, indentStr + "Chunk Holder Priority - " + chunkHolder.queueLevel);
|
|
+
|
|
+ if (!chunkHolder.neighbors.isEmpty()) {
|
|
+ if (indent >= maxDepth) {
|
|
+ PaperFileIOThread.LOGGER.log(Level.ERROR, indentStr + "Chunk Neighbors: (Can't show, too deeply nested)");
|
|
+ return;
|
|
+ }
|
|
+ PaperFileIOThread.LOGGER.log(Level.ERROR, indentStr + "Chunk Neighbors: ");
|
|
+ for (ChunkHolder neighbor : chunkHolder.neighbors.keySet()) {
|
|
+ ChunkStatus status = neighbor.getChunkHolderStatus();
|
|
+ if (status != null && status.isOrAfter(ChunkHolder.getStatus(neighbor.getTicketLevel()))) {
|
|
+ continue;
|
|
+ }
|
|
+ int nx = neighbor.pos.x;
|
|
+ int nz = neighbor.pos.z;
|
|
+ if (seenChunks.contains(neighbor)) {
|
|
+ PaperFileIOThread.LOGGER.log(Level.ERROR, indentStr + " " + nx + "," + nz + " in " + chunkHolder.getWorld().getWorld().getName() + " (CIRCULAR)");
|
|
+ continue;
|
|
+ }
|
|
+ PaperFileIOThread.LOGGER.log(Level.ERROR, indentStr + " " + nx + "," + nz + " in " + chunkHolder.getWorld().getWorld().getName() + ":");
|
|
+ dumpChunkInfo(seenChunks, neighbor, nx, nz, indent + 1, maxDepth);
|
|
+ }
|
|
+ }
|
|
+ // Paper end
|
|
}
|
|
}
|
|
|
|
diff --git a/src/main/java/net/minecraft/server/MCUtil.java b/src/main/java/net/minecraft/server/MCUtil.java
|
|
index 4bc418f45c6f16a9a1c78b6688625268870a96ad..379ba589b0423284d63782d951c64770b160cf2d 100644
|
|
--- a/src/main/java/net/minecraft/server/MCUtil.java
|
|
+++ b/src/main/java/net/minecraft/server/MCUtil.java
|
|
@@ -671,6 +671,7 @@ public final class MCUtil {
|
|
chunkData.addProperty("x", playerChunk.pos.x);
|
|
chunkData.addProperty("z", playerChunk.pos.z);
|
|
chunkData.addProperty("ticket-level", playerChunk.getTicketLevel());
|
|
+ chunkData.addProperty("priority", playerChunk.queueLevel); // Paper - priority
|
|
chunkData.addProperty("state", ChunkHolder.getFullChunkStatus(playerChunk.getTicketLevel()).toString());
|
|
chunkData.addProperty("queued-for-unload", chunkMap.toDrop.contains(playerChunk.pos.longKey));
|
|
chunkData.addProperty("status", status == null ? "unloaded" : status.toString());
|
|
diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java
|
|
index fe8e6d66665dc8c38e74c0b264cf507f99f83091..e317c73b430105052cbf28502916adc3bd60846b 100644
|
|
--- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
|
|
+++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java
|
|
@@ -60,7 +60,7 @@ public class ChunkHolder {
|
|
private final DebugBuffer<ChunkHolder.ChunkSaveDebug> chunkToSaveHistory;
|
|
public int oldTicketLevel;
|
|
private int ticketLevel;
|
|
- private int queueLevel;
|
|
+ public volatile int queueLevel; // Paper - private->public, make volatile since this is concurrently accessed
|
|
public final ChunkPos pos;
|
|
private boolean hasChangedSections;
|
|
private final ShortSet[] changedBlocksPerSection;
|
|
@@ -75,6 +75,7 @@ public class ChunkHolder {
|
|
|
|
boolean isUpdateQueued = false; // Paper
|
|
private final ChunkMap chunkMap; // Paper
|
|
+ public ServerLevel getWorld() { return chunkMap.level; } // Paper
|
|
// Paper start - no-tick view distance
|
|
public final LevelChunk getSendingChunk() {
|
|
// it's important that we use getChunkAtIfLoadedImmediately to mirror the chunk sending logic used
|
|
@@ -100,6 +101,136 @@ public class ChunkHolder {
|
|
}
|
|
// Paper end
|
|
|
|
+ // Paper start - Chunk gen/load priority system
|
|
+ volatile int neighborPriority = -1;
|
|
+ volatile int priorityBoost = 0;
|
|
+ public final java.util.concurrent.ConcurrentHashMap<ChunkHolder, ChunkStatus> neighbors = new java.util.concurrent.ConcurrentHashMap<>();
|
|
+ public final it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<Integer> neighborPriorities = new it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<>();
|
|
+ int requestedPriority = ChunkMap.MAX_CHUNK_DISTANCE + 1; // this priority is possible pending, but is used to ensure needless updates are not queued
|
|
+
|
|
+ private int getDemandedPriority() {
|
|
+ int priority = neighborPriority; // if we have a neighbor priority, use it
|
|
+ int myPriority = getMyPriority();
|
|
+
|
|
+ if (priority == -1 || (ticketLevel <= 33 && priority > myPriority)) {
|
|
+ priority = myPriority;
|
|
+ }
|
|
+
|
|
+ return Math.max(1, Math.min(Math.max(ticketLevel, ChunkMap.MAX_CHUNK_DISTANCE), priority));
|
|
+ }
|
|
+
|
|
+ private int getMyPriority() {
|
|
+ if (priorityBoost == DistanceManager.URGENT_PRIORITY) {
|
|
+ return 2; // Urgent - ticket level isn't always 31 so 33-30 = 3, but allow 1 more tasks to go below this for dependents
|
|
+ }
|
|
+ return ticketLevel - priorityBoost;
|
|
+ }
|
|
+
|
|
+ private int getNeighborsPriority() {
|
|
+ return (neighborPriorities.isEmpty() ? getMyPriority() : getDemandedPriority()) + 1;
|
|
+ }
|
|
+
|
|
+ public void onNeighborRequest(ChunkHolder neighbor, ChunkStatus status) {
|
|
+ neighbor.setNeighborPriority(this, getNeighborsPriority());
|
|
+ this.neighbors.compute(neighbor, (playerChunk, currentWantedStatus) -> {
|
|
+ if (currentWantedStatus == null || !currentWantedStatus.isOrAfter(status)) {
|
|
+ //System.out.println(this + " request " + neighbor + " at " + status + " currently " + currentWantedStatus);
|
|
+ return status;
|
|
+ } else {
|
|
+ //System.out.println(this + " requested " + neighbor + " at " + status + " but thats lower than other wanted status " + currentWantedStatus);
|
|
+ return currentWantedStatus;
|
|
+ }
|
|
+ });
|
|
+
|
|
+ }
|
|
+
|
|
+ public void onNeighborDone(ChunkHolder neighbor, ChunkStatus chunkstatus, ChunkAccess chunk) {
|
|
+ this.neighbors.compute(neighbor, (playerChunk, wantedStatus) -> {
|
|
+ if (wantedStatus != null && chunkstatus.isOrAfter(wantedStatus)) {
|
|
+ //System.out.println(this + " neighbor done at " + neighbor + " for status " + chunkstatus + " wanted " + wantedStatus);
|
|
+ neighbor.removeNeighborPriority(this);
|
|
+ return null;
|
|
+ } else {
|
|
+ //System.out.println(this + " neighbor finished our previous request at " + neighbor + " for status " + chunkstatus + " but we now want instead " + wantedStatus);
|
|
+ return wantedStatus;
|
|
+ }
|
|
+ });
|
|
+ }
|
|
+
|
|
+ private void removeNeighborPriority(ChunkHolder requester) {
|
|
+ synchronized (neighborPriorities) {
|
|
+ neighborPriorities.remove(requester.pos.toLong());
|
|
+ recalcNeighborPriority();
|
|
+ }
|
|
+ checkPriority();
|
|
+ }
|
|
+
|
|
+
|
|
+ private void setNeighborPriority(ChunkHolder requester, int priority) {
|
|
+ synchronized (neighborPriorities) {
|
|
+ if (!Integer.valueOf(priority).equals(neighborPriorities.put(requester.pos.toLong(), Integer.valueOf(priority)))) {
|
|
+ recalcNeighborPriority();
|
|
+ }
|
|
+ }
|
|
+ checkPriority();
|
|
+ }
|
|
+
|
|
+ private void recalcNeighborPriority() {
|
|
+ neighborPriority = -1;
|
|
+ if (!neighborPriorities.isEmpty()) {
|
|
+ synchronized (neighborPriorities) {
|
|
+ for (Integer neighbor : neighborPriorities.values()) {
|
|
+ if (neighbor < neighborPriority || neighborPriority == -1) {
|
|
+ neighborPriority = neighbor;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ private void checkPriority() {
|
|
+ if (this.requestedPriority != getDemandedPriority()) this.chunkMap.queueHolderUpdate(this);
|
|
+ }
|
|
+
|
|
+ public final double getDistance(ServerPlayer player) {
|
|
+ return getDistance(player.getX(), player.getZ());
|
|
+ }
|
|
+ public final double getDistance(double blockX, double blockZ) {
|
|
+ int cx = net.minecraft.server.MCUtil.fastFloor(blockX) >> 4;
|
|
+ int cz = net.minecraft.server.MCUtil.fastFloor(blockZ) >> 4;
|
|
+ final double x = pos.x - cx;
|
|
+ final double z = pos.z - cz;
|
|
+ return (x * x) + (z * z);
|
|
+ }
|
|
+
|
|
+ public final double getDistanceFrom(BlockPos pos) {
|
|
+ return getDistance(pos.getX(), pos.getZ());
|
|
+ }
|
|
+
|
|
+ public static ChunkStatus getNextStatus(ChunkStatus status) {
|
|
+ if (status == ChunkStatus.FULL) {
|
|
+ return status;
|
|
+ }
|
|
+ return CHUNK_STATUSES.get(status.getIndex() + 1);
|
|
+ }
|
|
+ public CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> getStatusFutureUncheckedMain(ChunkStatus chunkstatus) {
|
|
+ return ensureMain(getFutureIfPresentUnchecked(chunkstatus));
|
|
+ }
|
|
+ public <T> CompletableFuture<T> ensureMain(CompletableFuture<T> future) {
|
|
+ return future.thenApplyAsync(r -> r, chunkMap.mainInvokingExecutor);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public String toString() {
|
|
+ return "PlayerChunk{" +
|
|
+ "location=" + pos +
|
|
+ ", ticketLevel=" + ticketLevel + "/" + getStatus(this.ticketLevel) +
|
|
+ ", chunkHolderStatus=" + getChunkHolderStatus() +
|
|
+ ", neighborPriority=" + getNeighborsPriority() +
|
|
+ ", priority=(" + ticketLevel + " - " + priorityBoost +" vs N " + neighborPriority + ") = " + getDemandedPriority() + " A " + queueLevel +
|
|
+ '}';
|
|
+ }
|
|
+ // Paper end
|
|
+
|
|
// Paper start - optimise isOutsideOfRange
|
|
// cached here to avoid a map lookup
|
|
com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> playersInMobSpawnRange;
|
|
@@ -470,12 +601,18 @@ public class ChunkHolder {
|
|
});
|
|
}
|
|
|
|
+ // Paper start
|
|
+ private boolean loadCallbackScheduled = false;
|
|
+ private boolean unloadCallbackScheduled = false;
|
|
+ // Paper end
|
|
+
|
|
private void demoteFullChunk(ChunkMap playerchunkmap, ChunkHolder.FullChunkStatus playerchunk_state) {
|
|
this.pendingFullStateConfirmation.cancel(false);
|
|
playerchunkmap.onFullChunkStatusChange(this.pos, playerchunk_state);
|
|
}
|
|
|
|
protected void updateFutures(ChunkMap chunkStorage, Executor executor) {
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread("Async ticket level update"); // Paper
|
|
ChunkStatus chunkstatus = ChunkHolder.getStatus(this.oldTicketLevel);
|
|
ChunkStatus chunkstatus1 = ChunkHolder.getStatus(this.ticketLevel);
|
|
boolean flag = this.oldTicketLevel <= ChunkMap.MAX_CHUNK_DISTANCE;
|
|
@@ -486,9 +623,22 @@ public class ChunkHolder {
|
|
// ChunkUnloadEvent: Called before the chunk is unloaded: isChunkLoaded is still true and chunk can still be modified by plugins.
|
|
if (playerchunk_state.isOrAfter(ChunkHolder.FullChunkStatus.BORDER) && !playerchunk_state1.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) {
|
|
this.getFutureIfPresentUnchecked(ChunkStatus.FULL).thenAccept((either) -> {
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread("Async full status chunk future completion"); // Paper
|
|
LevelChunk chunk = (LevelChunk)either.left().orElse(null);
|
|
- if (chunk != null) {
|
|
+ if (chunk != null && chunk.wasLoadCallbackInvoked() && ChunkHolder.this.ticketLevel > 33) { // Paper - only invoke unload if load was called
|
|
+ // Paper start - only schedule once, now the future is no longer completed as RIGHT if unloaded...
|
|
+ if (ChunkHolder.this.unloadCallbackScheduled) {
|
|
+ return;
|
|
+ }
|
|
+ ChunkHolder.this.unloadCallbackScheduled = true;
|
|
+ // Paper end - only schedule once, now the future is no longer completed as RIGHT if unloaded...
|
|
chunkStorage.callbackExecutor.execute(() -> {
|
|
+ // Paper start - only schedule once, now the future is no longer completed as RIGHT if unloaded...
|
|
+ ChunkHolder.this.unloadCallbackScheduled = false;
|
|
+ if (ChunkHolder.this.ticketLevel <= 33) {
|
|
+ return;
|
|
+ }
|
|
+ // Paper end - only schedule once, now the future is no longer completed as RIGHT if unloaded...
|
|
// Minecraft will apply the chunks tick lists to the world once the chunk got loaded, and then store the tick
|
|
// lists again inside the chunk once the chunk becomes inaccessible and set the chunk's needsSaving flag.
|
|
// These actions may however happen deferred, so we manually set the needsSaving flag already here.
|
|
@@ -545,12 +695,14 @@ public class ChunkHolder {
|
|
this.scheduleFullChunkPromotion(chunkStorage, this.fullChunkFuture, executor, ChunkHolder.FullChunkStatus.BORDER);
|
|
// Paper start - cache ticking ready status
|
|
this.fullChunkFuture.thenAccept(either -> {
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread("Async full chunk future completion"); // Paper
|
|
final Optional<LevelChunk> left = either.left();
|
|
if (left.isPresent() && ChunkHolder.this.fullChunkCreateCount == expectCreateCount) {
|
|
// note: Here is a very good place to add callbacks to logic waiting on this.
|
|
LevelChunk fullChunk = either.left().get();
|
|
ChunkHolder.this.isFullChunkReady = true;
|
|
fullChunk.playerChunk = ChunkHolder.this;
|
|
+ this.chunkMap.distanceManager.clearPriorityTickets(pos);
|
|
}
|
|
});
|
|
this.updateChunkToSave(this.fullChunkFuture, "full");
|
|
@@ -575,6 +727,7 @@ public class ChunkHolder {
|
|
this.scheduleFullChunkPromotion(chunkStorage, this.tickingChunkFuture, executor, ChunkHolder.FullChunkStatus.TICKING);
|
|
// Paper start - cache ticking ready status
|
|
this.tickingChunkFuture.thenAccept(either -> {
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread("Async full chunk future completion"); // Paper
|
|
either.ifLeft(chunk -> {
|
|
// note: Here is a very good place to add callbacks to logic waiting on this.
|
|
ChunkHolder.this.isTickingReady = true;
|
|
@@ -615,6 +768,7 @@ public class ChunkHolder {
|
|
this.scheduleFullChunkPromotion(chunkStorage, this.entityTickingChunkFuture, executor, ChunkHolder.FullChunkStatus.ENTITY_TICKING);
|
|
// Paper start - cache ticking ready status
|
|
this.entityTickingChunkFuture.thenAccept(either -> {
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread("Async full chunk future completion"); // Paper
|
|
either.ifLeft(chunk -> {
|
|
ChunkHolder.this.isEntityTickingReady = true;
|
|
// Paper start - entity ticking chunk set
|
|
@@ -641,16 +795,45 @@ public class ChunkHolder {
|
|
this.demoteFullChunk(chunkStorage, playerchunk_state1);
|
|
}
|
|
|
|
- this.onLevelChange.onLevelChange(this.pos, this::getQueueLevel, this.ticketLevel, this::setQueueLevel);
|
|
+ //this.onLevelChange.onLevelChange(this.pos, this::getQueueLevel, this.ticketLevel, this::setQueueLevel);
|
|
+ // Paper start - raise IO/load priority if priority changes, use our preferred priority
|
|
+ priorityBoost = chunkMap.distanceManager.getChunkPriority(pos);
|
|
+ int currRequestedPriority = this.requestedPriority;
|
|
+ int priority = getDemandedPriority();
|
|
+ int newRequestedPriority = this.requestedPriority = priority;
|
|
+ if (this.queueLevel > priority) {
|
|
+ int ioPriority = com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY;
|
|
+ if (priority <= 10) {
|
|
+ ioPriority = com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGHEST_PRIORITY;
|
|
+ } else if (priority <= 20) {
|
|
+ ioPriority = com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGH_PRIORITY;
|
|
+ }
|
|
+ chunkMap.level.asyncChunkTaskManager.raisePriority(pos.x, pos.z, ioPriority);
|
|
+ chunkMap.level.getChunkSource().getLightEngine().queue.changePriority(pos.toLong(), this.queueLevel, priority); // Paper // Restore this in chunk priority later?
|
|
+ }
|
|
+ if (currRequestedPriority != newRequestedPriority) {
|
|
+ this.onLevelChange.onLevelChange(this.pos, () -> this.queueLevel, priority, p -> this.queueLevel = p); // use preferred priority
|
|
+ int neighborsPriority = getNeighborsPriority();
|
|
+ this.neighbors.forEach((neighbor, neighborDesired) -> neighbor.setNeighborPriority(this, neighborsPriority));
|
|
+ }
|
|
+ // Paper end
|
|
this.oldTicketLevel = this.ticketLevel;
|
|
// CraftBukkit start
|
|
// ChunkLoadEvent: Called after the chunk is loaded: isChunkLoaded returns true and chunk is ready to be modified by plugins.
|
|
if (!playerchunk_state.isOrAfter(ChunkHolder.FullChunkStatus.BORDER) && playerchunk_state1.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) {
|
|
this.getFutureIfPresentUnchecked(ChunkStatus.FULL).thenAccept((either) -> {
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread("Async full status chunk future completion"); // Paper
|
|
LevelChunk chunk = (LevelChunk)either.left().orElse(null);
|
|
- if (chunk != null) {
|
|
+ if (chunk != null && ChunkHolder.this.oldTicketLevel <= 33 && !chunk.wasLoadCallbackInvoked()) { // Paper - ensure ticket level is set to loaded before calling, as now this can complete with ticket level > 33
|
|
+ // Paper start - only schedule once, now the future is no longer completed as RIGHT if unloaded...
|
|
+ if (ChunkHolder.this.loadCallbackScheduled) {
|
|
+ return;
|
|
+ }
|
|
+ ChunkHolder.this.loadCallbackScheduled = true;
|
|
+ // Paper end - only schedule once, now the future is no longer completed as RIGHT if unloaded...
|
|
chunkStorage.callbackExecutor.execute(() -> {
|
|
- chunk.loadCallback();
|
|
+ ChunkHolder.this.loadCallbackScheduled = false; // Paper - only schedule once, now the future is no longer completed as RIGHT if unloaded...
|
|
+ if (ChunkHolder.this.oldTicketLevel <= 33) chunk.loadCallback(); // Paper "
|
|
});
|
|
}
|
|
}).exceptionally((throwable) -> {
|
|
diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
|
|
index daa13434cf4ba47f77b4f47c4d2ecb3d9d672031..ac848504fd7105506d6d733709ad05dc8b37c9d9 100644
|
|
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
|
|
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
|
|
@@ -122,6 +122,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
public final ServerLevel level;
|
|
private final ThreadedLevelLightEngine lightEngine;
|
|
private final BlockableEventLoop<Runnable> mainThreadExecutor;
|
|
+ final java.util.concurrent.Executor mainInvokingExecutor; // Paper
|
|
public final ChunkGenerator generator;
|
|
public final Supplier<DimensionDataStorage> overworldDataStorage;
|
|
private final PoiManager poiManager;
|
|
@@ -347,6 +348,15 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
this.level = world;
|
|
this.generator = chunkGenerator;
|
|
this.mainThreadExecutor = mainThreadExecutor;
|
|
+ // Paper start
|
|
+ this.mainInvokingExecutor = (run) -> {
|
|
+ if (MCUtil.isMainThread()) {
|
|
+ run.run();
|
|
+ } else {
|
|
+ mainThreadExecutor.execute(run);
|
|
+ }
|
|
+ };
|
|
+ // Paper end
|
|
ProcessorMailbox<Runnable> threadedmailbox = ProcessorMailbox.create(executor, "worldgen");
|
|
|
|
Objects.requireNonNull(mainThreadExecutor);
|
|
@@ -482,6 +492,116 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
// Paper end - optimise PlayerChunkMap#isOutsideRange
|
|
}
|
|
|
|
+ // Paper start - Chunk Prioritization
|
|
+ public void queueHolderUpdate(ChunkHolder playerchunk) {
|
|
+ Runnable runnable = () -> {
|
|
+ if (isUnloading(playerchunk)) {
|
|
+ return; // unloaded
|
|
+ }
|
|
+ distanceManager.pendingChunkUpdates.add(playerchunk);
|
|
+ if (!distanceManager.pollingPendingChunkUpdates) {
|
|
+ level.getChunkSource().runDistanceManagerUpdates();
|
|
+ }
|
|
+ };
|
|
+ if (MCUtil.isMainThread()) {
|
|
+ // We can't use executor here because it will not execute tasks if its currently in the middle of executing tasks...
|
|
+ runnable.run();
|
|
+ } else {
|
|
+ mainThreadExecutor.execute(runnable);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private boolean isUnloading(ChunkHolder playerchunk) {
|
|
+ return playerchunk == null || toDrop.contains(playerchunk.pos.toLong());
|
|
+ }
|
|
+
|
|
+ private void updateChunkPriorityMap(it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap map, long chunk, int level) {
|
|
+ int prev = map.getOrDefault(chunk, -1);
|
|
+ if (level > prev) {
|
|
+ map.put(chunk, level);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public void checkHighPriorityChunks(ServerPlayer player) {
|
|
+ int currentTick = MinecraftServer.currentTick;
|
|
+ if (currentTick - player.lastHighPriorityChecked < 20 || !player.isRealPlayer) { // weed out fake players
|
|
+ return;
|
|
+ }
|
|
+ player.lastHighPriorityChecked = currentTick;
|
|
+ it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap priorities = new it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap();
|
|
+
|
|
+ int viewDistance = getEffectiveNoTickViewDistance();
|
|
+ net.minecraft.core.BlockPos.MutableBlockPos pos = new net.minecraft.core.BlockPos.MutableBlockPos();
|
|
+
|
|
+ // Prioritize circular near
|
|
+ double playerChunkX = Mth.floor(player.getX()) >> 4;
|
|
+ double playerChunkZ = Mth.floor(player.getZ()) >> 4;
|
|
+ pos.set(player.getX(), 0, player.getZ());
|
|
+ double twoThirdModifier = 2D / 3D;
|
|
+ MCUtil.getSpiralOutChunks(pos, Math.min(6, viewDistance)).forEach(coord -> {
|
|
+ if (shouldSkipPrioritization(coord)) return;
|
|
+
|
|
+ double dist = MCUtil.distance(playerChunkX, 0, playerChunkZ, coord.x, 0, coord.z);
|
|
+ // Prioritize immediate
|
|
+ if (dist <= 4) {
|
|
+ updateChunkPriorityMap(priorities, coord.toLong(), (int) (27 - dist));
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ // Prioritize nearby chunks
|
|
+ updateChunkPriorityMap(priorities, coord.toLong(), (int) (20 - dist * twoThirdModifier));
|
|
+ });
|
|
+
|
|
+ // Prioritize Frustum near 3
|
|
+ ChunkPos front3 = player.getChunkInFront(3);
|
|
+ pos.set(front3.x << 4, 0, front3.z << 4);
|
|
+ MCUtil.getSpiralOutChunks(pos, Math.min(5, viewDistance)).forEach(coord -> {
|
|
+ if (shouldSkipPrioritization(coord)) return;
|
|
+
|
|
+ double dist = MCUtil.distance(playerChunkX, 0, playerChunkZ, coord.x, 0, coord.z);
|
|
+ updateChunkPriorityMap(priorities, coord.toLong(), (int) (25 - dist * twoThirdModifier));
|
|
+ });
|
|
+
|
|
+ // Prioritize Frustum near 5
|
|
+ if (viewDistance > 4) {
|
|
+ ChunkPos front5 = player.getChunkInFront(5);
|
|
+ pos.set(front5.x << 4, 0, front5.z << 4);
|
|
+ MCUtil.getSpiralOutChunks(pos, 4).forEach(coord -> {
|
|
+ if (shouldSkipPrioritization(coord)) return;
|
|
+
|
|
+ double dist = MCUtil.distance(playerChunkX, 0, playerChunkZ, coord.x, 0, coord.z);
|
|
+ updateChunkPriorityMap(priorities, coord.toLong(), (int) (25 - dist * twoThirdModifier));
|
|
+ });
|
|
+ }
|
|
+
|
|
+ // Prioritize Frustum far 7
|
|
+ if (viewDistance > 6) {
|
|
+ ChunkPos front7 = player.getChunkInFront(7);
|
|
+ pos.set(front7.x << 4, 0, front7.z << 4);
|
|
+ MCUtil.getSpiralOutChunks(pos, 3).forEach(coord -> {
|
|
+ if (shouldSkipPrioritization(coord)) {
|
|
+ return;
|
|
+ }
|
|
+ double dist = MCUtil.distance(playerChunkX, 0, playerChunkZ, coord.x, 0, coord.z);
|
|
+ updateChunkPriorityMap(priorities, coord.toLong(), (int) (25 - dist * twoThirdModifier));
|
|
+ });
|
|
+ }
|
|
+
|
|
+ if (priorities.isEmpty()) return;
|
|
+ distanceManager.delayDistanceManagerTick = true;
|
|
+ priorities.long2IntEntrySet().fastForEach(entry -> distanceManager.markHighPriority(new ChunkPos(entry.getLongKey()), entry.getIntValue()));
|
|
+ distanceManager.delayDistanceManagerTick = false;
|
|
+ level.getChunkSource().runDistanceManagerUpdates();
|
|
+
|
|
+ }
|
|
+
|
|
+ private boolean shouldSkipPrioritization(ChunkPos coord) {
|
|
+ if (playerViewDistanceNoTickMap.getObjectsInRange(coord.toLong()) == null) return true;
|
|
+ ChunkHolder chunk = getUpdatingChunkIfPresent(coord.toLong());
|
|
+ return chunk != null && (chunk.isFullChunkReady());
|
|
+ }
|
|
+ // Paper end
|
|
+
|
|
// Paper start
|
|
public void updatePlayerMobTypeMap(Entity entity) {
|
|
if (!this.level.paperConfig.perPlayerMobSpawns) {
|
|
@@ -597,6 +717,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
List<CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>>> list = Lists.newArrayList();
|
|
int j = centerChunk.x;
|
|
int k = centerChunk.z;
|
|
+ ChunkHolder requestingNeighbor = getUpdatingChunkIfPresent(centerChunk.toLong()); // Paper
|
|
|
|
for (int l = -margin; l <= margin; ++l) {
|
|
for (int i1 = -margin; i1 <= margin; ++i1) {
|
|
@@ -615,6 +736,14 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
|
|
ChunkStatus chunkstatus = (ChunkStatus) distanceToStatus.apply(j1);
|
|
CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> completablefuture = playerchunk.getOrScheduleFuture(chunkstatus, this);
|
|
+ // Paper start
|
|
+ if (requestingNeighbor != null && requestingNeighbor != playerchunk && !completablefuture.isDone()) {
|
|
+ requestingNeighbor.onNeighborRequest(playerchunk, chunkstatus);
|
|
+ completablefuture.thenAccept(either -> {
|
|
+ requestingNeighbor.onNeighborDone(playerchunk, chunkstatus, either.left().orElse(null));
|
|
+ });
|
|
+ }
|
|
+ // Paper end
|
|
|
|
list.add(completablefuture);
|
|
}
|
|
@@ -984,11 +1113,19 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
if (requiredStatus == ChunkStatus.EMPTY) {
|
|
return this.scheduleChunkLoad(chunkcoordintpair);
|
|
} else {
|
|
+ // Paper start - revert 1.17 chunk system changes
|
|
+ CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> future = holder.getOrScheduleFuture(requiredStatus.getParent(), this);
|
|
+ return future.thenComposeAsync((either) -> {
|
|
+ Optional<ChunkAccess> optional = either.left();
|
|
+ if (!optional.isPresent()) {
|
|
+ return CompletableFuture.completedFuture(either);
|
|
+ }
|
|
+ // Paper end - revert 1.17 chunk system changes
|
|
if (requiredStatus == ChunkStatus.LIGHT) {
|
|
this.distanceManager.addTicket(TicketType.LIGHT, chunkcoordintpair, 33 + ChunkStatus.getDistance(ChunkStatus.LIGHT), chunkcoordintpair);
|
|
}
|
|
|
|
- Optional<ChunkAccess> optional = ((Either) holder.getOrScheduleFuture(requiredStatus.getParent(), this).getNow(ChunkHolder.UNLOADED_CHUNK)).left();
|
|
+ // Paper - revert 1.17 chunk system changes
|
|
|
|
if (optional.isPresent() && ((ChunkAccess) optional.get()).getStatus().isOrAfter(requiredStatus)) {
|
|
CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> completablefuture = requiredStatus.load(this.level, this.structureManager, this.lightEngine, (ichunkaccess) -> {
|
|
@@ -1000,6 +1137,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
} else {
|
|
return this.scheduleChunkGeneration(holder, requiredStatus);
|
|
}
|
|
+ }, this.mainThreadExecutor).thenComposeAsync(CompletableFuture::completedFuture, this.mainThreadExecutor); // Paper - revert 1.17 chunk system changes
|
|
}
|
|
}
|
|
|
|
@@ -1059,14 +1197,24 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
};
|
|
|
|
CompletableFuture<CompoundTag> chunkSaveFuture = this.level.asyncChunkTaskManager.getChunkSaveFuture(pos.x, pos.z);
|
|
+ // Paper start
|
|
+ ChunkHolder playerChunk = getUpdatingChunkIfPresent(pos.toLong());
|
|
+ int chunkPriority = playerChunk != null ? playerChunk.requestedPriority : 33;
|
|
+ int priority = com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY;
|
|
+
|
|
+ if (chunkPriority <= 10) {
|
|
+ priority = com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGHEST_PRIORITY;
|
|
+ } else if (chunkPriority <= 20) {
|
|
+ priority = com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGH_PRIORITY;
|
|
+ }
|
|
+ boolean isHighestPriority = priority == com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGHEST_PRIORITY;
|
|
+ // Paper end
|
|
if (chunkSaveFuture != null) {
|
|
- this.level.asyncChunkTaskManager.scheduleChunkLoad(pos.x, pos.z,
|
|
- com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGH_PRIORITY, chunkHolderConsumer, false, chunkSaveFuture);
|
|
- this.level.asyncChunkTaskManager.raisePriority(pos.x, pos.z, com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGH_PRIORITY);
|
|
+ this.level.asyncChunkTaskManager.scheduleChunkLoad(pos.x, pos.z, priority, chunkHolderConsumer, isHighestPriority, chunkSaveFuture); // Paper
|
|
} else {
|
|
- this.level.asyncChunkTaskManager.scheduleChunkLoad(pos.x, pos.z,
|
|
- com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY, chunkHolderConsumer, false);
|
|
+ this.level.asyncChunkTaskManager.scheduleChunkLoad(pos.x, pos.z, priority, chunkHolderConsumer, isHighestPriority); // Paper
|
|
}
|
|
+ this.level.asyncChunkTaskManager.raisePriority(pos.x, pos.z, priority); // Paper
|
|
return ret;
|
|
// Paper end
|
|
}
|
|
@@ -1115,7 +1263,10 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
this.releaseLightTicket(chunkcoordintpair);
|
|
return CompletableFuture.completedFuture(Either.right(playerchunk_failure));
|
|
});
|
|
- }, executor);
|
|
+ }, executor).thenComposeAsync((either) -> { // Paper start - force competion on the main thread
|
|
+ return CompletableFuture.completedFuture(either);
|
|
+ }, this.mainThreadExecutor); // use the main executor, we want to ensure only one chunk callback can be completed per runnable execute
|
|
+ // Paper end - force competion on the main thread
|
|
}
|
|
|
|
protected void releaseLightTicket(ChunkPos pos) {
|
|
@@ -1198,7 +1349,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
long i = playerchunk.getPos().toLong();
|
|
|
|
Objects.requireNonNull(playerchunk);
|
|
- mailbox.tell(ChunkTaskPriorityQueueSorter.message(runnable, i, playerchunk::getTicketLevel));
|
|
+ mailbox.tell(ChunkTaskPriorityQueueSorter.message(runnable, i, () -> 1)); // Paper - final loads are always urgent!
|
|
});
|
|
}
|
|
|
|
diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java
|
|
index d94241bcca4f2fd5e464a860bd356af504dc68b7..1cc4e0a1f3d8235ef88b48e01ca8b78a263d2676 100644
|
|
--- a/src/main/java/net/minecraft/server/level/DistanceManager.java
|
|
+++ b/src/main/java/net/minecraft/server/level/DistanceManager.java
|
|
@@ -98,6 +98,7 @@ public abstract class DistanceManager {
|
|
}
|
|
|
|
private static int getTicketLevelAt(SortedArraySet<Ticket<?>> arraysetsorted) {
|
|
+ org.spigotmc.AsyncCatcher.catchOp("ChunkMapDistance::getLowestTicketLevel"); // Paper
|
|
return !arraysetsorted.isEmpty() ? ((Ticket) arraysetsorted.first()).getTicketLevel() : ChunkMap.MAX_CHUNK_DISTANCE + 1;
|
|
}
|
|
|
|
@@ -111,6 +112,7 @@ public abstract class DistanceManager {
|
|
|
|
public boolean runAllUpdates(ChunkMap playerchunkmap) {
|
|
//this.f.a(); // Paper - no longer used
|
|
+ org.spigotmc.AsyncCatcher.catchOp("DistanceManagerTick"); // Paper
|
|
this.playerTicketManager.runAllUpdates();
|
|
int i = Integer.MAX_VALUE - this.ticketTracker.runDistanceUpdates(Integer.MAX_VALUE);
|
|
boolean flag = i != 0;
|
|
@@ -121,11 +123,13 @@ public abstract class DistanceManager {
|
|
|
|
// Paper start
|
|
if (!this.pendingChunkUpdates.isEmpty()) {
|
|
+ this.pollingPendingChunkUpdates = true; try { // Paper - Chunk priority
|
|
while(!this.pendingChunkUpdates.isEmpty()) {
|
|
ChunkHolder remove = this.pendingChunkUpdates.remove();
|
|
remove.isUpdateQueued = false;
|
|
remove.updateFutures(playerchunkmap, this.mainThreadExecutor);
|
|
}
|
|
+ } finally { this.pollingPendingChunkUpdates = false; } // Paper - Chunk priority
|
|
// Paper end
|
|
return true;
|
|
} else {
|
|
@@ -161,8 +165,10 @@ public abstract class DistanceManager {
|
|
return flag;
|
|
}
|
|
}
|
|
+ boolean pollingPendingChunkUpdates = false; // Paper - Chunk priority
|
|
|
|
boolean addTicket(long i, Ticket<?> ticket) { // CraftBukkit - void -> boolean
|
|
+ org.spigotmc.AsyncCatcher.catchOp("ChunkMapDistance::addTicket"); // Paper
|
|
SortedArraySet<Ticket<?>> arraysetsorted = this.getTickets(i);
|
|
int j = DistanceManager.getTicketLevelAt(arraysetsorted);
|
|
Ticket<?> ticket1 = (Ticket) arraysetsorted.addOrGet(ticket); // CraftBukkit - decompile error
|
|
@@ -176,7 +182,9 @@ public abstract class DistanceManager {
|
|
}
|
|
|
|
boolean removeTicket(long i, Ticket<?> ticket) { // CraftBukkit - void -> boolean
|
|
+ org.spigotmc.AsyncCatcher.catchOp("ChunkMapDistance::removeTicket"); // Paper
|
|
SortedArraySet<Ticket<?>> arraysetsorted = this.getTickets(i);
|
|
+ int oldLevel = getTicketLevelAt(arraysetsorted); // Paper
|
|
|
|
boolean removed = false; // CraftBukkit
|
|
if (arraysetsorted.remove(ticket)) {
|
|
@@ -208,7 +216,12 @@ public abstract class DistanceManager {
|
|
this.tickets.remove(i);
|
|
}
|
|
|
|
- this.ticketTracker.update(i, DistanceManager.getTicketLevelAt(arraysetsorted), false);
|
|
+ // Paper start - Chunk priority
|
|
+ int newLevel = getTicketLevelAt(arraysetsorted);
|
|
+ if (newLevel > oldLevel) {
|
|
+ this.ticketTracker.update(i, newLevel, false);
|
|
+ }
|
|
+ // Paper end
|
|
return removed; // CraftBukkit
|
|
}
|
|
|
|
@@ -250,6 +263,136 @@ public abstract class DistanceManager {
|
|
});
|
|
}
|
|
|
|
+ // Paper start - Chunk priority
|
|
+ public static final int PRIORITY_TICKET_LEVEL = ChunkMap.MAX_CHUNK_DISTANCE;
|
|
+ public static final int URGENT_PRIORITY = 29;
|
|
+ public boolean delayDistanceManagerTick = false;
|
|
+ public boolean markUrgent(ChunkPos coords) {
|
|
+ return addPriorityTicket(coords, TicketType.URGENT, URGENT_PRIORITY);
|
|
+ }
|
|
+ public boolean markHighPriority(ChunkPos coords, int priority) {
|
|
+ priority = Math.min(URGENT_PRIORITY - 1, Math.max(1, priority));
|
|
+ return addPriorityTicket(coords, TicketType.PRIORITY, priority);
|
|
+ }
|
|
+
|
|
+ public void markAreaHighPriority(ChunkPos center, int priority, int radius) {
|
|
+ delayDistanceManagerTick = true;
|
|
+ priority = Math.min(URGENT_PRIORITY - 1, Math.max(1, priority));
|
|
+ int finalPriority = priority;
|
|
+ net.minecraft.server.MCUtil.getSpiralOutChunks(center.getWorldPosition(), radius).forEach(coords -> {
|
|
+ addPriorityTicket(coords, TicketType.PRIORITY, finalPriority);
|
|
+ });
|
|
+ delayDistanceManagerTick = false;
|
|
+ chunkMap.level.getChunkSource().runDistanceManagerUpdates();
|
|
+ }
|
|
+
|
|
+ public void clearAreaPriorityTickets(ChunkPos center, int radius) {
|
|
+ delayDistanceManagerTick = true;
|
|
+ net.minecraft.server.MCUtil.getSpiralOutChunks(center.getWorldPosition(), radius).forEach(coords -> {
|
|
+ this.removeTicket(coords.toLong(), new Ticket<ChunkPos>(TicketType.PRIORITY, PRIORITY_TICKET_LEVEL, coords));
|
|
+ });
|
|
+ delayDistanceManagerTick = false;
|
|
+ chunkMap.level.getChunkSource().runDistanceManagerUpdates();
|
|
+ }
|
|
+
|
|
+ private boolean hasPlayerTicket(ChunkPos coords, int level) {
|
|
+ SortedArraySet<Ticket<?>> tickets = this.tickets.get(coords.toLong());
|
|
+ if (tickets == null || tickets.isEmpty()) {
|
|
+ return false;
|
|
+ }
|
|
+ for (Ticket<?> ticket : tickets) {
|
|
+ if (ticket.getType() == TicketType.PLAYER && ticket.getTicketLevel() == level) {
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ private boolean addPriorityTicket(ChunkPos coords, TicketType<ChunkPos> ticketType, int priority) {
|
|
+ org.spigotmc.AsyncCatcher.catchOp("ChunkMapDistance::addPriorityTicket");
|
|
+ long pair = coords.toLong();
|
|
+ ChunkHolder chunk = chunkMap.getUpdatingChunkIfPresent(pair);
|
|
+ boolean needsTicket = chunkMap.playerViewDistanceNoTickMap.getObjectsInRange(pair) != null && !hasPlayerTicket(coords, 33);
|
|
+
|
|
+ if (needsTicket) {
|
|
+ Ticket<?> ticket = new Ticket<>(TicketType.PLAYER, 33, coords);
|
|
+ this.ticketsToRelease.add(pair);
|
|
+ addTicket(pair, ticket);
|
|
+ }
|
|
+ if ((chunk != null && chunk.isFullChunkReady())) {
|
|
+ if (needsTicket) {
|
|
+ chunkMap.level.getChunkSource().runDistanceManagerUpdates();
|
|
+ }
|
|
+ return needsTicket;
|
|
+ }
|
|
+
|
|
+ boolean success;
|
|
+ if (!(success = updatePriorityTicket(coords, ticketType, priority))) {
|
|
+ Ticket<ChunkPos> ticket = new Ticket<ChunkPos>(ticketType, PRIORITY_TICKET_LEVEL, coords);
|
|
+ ticket.priority = priority;
|
|
+ success = this.addTicket(pair, ticket);
|
|
+ } else {
|
|
+ if (chunk == null) {
|
|
+ chunk = chunkMap.getUpdatingChunkIfPresent(pair);
|
|
+ }
|
|
+ chunkMap.queueHolderUpdate(chunk);
|
|
+ }
|
|
+
|
|
+ //chunkMap.world.getWorld().spawnParticle(priority <= 15 ? org.bukkit.Particle.EXPLOSION_HUGE : org.bukkit.Particle.EXPLOSION_NORMAL, chunkMap.world.getWorld().getPlayers(), null, coords.x << 4, 70, coords.z << 4, 2, 0, 0, 0, 1, null, true);
|
|
+
|
|
+ chunkMap.level.getChunkSource().runDistanceManagerUpdates();
|
|
+
|
|
+ return success;
|
|
+ }
|
|
+
|
|
+ private boolean updatePriorityTicket(ChunkPos coords, TicketType<ChunkPos> type, int priority) {
|
|
+ SortedArraySet<Ticket<?>> tickets = this.tickets.get(coords.toLong());
|
|
+ if (tickets == null) {
|
|
+ return false;
|
|
+ }
|
|
+ for (Ticket<?> ticket : tickets) {
|
|
+ if (ticket.getType() == type) {
|
|
+ // We only support increasing, not decreasing, too complicated
|
|
+ ticket.setCreatedTick(this.ticketTickCounter);
|
|
+ ticket.priority = Math.max(ticket.priority, priority);
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ public int getChunkPriority(ChunkPos coords) {
|
|
+ org.spigotmc.AsyncCatcher.catchOp("ChunkMapDistance::getChunkPriority");
|
|
+ SortedArraySet<Ticket<?>> tickets = this.tickets.get(coords.toLong());
|
|
+ if (tickets == null) {
|
|
+ return 0;
|
|
+ }
|
|
+ for (Ticket<?> ticket : tickets) {
|
|
+ if (ticket.getType() == TicketType.URGENT) {
|
|
+ return URGENT_PRIORITY;
|
|
+ }
|
|
+ }
|
|
+ for (Ticket<?> ticket : tickets) {
|
|
+ if (ticket.getType() == TicketType.PRIORITY && ticket.priority > 0) {
|
|
+ return ticket.priority;
|
|
+ }
|
|
+ }
|
|
+ return 0;
|
|
+ }
|
|
+
|
|
+ public void clearPriorityTickets(ChunkPos coords) {
|
|
+ org.spigotmc.AsyncCatcher.catchOp("ChunkMapDistance::clearPriority");
|
|
+ this.removeTicket(coords.toLong(), new Ticket<ChunkPos>(TicketType.PRIORITY, PRIORITY_TICKET_LEVEL, coords));
|
|
+ }
|
|
+
|
|
+ public void clearUrgent(ChunkPos coords) {
|
|
+ org.spigotmc.AsyncCatcher.catchOp("ChunkMapDistance::clearUrgent");
|
|
+ this.removeTicket(coords.toLong(), new Ticket<ChunkPos>(TicketType.URGENT, PRIORITY_TICKET_LEVEL, coords));
|
|
+ }
|
|
+ // Paper end
|
|
+
|
|
protected void updateChunkForced(ChunkPos pos, boolean forced) {
|
|
Ticket<ChunkPos> ticket = new Ticket<>(TicketType.FORCED, 31, pos);
|
|
|
|
@@ -516,41 +659,68 @@ public abstract class DistanceManager {
|
|
|
|
public void updateViewDistance(int watchDistance) {
|
|
ObjectIterator objectiterator = this.chunks.long2ByteEntrySet().iterator();
|
|
+ // Paper start - set the view distance before scheduling chunk loads/unloads
|
|
+ int lastViewDistance = this.viewDistance;
|
|
+ this.viewDistance = watchDistance;
|
|
+ // Paper end
|
|
|
|
while (objectiterator.hasNext()) {
|
|
it.unimi.dsi.fastutil.longs.Long2ByteMap.Entry it_unimi_dsi_fastutil_longs_long2bytemap_entry = (it.unimi.dsi.fastutil.longs.Long2ByteMap.Entry) objectiterator.next();
|
|
byte b0 = it_unimi_dsi_fastutil_longs_long2bytemap_entry.getByteValue();
|
|
long j = it_unimi_dsi_fastutil_longs_long2bytemap_entry.getLongKey();
|
|
|
|
- this.onLevelChange(j, b0, this.haveTicketFor(b0), b0 <= watchDistance - 2);
|
|
+ this.onLevelChange(j, b0, b0 <= lastViewDistance - 2, this.haveTicketFor(b0)); // Paper
|
|
}
|
|
|
|
- this.viewDistance = watchDistance;
|
|
+ // this.viewDistance = watchDistance; // Paper - view distance is now set further up
|
|
}
|
|
|
|
private void onLevelChange(long pos, int distance, boolean oldWithinViewDistance, boolean withinViewDistance) {
|
|
if (oldWithinViewDistance != withinViewDistance) {
|
|
- Ticket<?> ticket = new Ticket<>(TicketType.PLAYER, 33, new ChunkPos(pos)); // Paper - no-tick view distance
|
|
+ ChunkPos coords = new ChunkPos(pos); // Paper - reuse variable
|
|
+ Ticket<?> ticket = new Ticket<>(TicketType.PLAYER, 33, coords); // Paper - no-tick view distance
|
|
|
|
if (withinViewDistance) {
|
|
+ scheduleChunkLoad(pos, net.minecraft.server.MinecraftServer.currentTick, distance, (priority) -> { // Paper - smarter ticket delay based on frustum and distance
|
|
+ // Paper start - recheck its still valid if not cancel
|
|
+ if (!isChunkInRange(pos)) {
|
|
+ DistanceManager.this.ticketThrottlerReleaser.tell(ChunkTaskPriorityQueueSorter.release(() -> {
|
|
+ DistanceManager.this.mainThreadExecutor.execute(() -> {
|
|
+ DistanceManager.this.removeTicket(pos, ticket);
|
|
+ DistanceManager.this.clearPriorityTickets(coords);
|
|
+ });
|
|
+ }, pos, false));
|
|
+ return;
|
|
+ }
|
|
+ // abort early if we got a ticket already
|
|
+ if (hasPlayerTicket(coords, 33)) return;
|
|
+ // skip player ticket throttle for near chunks
|
|
+ if (priority <= 3) {
|
|
+ DistanceManager.this.addTicket(pos, ticket);
|
|
+ DistanceManager.this.ticketsToRelease.add(pos);
|
|
+ return;
|
|
+ }
|
|
+ // Paper end
|
|
DistanceManager.this.ticketThrottlerInput.tell(ChunkTaskPriorityQueueSorter.message(() -> {
|
|
DistanceManager.this.mainThreadExecutor.execute(() -> {
|
|
- if (this.haveTicketFor(this.getLevel(pos))) {
|
|
+ if (isChunkInRange(pos)) { if (!hasPlayerTicket(coords, 33)) { // Paper - high priority might of already added it
|
|
DistanceManager.this.addTicket(pos, ticket);
|
|
DistanceManager.this.ticketsToRelease.add(pos);
|
|
- } else {
|
|
+ }} else { // Paper
|
|
DistanceManager.this.ticketThrottlerReleaser.tell(ChunkTaskPriorityQueueSorter.release(() -> {
|
|
}, pos, false));
|
|
}
|
|
|
|
});
|
|
}, pos, () -> {
|
|
- return distance;
|
|
+ return Math.min(ChunkMap.MAX_CHUNK_DISTANCE, priority); // Paper - Chunk priority
|
|
}));
|
|
+ }); // Paper
|
|
} else {
|
|
DistanceManager.this.ticketThrottlerReleaser.tell(ChunkTaskPriorityQueueSorter.release(() -> {
|
|
DistanceManager.this.mainThreadExecutor.execute(() -> {
|
|
DistanceManager.this.removeTicket(pos, ticket);
|
|
+ DistanceManager.this.clearPriorityTickets(coords); // Paper - Chunk priority
|
|
});
|
|
}, pos, true));
|
|
}
|
|
@@ -592,5 +762,100 @@ public abstract class DistanceManager {
|
|
private boolean haveTicketFor(int distance) {
|
|
return distance <= this.viewDistance - 2;
|
|
}
|
|
+
|
|
+ // Paper start - smart scheduling of player tickets
|
|
+ private boolean isChunkInRange(long i) {
|
|
+ return this.haveTicketFor(this.getLevel(i));
|
|
+ }
|
|
+ public void scheduleChunkLoad(long i, long startTick, int initialDistance, java.util.function.Consumer<Integer> task) {
|
|
+ long elapsed = net.minecraft.server.MinecraftServer.currentTick - startTick;
|
|
+ ChunkPos chunkPos = new ChunkPos(i);
|
|
+ ChunkHolder updatingChunk = chunkMap.getUpdatingChunkIfPresent(i);
|
|
+ if ((updatingChunk != null && updatingChunk.isFullChunkReady()) || !isChunkInRange(i) || getChunkPriority(chunkPos) > 0) { // Copied from above
|
|
+ // no longer needed
|
|
+ task.accept(1);
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ int desireDelay = 0;
|
|
+ double minDist = Double.MAX_VALUE;
|
|
+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> players = chunkMap.playerViewDistanceNoTickMap.getObjectsInRange(i);
|
|
+ if (elapsed == 0 && initialDistance <= 4) {
|
|
+ // Aim for no delay on initial 6 chunk radius tickets save on performance of the below code to only > 6
|
|
+ minDist = initialDistance;
|
|
+ } else if (players != null) {
|
|
+ Object[] backingSet = players.getBackingSet();
|
|
+
|
|
+ net.minecraft.core.BlockPos blockPos = chunkPos.getWorldPosition();
|
|
+
|
|
+ boolean isFront = false;
|
|
+ net.minecraft.core.BlockPos.MutableBlockPos pos = new net.minecraft.core.BlockPos.MutableBlockPos();
|
|
+ for (int index = 0, len = backingSet.length; index < len; ++index) {
|
|
+ if (!(backingSet[index] instanceof ServerPlayer)) {
|
|
+ continue;
|
|
+ }
|
|
+ ServerPlayer player = (ServerPlayer) backingSet[index];
|
|
+
|
|
+ ChunkPos pointInFront = player.getChunkInFront(5);
|
|
+ pos.set(pointInFront.x << 4, 0, pointInFront.z << 4);
|
|
+ double frontDist = net.minecraft.server.MCUtil.distanceSq(pos, blockPos);
|
|
+
|
|
+ pos.set(player.getX(), 0, player.getZ());
|
|
+ double center = net.minecraft.server.MCUtil.distanceSq(pos, blockPos);
|
|
+
|
|
+ double dist = Math.min(frontDist, center);
|
|
+ if (!isFront) {
|
|
+ ChunkPos pointInBack = player.getChunkInFront(-7);
|
|
+ pos.set(pointInBack.x << 4, 0, pointInBack.z << 4);
|
|
+ double backDist = net.minecraft.server.MCUtil.distanceSq(pos, blockPos);
|
|
+ if (frontDist < backDist) {
|
|
+ isFront = true;
|
|
+ }
|
|
+ }
|
|
+ if (dist < minDist) {
|
|
+ minDist = dist;
|
|
+ }
|
|
+ }
|
|
+ if (minDist == Double.MAX_VALUE) {
|
|
+ minDist = 15;
|
|
+ } else {
|
|
+ minDist = Math.sqrt(minDist) / 16;
|
|
+ }
|
|
+ if (minDist > 4) {
|
|
+ int desiredTimeDelayMax = isFront ?
|
|
+ (minDist < 10 ? 7 : 15) : // Front
|
|
+ (minDist < 10 ? 15 : 45); // Back
|
|
+ desireDelay += (desiredTimeDelayMax * 20) * (minDist / 32);
|
|
+ }
|
|
+ } else {
|
|
+ minDist = initialDistance;
|
|
+ desireDelay = 1;
|
|
+ }
|
|
+ long delay = desireDelay - elapsed;
|
|
+ if (delay <= 0 && minDist > 4 && minDist < Double.MAX_VALUE) {
|
|
+ boolean hasAnyNeighbor = false;
|
|
+ for (int x = -1; x <= 1; x++) {
|
|
+ for (int z = -1; z <= 1; z++) {
|
|
+ if (x == 0 && z == 0) continue;
|
|
+ long pair = ChunkPos.asLong(chunkPos.x + x, chunkPos.z + z);
|
|
+ ChunkHolder neighbor = chunkMap.getUpdatingChunkIfPresent(pair);
|
|
+ ChunkStatus current = neighbor != null ? neighbor.getChunkHolderStatus() : null;
|
|
+ if (current != null && current.isOrAfter(ChunkStatus.LIGHT)) {
|
|
+ hasAnyNeighbor = true;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ if (!hasAnyNeighbor) {
|
|
+ delay += 20;
|
|
+ }
|
|
+ }
|
|
+ if (delay <= 0) {
|
|
+ task.accept((int) minDist);
|
|
+ } else {
|
|
+ int taskDelay = (int) Math.min(delay, minDist >= 10 ? 40 : (minDist < 6 ? 5 : 20));
|
|
+ net.minecraft.server.MCUtil.scheduleTask(taskDelay, () -> scheduleChunkLoad(i, startTick, initialDistance, task), "Player Ticket Delayer");
|
|
+ }
|
|
+ }
|
|
+ // Paper end
|
|
}
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
|
|
index e5317a994cb9b30293ad54b8fc537f703ef994dc..eb6a802ea12a19a058fb7e23b7418b8b426b4ca0 100644
|
|
--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
|
|
+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
|
|
@@ -610,6 +610,26 @@ public class ServerChunkCache extends ChunkSource {
|
|
public <T> void removeTicketAtLevel(TicketType<T> ticketType, ChunkPos chunkPos, int ticketLevel, T identifier) {
|
|
this.distanceManager.removeTicketAtLevel(ticketType, chunkPos, ticketLevel, identifier);
|
|
}
|
|
+
|
|
+ public boolean markUrgent(ChunkPos coords) {
|
|
+ return this.distanceManager.markUrgent(coords);
|
|
+ }
|
|
+
|
|
+ public boolean markHighPriority(ChunkPos coords, int priority) {
|
|
+ return this.distanceManager.markHighPriority(coords, priority);
|
|
+ }
|
|
+
|
|
+ public void markAreaHighPriority(ChunkPos center, int priority, int radius) {
|
|
+ this.distanceManager.markAreaHighPriority(center, priority, radius);
|
|
+ }
|
|
+
|
|
+ public void clearAreaPriorityTickets(ChunkPos center, int radius) {
|
|
+ this.distanceManager.clearAreaPriorityTickets(center, radius);
|
|
+ }
|
|
+
|
|
+ public void clearPriorityTickets(ChunkPos coords) {
|
|
+ this.distanceManager.clearPriorityTickets(coords);
|
|
+ }
|
|
// Paper end - async chunk io
|
|
|
|
@Nullable
|
|
@@ -650,6 +670,8 @@ public class ServerChunkCache extends ChunkSource {
|
|
Objects.requireNonNull(completablefuture);
|
|
if (!completablefuture.isDone()) { // Paper
|
|
// Paper start - async chunk io/loading
|
|
+ ChunkPos pair = new ChunkPos(x1, z1); // Paper - Chunk priority
|
|
+ this.distanceManager.markUrgent(pair); // Paper - Chunk priority
|
|
this.level.asyncChunkTaskManager.raisePriority(x1, z1, com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGHEST_PRIORITY);
|
|
com.destroystokyo.paper.io.chunk.ChunkTaskManager.pushChunkWait(this.level, x1, z1);
|
|
// Paper end
|
|
@@ -658,6 +680,8 @@ public class ServerChunkCache extends ChunkSource {
|
|
chunkproviderserver_a.managedBlock(completablefuture::isDone);
|
|
com.destroystokyo.paper.io.chunk.ChunkTaskManager.popChunkWait(); // Paper - async chunk debug
|
|
this.level.timings.syncChunkLoad.stopTiming(); // Paper
|
|
+ this.distanceManager.clearPriorityTickets(pair); // Paper - Chunk priority
|
|
+ this.distanceManager.clearUrgent(pair); // Paper - Chunk priority
|
|
} // Paper
|
|
ichunkaccess = (ChunkAccess) ((Either) completablefuture.join()).map((ichunkaccess1) -> {
|
|
return ichunkaccess1;
|
|
@@ -731,10 +755,12 @@ public class ServerChunkCache extends ChunkSource {
|
|
if (flag && !currentlyUnloading) {
|
|
// CraftBukkit end
|
|
this.distanceManager.addTicket(TicketType.UNKNOWN, chunkcoordintpair, l, chunkcoordintpair);
|
|
+ if (isUrgent) this.distanceManager.markUrgent(chunkcoordintpair); // Paper - Chunk priority
|
|
if (this.chunkAbsent(playerchunk, l)) {
|
|
ProfilerFiller gameprofilerfiller = this.level.getProfiler();
|
|
|
|
gameprofilerfiller.push("chunkLoad");
|
|
+ distanceManager.delayDistanceManagerTick = false; // Paper - Chunk priority - ensure this is never false
|
|
this.runDistanceManagerUpdates();
|
|
playerchunk = this.getVisibleChunkIfPresent(k);
|
|
gameprofilerfiller.pop();
|
|
@@ -743,8 +769,13 @@ public class ServerChunkCache extends ChunkSource {
|
|
}
|
|
}
|
|
}
|
|
-
|
|
- return this.chunkAbsent(playerchunk, l) ? ChunkHolder.UNLOADED_CHUNK_FUTURE : playerchunk.getOrScheduleFuture(chunkstatus, this.chunkMap);
|
|
+ // Paper start - Chunk priority
|
|
+ CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> future = this.chunkAbsent(playerchunk, l) ? ChunkHolder.UNLOADED_CHUNK_FUTURE : playerchunk.getOrScheduleFuture(chunkstatus, this.chunkMap);
|
|
+ if (isUrgent) {
|
|
+ future.thenAccept(either -> this.distanceManager.clearUrgent(chunkcoordintpair));
|
|
+ }
|
|
+ return future;
|
|
+ // Paper end
|
|
}
|
|
|
|
private boolean chunkAbsent(@Nullable ChunkHolder holder, int maxLevel) {
|
|
@@ -796,6 +827,7 @@ public class ServerChunkCache extends ChunkSource {
|
|
}
|
|
|
|
public boolean runDistanceManagerUpdates() {
|
|
+ if (distanceManager.delayDistanceManagerTick) return false; // Paper - Chunk priority
|
|
boolean flag = this.distanceManager.runAllUpdates(this.chunkMap);
|
|
boolean flag1 = this.chunkMap.promoteChunkMap();
|
|
|
|
diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java
|
|
index eb392fd936f0951c12d9ecfc3c557de14d112e8e..f1cd338942d709e3d2d8de40241fe6e14bcba062 100644
|
|
--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
|
|
+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
|
|
@@ -189,6 +189,14 @@ public class ServerPlayer extends Player {
|
|
private int lastRecordedArmor = Integer.MIN_VALUE;
|
|
private int lastRecordedLevel = Integer.MIN_VALUE;
|
|
private int lastRecordedExperience = Integer.MIN_VALUE;
|
|
+ // Paper start - Chunk priority
|
|
+ public long lastHighPriorityChecked;
|
|
+ public void forceCheckHighPriority() {
|
|
+ lastHighPriorityChecked = -1;
|
|
+ getLevel().getChunkSource().chunkMap.checkHighPriorityChunks(this);
|
|
+ }
|
|
+ public boolean isRealPlayer;
|
|
+ // Paper end
|
|
private float lastSentHealth = -1.0E8F;
|
|
private int lastSentFood = -99999999;
|
|
private boolean lastFoodSaturationZero = true;
|
|
@@ -325,6 +333,21 @@ public class ServerPlayer extends Player {
|
|
this.maxHealthCache = this.getMaxHealth();
|
|
this.cachedSingleMobDistanceMap = new com.destroystokyo.paper.util.PooledHashSets.PooledObjectLinkedOpenHashSet<>(this); // Paper
|
|
}
|
|
+ // Paper start - Chunk priority
|
|
+ public BlockPos getPointInFront(double inFront) {
|
|
+ double rads = Math.toRadians(net.minecraft.server.MCUtil.normalizeYaw(this.yRot + 90)); // MC rotates yaw 90 for some odd reason
|
|
+ final double x = getX() + inFront * Math.cos(rads);
|
|
+ final double z = getZ() + inFront * Math.sin(rads);
|
|
+ return new BlockPos(x, getY(), z);
|
|
+ }
|
|
+
|
|
+ public ChunkPos getChunkInFront(double inFront) {
|
|
+ double rads = Math.toRadians(net.minecraft.server.MCUtil.normalizeYaw(this.yRot + 90)); // MC rotates yaw 90 for some odd reason
|
|
+ final double x = getX() + (inFront * 16) * Math.cos(rads);
|
|
+ final double z = getZ() + (inFront * 16) * Math.sin(rads);
|
|
+ return new ChunkPos(Mth.floor(x) >> 4, Mth.floor(z) >> 4);
|
|
+ }
|
|
+ // Paper end
|
|
|
|
// Yes, this doesn't match Vanilla, but it's the best we can do for now.
|
|
// If this is an issue, PRs are welcome
|
|
@@ -645,6 +668,7 @@ public class ServerPlayer extends Player {
|
|
if (valid && !this.isSpectator() || !this.touchingUnloadedChunk()) { // Paper - don't tick dead players that are not in the world currently (pending respawn)
|
|
super.tick();
|
|
}
|
|
+ if (valid && isAlive() && connection != null) ((ServerLevel)level).getChunkSource().chunkMap.checkHighPriorityChunks(this); // Paper - Chunk priority
|
|
|
|
for (int i = 0; i < this.getInventory().getContainerSize(); ++i) {
|
|
ItemStack itemstack = this.getInventory().getItem(i);
|
|
diff --git a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java
|
|
index 2c4aa4b66d83b6e7a104479860b7982629c63c3b..833b6b2193cf08e123aabb344f2283730aed1bcd 100644
|
|
--- a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java
|
|
+++ b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java
|
|
@@ -1,6 +1,7 @@
|
|
package net.minecraft.server.level;
|
|
|
|
import com.mojang.datafixers.util.Pair;
|
|
+import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; // Paper
|
|
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
|
import it.unimi.dsi.fastutil.objects.ObjectList;
|
|
import it.unimi.dsi.fastutil.objects.ObjectListIterator;
|
|
@@ -16,6 +17,7 @@ import net.minecraft.util.thread.ProcessorMailbox;
|
|
import net.minecraft.world.level.ChunkPos;
|
|
import net.minecraft.world.level.LightLayer;
|
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
|
+import net.minecraft.world.level.chunk.ChunkStatus;
|
|
import net.minecraft.world.level.chunk.DataLayer;
|
|
import net.minecraft.world.level.chunk.LevelChunkSection;
|
|
import net.minecraft.world.level.chunk.LightChunkGetter;
|
|
@@ -26,15 +28,140 @@ import org.apache.logging.log4j.Logger;
|
|
public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCloseable {
|
|
private static final Logger LOGGER = LogManager.getLogger();
|
|
private final ProcessorMailbox<Runnable> taskMailbox;
|
|
- private final ObjectList<Pair<ThreadedLevelLightEngine.TaskType, Runnable>> lightTasks = new ObjectArrayList<>();
|
|
- private final ChunkMap chunkMap;
|
|
+ // Paper start
|
|
+ private static final int MAX_PRIORITIES = ChunkMap.MAX_CHUNK_DISTANCE + 2;
|
|
+
|
|
+ static class ChunkLightQueue {
|
|
+ public boolean shouldFastUpdate;
|
|
+ java.util.ArrayDeque<Runnable> pre = new java.util.ArrayDeque<Runnable>();
|
|
+ java.util.ArrayDeque<Runnable> post = new java.util.ArrayDeque<Runnable>();
|
|
+
|
|
+ ChunkLightQueue(long chunk) {}
|
|
+ }
|
|
+
|
|
+ static class PendingLightTask {
|
|
+ long chunkId;
|
|
+ IntSupplier priority;
|
|
+ Runnable pre;
|
|
+ Runnable post;
|
|
+ boolean fastUpdate;
|
|
+
|
|
+ public PendingLightTask(long chunkId, IntSupplier priority, Runnable pre, Runnable post, boolean fastUpdate) {
|
|
+ this.chunkId = chunkId;
|
|
+ this.priority = priority;
|
|
+ this.pre = pre;
|
|
+ this.post = post;
|
|
+ this.fastUpdate = fastUpdate;
|
|
+ }
|
|
+ }
|
|
+
|
|
+
|
|
+ // Retain the chunks priority level for queued light tasks
|
|
+ class LightQueue {
|
|
+ private int size = 0;
|
|
+ private final Long2ObjectLinkedOpenHashMap<ChunkLightQueue>[] buckets = new Long2ObjectLinkedOpenHashMap[MAX_PRIORITIES];
|
|
+ private final java.util.concurrent.ConcurrentLinkedQueue<PendingLightTask> pendingTasks = new java.util.concurrent.ConcurrentLinkedQueue<>();
|
|
+ private final java.util.concurrent.ConcurrentLinkedQueue<Runnable> priorityChanges = new java.util.concurrent.ConcurrentLinkedQueue<>();
|
|
+
|
|
+ private LightQueue() {
|
|
+ for (int i = 0; i < buckets.length; i++) {
|
|
+ buckets[i] = new Long2ObjectLinkedOpenHashMap<>();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public void changePriority(long pair, int currentPriority, int priority) {
|
|
+ this.priorityChanges.add(() -> {
|
|
+ ChunkLightQueue remove = this.buckets[currentPriority].remove(pair);
|
|
+ if (remove != null) {
|
|
+ ChunkLightQueue existing = this.buckets[Math.max(1, priority)].put(pair, remove);
|
|
+ if (existing != null) {
|
|
+ remove.pre.addAll(existing.pre);
|
|
+ remove.post.addAll(existing.post);
|
|
+ }
|
|
+ }
|
|
+ });
|
|
+ }
|
|
+
|
|
+ public final void addChunk(long chunkId, IntSupplier priority, Runnable pre, Runnable post) {
|
|
+ pendingTasks.add(new PendingLightTask(chunkId, priority, pre, post, true));
|
|
+ tryScheduleUpdate();
|
|
+ }
|
|
+
|
|
+ public final void add(long chunkId, IntSupplier priority, ThreadedLevelLightEngine.TaskType type, Runnable run) {
|
|
+ pendingTasks.add(new PendingLightTask(chunkId, priority, type == TaskType.PRE_UPDATE ? run : null, type == TaskType.POST_UPDATE ? run : null, false));
|
|
+ }
|
|
+ public final void add(PendingLightTask update) {
|
|
+ int priority = update.priority.getAsInt();
|
|
+ ChunkLightQueue lightQueue = this.buckets[priority].computeIfAbsent(update.chunkId, ChunkLightQueue::new);
|
|
+
|
|
+ if (update.pre != null) {
|
|
+ this.size++;
|
|
+ lightQueue.pre.add(update.pre);
|
|
+ }
|
|
+ if (update.post != null) {
|
|
+ this.size++;
|
|
+ lightQueue.post.add(update.post);
|
|
+ }
|
|
+ if (update.fastUpdate) {
|
|
+ lightQueue.shouldFastUpdate = true;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public final boolean isEmpty() {
|
|
+ return this.size == 0 && this.pendingTasks.isEmpty();
|
|
+ }
|
|
+
|
|
+ public final int size() {
|
|
+ return this.size;
|
|
+ }
|
|
+
|
|
+ public boolean poll(java.util.List<Runnable> pre, java.util.List<Runnable> post) {
|
|
+ PendingLightTask pending;
|
|
+ while ((pending = pendingTasks.poll()) != null) {
|
|
+ add(pending);
|
|
+ }
|
|
+ Runnable run;
|
|
+ while ((run = priorityChanges.poll()) != null) {
|
|
+ run.run();
|
|
+ }
|
|
+ boolean hasWork = false;
|
|
+ Long2ObjectLinkedOpenHashMap<ChunkLightQueue>[] buckets = this.buckets;
|
|
+ int priority = 0;
|
|
+ while (priority < MAX_PRIORITIES && !isEmpty()) {
|
|
+ Long2ObjectLinkedOpenHashMap<ChunkLightQueue> bucket = buckets[priority];
|
|
+ if (bucket.isEmpty()) {
|
|
+ priority++;
|
|
+ if (hasWork) {
|
|
+ return true;
|
|
+ } else {
|
|
+ continue;
|
|
+ }
|
|
+ }
|
|
+ ChunkLightQueue queue = bucket.removeFirst();
|
|
+ this.size -= queue.pre.size() + queue.post.size();
|
|
+ pre.addAll(queue.pre);
|
|
+ post.addAll(queue.post);
|
|
+ queue.pre.clear();
|
|
+ queue.post.clear();
|
|
+ hasWork = true;
|
|
+ if (queue.shouldFastUpdate) {
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+ return hasWork;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ final LightQueue queue = new LightQueue();
|
|
+ // Paper end
|
|
+ private final ChunkMap chunkMap; private final ChunkMap playerChunkMap; // Paper
|
|
private final ProcessorHandle<ChunkTaskPriorityQueueSorter.Message<Runnable>> sorterMailbox;
|
|
private volatile int taskPerBatch = 5;
|
|
private final AtomicBoolean scheduled = new AtomicBoolean();
|
|
|
|
public ThreadedLevelLightEngine(LightChunkGetter chunkProvider, ChunkMap chunkStorage, boolean hasBlockLight, ProcessorMailbox<Runnable> processor, ProcessorHandle<ChunkTaskPriorityQueueSorter.Message<Runnable>> executor) {
|
|
super(chunkProvider, true, hasBlockLight);
|
|
- this.chunkMap = chunkStorage;
|
|
+ this.chunkMap = chunkStorage; this.playerChunkMap = chunkMap; // Paper
|
|
this.sorterMailbox = executor;
|
|
this.taskMailbox = processor;
|
|
}
|
|
@@ -120,13 +247,9 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
|
|
}
|
|
|
|
private void addTask(int x, int z, IntSupplier completedLevelSupplier, ThreadedLevelLightEngine.TaskType stage, Runnable task) {
|
|
- this.sorterMailbox.tell(ChunkTaskPriorityQueueSorter.message(() -> {
|
|
- this.lightTasks.add(Pair.of(stage, task));
|
|
- if (this.lightTasks.size() >= this.taskPerBatch) {
|
|
- this.runUpdate();
|
|
- }
|
|
-
|
|
- }, ChunkPos.asLong(x, z), completedLevelSupplier));
|
|
+ // Paper start - replace method
|
|
+ this.queue.add(ChunkPos.asLong(x, z), completedLevelSupplier, stage, task);
|
|
+ // Paper end
|
|
}
|
|
|
|
@Override
|
|
@@ -142,8 +265,14 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
|
|
|
|
public CompletableFuture<ChunkAccess> lightChunk(ChunkAccess chunk, boolean excludeBlocks) {
|
|
ChunkPos chunkPos = chunk.getPos();
|
|
- chunk.setLightCorrect(false);
|
|
- this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
|
|
+ // Paper start
|
|
+ //ichunkaccess.b(false); // Don't need to disable this
|
|
+ long pair = chunkPos.toLong();
|
|
+ CompletableFuture<ChunkAccess> future = new CompletableFuture<>();
|
|
+ IntSupplier prioritySupplier = playerChunkMap.getChunkQueueLevel(pair);
|
|
+ boolean[] skippedPre = {false};
|
|
+ this.queue.addChunk(pair, prioritySupplier, Util.name(() -> {
|
|
+ // Paper end
|
|
LevelChunkSection[] levelChunkSections = chunk.getSections();
|
|
|
|
for(int i = 0; i < chunk.getSectionsCount(); ++i) {
|
|
@@ -163,51 +292,45 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
|
|
|
|
}, () -> {
|
|
return "lightChunk " + chunkPos + " " + excludeBlocks;
|
|
- }));
|
|
- return CompletableFuture.supplyAsync(() -> {
|
|
+ // Paper start - merge the 2 together
|
|
+ }), () -> {
|
|
+ this.chunkMap.releaseLightTicket(chunkPos); // Paper - moved from below, we want to call this even when returning early
|
|
+ if (skippedPre[0]) return; // Paper - future's already complete
|
|
chunk.setLightCorrect(true);
|
|
super.retainData(chunkPos, false);
|
|
- this.chunkMap.releaseLightTicket(chunkPos);
|
|
- return chunk;
|
|
- }, (runnable) -> {
|
|
- this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.POST_UPDATE, runnable);
|
|
+ //this.chunkMap.releaseLightTicket(chunkPos); // Paper - moved up
|
|
+ future.complete(chunk);
|
|
});
|
|
+ return future;
|
|
+ // Paper end
|
|
}
|
|
|
|
public void tryScheduleUpdate() {
|
|
- if ((!this.lightTasks.isEmpty() || super.hasLightWork()) && this.scheduled.compareAndSet(false, true)) {
|
|
+ if ((!this.queue.isEmpty() || super.hasLightWork()) && this.scheduled.compareAndSet(false, true)) { // Paper
|
|
this.taskMailbox.tell(() -> {
|
|
this.runUpdate();
|
|
this.scheduled.set(false);
|
|
+ tryScheduleUpdate(); // Paper - if we still have work to do, do it!
|
|
});
|
|
}
|
|
|
|
}
|
|
|
|
+ // Paper start - replace impl
|
|
+ private final java.util.List<Runnable> pre = new java.util.ArrayList<>();
|
|
+ private final java.util.List<Runnable> post = new java.util.ArrayList<>();
|
|
private void runUpdate() {
|
|
- int i = Math.min(this.lightTasks.size(), this.taskPerBatch);
|
|
- ObjectListIterator<Pair<ThreadedLevelLightEngine.TaskType, Runnable>> objectListIterator = this.lightTasks.iterator();
|
|
-
|
|
- int j;
|
|
- for(j = 0; objectListIterator.hasNext() && j < i; ++j) {
|
|
- Pair<ThreadedLevelLightEngine.TaskType, Runnable> pair = objectListIterator.next();
|
|
- if (pair.getFirst() == ThreadedLevelLightEngine.TaskType.PRE_UPDATE) {
|
|
- pair.getSecond().run();
|
|
- }
|
|
+ if (queue.poll(pre, post)) {
|
|
+ pre.forEach(Runnable::run);
|
|
+ pre.clear();
|
|
+ super.runUpdates(Integer.MAX_VALUE, true, true);
|
|
+ post.forEach(Runnable::run);
|
|
+ post.clear();
|
|
+ } else {
|
|
+ // might have level updates to go still
|
|
+ super.runUpdates(Integer.MAX_VALUE, true, true);
|
|
}
|
|
-
|
|
- objectListIterator.back(j);
|
|
- super.runUpdates(Integer.MAX_VALUE, true, true);
|
|
-
|
|
- for(int var5 = 0; objectListIterator.hasNext() && var5 < i; ++var5) {
|
|
- Pair<ThreadedLevelLightEngine.TaskType, Runnable> pair2 = objectListIterator.next();
|
|
- if (pair2.getFirst() == ThreadedLevelLightEngine.TaskType.POST_UPDATE) {
|
|
- pair2.getSecond().run();
|
|
- }
|
|
-
|
|
- objectListIterator.remove();
|
|
- }
|
|
-
|
|
+ // Paper end
|
|
}
|
|
|
|
public void setTaskPerBatch(int taskBatchSize) {
|
|
diff --git a/src/main/java/net/minecraft/server/level/Ticket.java b/src/main/java/net/minecraft/server/level/Ticket.java
|
|
index f1128f0d4a9a0241ac6c9bc18dd13b431c616bb1..2b2b7851d5f68bcdb41d58bcc64740ba58bf1ef4 100644
|
|
--- a/src/main/java/net/minecraft/server/level/Ticket.java
|
|
+++ b/src/main/java/net/minecraft/server/level/Ticket.java
|
|
@@ -8,6 +8,7 @@ public final class Ticket<T> implements Comparable<Ticket<?>> {
|
|
public final T key;
|
|
public long createdTick;
|
|
public long delayUnloadBy; // Paper
|
|
+ public int priority; // Paper - Chunk priority
|
|
|
|
protected Ticket(TicketType<T> type, int level, T argument) {
|
|
this.type = type;
|
|
diff --git a/src/main/java/net/minecraft/server/level/TicketType.java b/src/main/java/net/minecraft/server/level/TicketType.java
|
|
index 8770fe0db46b01e8b608637df4f1a669a3f4cdde..3c1698ba0d3bc412ab957777d9b5211dbc555208 100644
|
|
--- a/src/main/java/net/minecraft/server/level/TicketType.java
|
|
+++ b/src/main/java/net/minecraft/server/level/TicketType.java
|
|
@@ -9,6 +9,8 @@ import net.minecraft.world.level.ChunkPos;
|
|
public class TicketType<T> {
|
|
public static final TicketType<Long> FUTURE_AWAIT = create("future_await", Long::compareTo); // Paper
|
|
public static final TicketType<Long> ASYNC_LOAD = create("async_load", Long::compareTo); // Paper
|
|
+ public static final TicketType<ChunkPos> PRIORITY = create("priority", Comparator.comparingLong(ChunkPos::toLong), 300); // Paper
|
|
+ public static final TicketType<ChunkPos> URGENT = create("urgent", Comparator.comparingLong(ChunkPos::toLong), 300); // Paper
|
|
|
|
private final String name;
|
|
private final Comparator<T> comparator;
|
|
diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
|
|
index ae3465cd5be426f3bddc3552b6aad6cad8c8ee25..91df850e66aa14b13a679357e02e2f74dc653628 100644
|
|
--- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
|
|
+++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
|
|
@@ -1545,6 +1545,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser
|
|
|
|
this.awaitingTeleportTime = this.tickCount;
|
|
this.player.absMoveTo(d0, d1, d2, f, f1);
|
|
+ this.player.forceCheckHighPriority(); // Paper
|
|
this.player.connection.send(new ClientboundPlayerPositionPacket(d0 - d3, d1 - d4, d2 - d5, f - f2, f1 - f3, set, this.awaitingTeleport, flag));
|
|
}
|
|
|
|
diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java
|
|
index e0c9857a922d8a3f421d7df0f056b82c4775390e..fefd3e4b7d9b2ec77dd2bc26298c3732d651df32 100644
|
|
--- a/src/main/java/net/minecraft/server/players/PlayerList.java
|
|
+++ b/src/main/java/net/minecraft/server/players/PlayerList.java
|
|
@@ -175,6 +175,7 @@ public abstract class PlayerList {
|
|
}
|
|
|
|
public void placeNewPlayer(Connection connection, ServerPlayer player) {
|
|
+ player.isRealPlayer = true; // Paper - Chunk priority
|
|
ServerPlayer prev = pendingPlayers.put(player.getUUID(), player);// Paper
|
|
if (prev != null) {
|
|
disconnectPendingPlayer(prev);
|
|
@@ -286,8 +287,8 @@ public abstract class PlayerList {
|
|
net.minecraft.server.level.ChunkMap playerChunkMap = worldserver1.getChunkSource().chunkMap;
|
|
net.minecraft.server.level.DistanceManager distanceManager = playerChunkMap.distanceManager;
|
|
distanceManager.addTicketAtLevel(net.minecraft.server.level.TicketType.LOGIN, pos, 31, pos.toLong());
|
|
- worldserver1.getChunkSource().runDistanceManagerUpdates();
|
|
- worldserver1.getChunkSource().getChunkAtAsynchronously(chunkX, chunkZ, true, true).thenApply(chunk -> {
|
|
+ worldserver1.getChunkSource().markAreaHighPriority(pos, 28, 3); // Paper - Chunk priority
|
|
+ worldserver1.getChunkSource().getChunkAtAsynchronously(chunkX, chunkZ, true, false).thenApply(chunk -> { // Paper - Chunk priority
|
|
net.minecraft.server.level.ChunkHolder updatingChunk = playerChunkMap.getUpdatingChunkIfPresent(pos.toLong());
|
|
if (updatingChunk != null) {
|
|
return updatingChunk.getEntityTickingChunkFuture();
|
|
@@ -893,6 +894,7 @@ public abstract class PlayerList {
|
|
// CraftBukkit end
|
|
|
|
worldserver1.getChunkSource().addRegionTicket(net.minecraft.server.level.TicketType.POST_TELEPORT, new net.minecraft.world.level.ChunkPos(location.getBlockX() >> 4, location.getBlockZ() >> 4), 1, entityplayer.getId()); // Paper
|
|
+ entityplayer1.forceCheckHighPriority(); // Player - Chunk priority
|
|
while (avoidSuffocation && !worldserver1.noCollision(entityplayer1) && entityplayer1.getY() < (double) worldserver1.getMaxBuildHeight()) {
|
|
entityplayer1.setPos(entityplayer1.getX(), entityplayer1.getY() + 1.0D, entityplayer1.getZ());
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java
|
|
index 3e1f34ed8c3b4bd4f6c3624332a5f2be62e196fe..8df955a0aa6cd0c7d10619fb63abd16af4754be8 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/Entity.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/Entity.java
|
|
@@ -223,7 +223,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n
|
|
private Vec3 position;
|
|
private BlockPos blockPosition;
|
|
private Vec3 deltaMovement;
|
|
- private float yRot;
|
|
+ public float yRot; // Paper - private->public
|
|
private float xRot;
|
|
public float yRotO;
|
|
public float xRotO;
|
|
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 a84b75a53a0324fab9aeb9b80bf74eb0a84ecd2e..d580f0375cce5e995c024f1b0cd4843b5718121c 100644
|
|
--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
|
|
+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
|
|
@@ -192,7 +192,7 @@ public class LevelChunk implements ChunkAccess {
|
|
return NEIGHBOUR_CACHE_RADIUS;
|
|
}
|
|
|
|
- boolean loadedTicketLevel;
|
|
+ boolean loadedTicketLevel; public final boolean wasLoadCallbackInvoked() { return this.loadedTicketLevel; } // Paper - public accessor
|
|
private long neighbourChunksLoadedBitset;
|
|
private final LevelChunk[] loadedNeighbourChunks = new LevelChunk[(NEIGHBOUR_CACHE_RADIUS * 2 + 1) * (NEIGHBOUR_CACHE_RADIUS * 2 + 1)];
|
|
|
|
@@ -794,6 +794,7 @@ public class LevelChunk implements ChunkAccess {
|
|
|
|
// CraftBukkit start
|
|
public void loadCallback() {
|
|
+ if (this.loadedTicketLevel) { LOGGER.error("Double calling chunk load!", new Throwable()); } // Paper
|
|
// Paper start - neighbour cache
|
|
int chunkX = this.chunkPos.x;
|
|
int chunkZ = this.chunkPos.z;
|
|
@@ -848,6 +849,7 @@ public class LevelChunk implements ChunkAccess {
|
|
}
|
|
|
|
public void unloadCallback() {
|
|
+ if (!this.loadedTicketLevel) { LOGGER.error("Double calling chunk unload!", new Throwable()); } // Paper
|
|
org.bukkit.Server server = this.level.getCraftServer();
|
|
org.bukkit.event.world.ChunkUnloadEvent unloadEvent = new org.bukkit.event.world.ChunkUnloadEvent(this.bukkitChunk, this.isUnsaved());
|
|
server.getPluginManager().callEvent(unloadEvent);
|
|
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
|
|
index 44f13b8bce4c6b9b3a74b2d9d868104feb3c24bf..4b70f3ad3a300dc099c15c70ba846e043d728e11 100644
|
|
--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
|
|
+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
|
|
@@ -1901,6 +1901,12 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
return future;
|
|
}
|
|
|
|
+ // Paper start - Chunk priority
|
|
+ if (!urgent) {
|
|
+ // If not urgent, at least use a slightly boosted priority
|
|
+ world.getChunkSource().markHighPriority(new ChunkPos(x, z), 1);
|
|
+ }
|
|
+ // Paper end
|
|
return this.world.getChunkSource().getChunkAtAsynchronously(x, z, gen, urgent).thenComposeAsync((either) -> {
|
|
net.minecraft.world.level.chunk.LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk) either.left().orElse(null);
|
|
if (chunk != null) addTicket(x, z); // Paper
|
|
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
|
|
index a3951039eeb1f5d3529213ca02d11c97d499be38..9ce4ebc9a0ddeefdc9fb3a7bd91be77aeb075011 100644
|
|
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
|
|
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
|
|
@@ -895,6 +895,16 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
|
|
throw new UnsupportedOperationException("Cannot set rotation of players. Consider teleporting instead.");
|
|
}
|
|
|
|
+ // Paper start - Chunk priority
|
|
+ @Override
|
|
+ public java.util.concurrent.CompletableFuture<Boolean> teleportAsync(Location loc, @javax.annotation.Nonnull PlayerTeleportEvent.TeleportCause cause) {
|
|
+ ((CraftWorld)loc.getWorld()).getHandle().getChunkSource().markAreaHighPriority(
|
|
+ new net.minecraft.world.level.ChunkPos(net.minecraft.util.Mth.floor(loc.getX()) >> 4,
|
|
+ net.minecraft.util.Mth.floor(loc.getZ()) >> 4), 28, 3); // Load area high priority
|
|
+ return super.teleportAsync(loc, cause);
|
|
+ }
|
|
+ // Paper end
|
|
+
|
|
@Override
|
|
public boolean teleport(Location location, PlayerTeleportEvent.TeleportCause cause) {
|
|
Preconditions.checkArgument(location != null, "location");
|