mirror of
https://github.com/PaperMC/Paper.git
synced 2024-09-12 06:58:11 +02:00
ensure we add missing player tickets even if already full status remove the player ticket throttler entirely... causes a lot of issues and our system handles the role that it was serving now too. increase max delays on farther out chunks load delay remove -5 priority delay for distant chunks seemed it applied at weird times. ensure if delay distance manager tick ever got left lingering it unsets on a chunk load. Fixes #3572
1127 lines
60 KiB
Diff
1127 lines
60 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 b5c2e1f4a2b5fdcaa6bb01f4b3b6847cd5b73ae8..6209b33d8497ec56bbde507e523db0649c66f590 100644
|
|
--- a/src/main/java/com/destroystokyo/paper/io/chunk/ChunkTaskManager.java
|
|
+++ b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkTaskManager.java
|
|
@@ -4,7 +4,10 @@ import com.destroystokyo.paper.io.PaperFileIOThread;
|
|
import com.destroystokyo.paper.io.IOUtil;
|
|
import com.destroystokyo.paper.io.PrioritizedTaskQueue;
|
|
import com.destroystokyo.paper.io.QueueExecutorThread;
|
|
+import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
|
|
+import net.minecraft.server.ChunkCoordIntPair;
|
|
import net.minecraft.server.ChunkRegionLoader;
|
|
+import net.minecraft.server.ChunkStatus;
|
|
import net.minecraft.server.IAsyncTaskHandler;
|
|
import net.minecraft.server.IChunkAccess;
|
|
import net.minecraft.server.MinecraftServer;
|
|
@@ -106,7 +109,7 @@ public final class ChunkTaskManager {
|
|
}
|
|
|
|
static void dumpChunkInfo(Set<PlayerChunk> seenChunks, PlayerChunk chunkHolder, int x, int z) {
|
|
- dumpChunkInfo(seenChunks, chunkHolder, x, z, 0, 1);
|
|
+ dumpChunkInfo(seenChunks, chunkHolder, x, z, 0, 4);
|
|
}
|
|
|
|
static void dumpChunkInfo(Set<PlayerChunk> seenChunks, PlayerChunk chunkHolder, int x, int z, int indent, int maxDepth) {
|
|
@@ -127,6 +130,30 @@ public final class ChunkTaskManager {
|
|
PaperFileIOThread.LOGGER.log(Level.ERROR, indentStr + "Chunk Status - " + ((chunk == null) ? "null chunk" : chunk.getChunkStatus().toString()));
|
|
PaperFileIOThread.LOGGER.log(Level.ERROR, indentStr + "Chunk Ticket Status - " + PlayerChunk.getChunkStatus(chunkHolder.getTicketLevel()));
|
|
PaperFileIOThread.LOGGER.log(Level.ERROR, indentStr + "Chunk Holder Status - " + ((holderStatus == null) ? "null" : holderStatus.toString()));
|
|
+ PaperFileIOThread.LOGGER.log(Level.ERROR, indentStr + "Chunk Holder Priority - " + chunkHolder.getCurrentPriority());
|
|
+
|
|
+ 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 (PlayerChunk neighbor : chunkHolder.neighbors.keySet()) {
|
|
+ ChunkStatus status = neighbor.getChunkHolderStatus();
|
|
+ if (status != null && status.isAtLeastStatus(PlayerChunk.getChunkStatus(neighbor.getTicketLevel()))) {
|
|
+ continue;
|
|
+ }
|
|
+ int nx = neighbor.location.x;
|
|
+ int nz = neighbor.location.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);
|
|
+ }
|
|
+ }
|
|
+
|
|
}
|
|
}
|
|
|
|
diff --git a/src/main/java/net/minecraft/server/ChunkCoordIntPair.java b/src/main/java/net/minecraft/server/ChunkCoordIntPair.java
|
|
index f617636a22167b06ac8073aa25efd8c7099155f0..0f40793f004639822b9d40521cd21ec50391ba3b 100644
|
|
--- a/src/main/java/net/minecraft/server/ChunkCoordIntPair.java
|
|
+++ b/src/main/java/net/minecraft/server/ChunkCoordIntPair.java
|
|
@@ -110,6 +110,7 @@ public class ChunkCoordIntPair {
|
|
return "[" + this.x + ", " + this.z + "]";
|
|
}
|
|
|
|
+ public final BlockPosition asPosition() { return l(); } // Paper - OBFHELPER
|
|
public BlockPosition l() {
|
|
return new BlockPosition(this.x << 4, 0, this.z << 4);
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/server/ChunkMapDistance.java b/src/main/java/net/minecraft/server/ChunkMapDistance.java
|
|
index 7702fbefa598bce7e6a2d287f7ec36b78a62bff8..f7be94e801e58a39e0efcc813d95bbd613afce03 100644
|
|
--- a/src/main/java/net/minecraft/server/ChunkMapDistance.java
|
|
+++ b/src/main/java/net/minecraft/server/ChunkMapDistance.java
|
|
@@ -23,6 +23,7 @@ import java.util.concurrent.Executor;
|
|
import javax.annotation.Nullable;
|
|
import org.apache.logging.log4j.LogManager;
|
|
import org.apache.logging.log4j.Logger;
|
|
+import org.spigotmc.AsyncCatcher; // Paper
|
|
|
|
public abstract class ChunkMapDistance {
|
|
|
|
@@ -84,6 +85,7 @@ public abstract class ChunkMapDistance {
|
|
}
|
|
|
|
private static int a(ArraySetSorted<Ticket<?>> arraysetsorted) {
|
|
+ AsyncCatcher.catchOp("ChunkMapDistance::getHighestTicketLevel"); // Paper
|
|
return !arraysetsorted.isEmpty() ? ((Ticket) arraysetsorted.b()).b() : PlayerChunkMap.GOLDEN_TICKET + 1;
|
|
}
|
|
|
|
@@ -97,6 +99,7 @@ public abstract class ChunkMapDistance {
|
|
|
|
public boolean a(PlayerChunkMap playerchunkmap) {
|
|
//this.f.a(); // Paper - no longer used
|
|
+ AsyncCatcher.catchOp("DistanceManagerTick");
|
|
this.g.a();
|
|
int i = Integer.MAX_VALUE - this.e.a(Integer.MAX_VALUE);
|
|
boolean flag = i != 0;
|
|
@@ -107,11 +110,13 @@ public abstract class ChunkMapDistance {
|
|
|
|
// Paper start
|
|
if (!this.pendingChunkUpdates.isEmpty()) {
|
|
+ this.pollingPendingChunkUpdates = true; try {
|
|
while(!this.pendingChunkUpdates.isEmpty()) {
|
|
PlayerChunk remove = this.pendingChunkUpdates.remove();
|
|
remove.isUpdateQueued = false;
|
|
remove.a(playerchunkmap);
|
|
}
|
|
+ } finally { this.pollingPendingChunkUpdates = false; }
|
|
// Paper end
|
|
return true;
|
|
} else {
|
|
@@ -147,8 +152,10 @@ public abstract class ChunkMapDistance {
|
|
return flag;
|
|
}
|
|
}
|
|
+ boolean pollingPendingChunkUpdates = false; // Paper
|
|
|
|
private boolean addTicket(long i, Ticket<?> ticket) { // CraftBukkit - void -> boolean
|
|
+ AsyncCatcher.catchOp("ChunkMapDistance::addTicket"); // Paper
|
|
ArraySetSorted<Ticket<?>> arraysetsorted = this.e(i);
|
|
int j = a(arraysetsorted);
|
|
Ticket<?> ticket1 = (Ticket) arraysetsorted.a(ticket); // CraftBukkit - decompile error
|
|
@@ -162,7 +169,9 @@ public abstract class ChunkMapDistance {
|
|
}
|
|
|
|
private boolean removeTicket(long i, Ticket<?> ticket) { // CraftBukkit - void -> boolean
|
|
+ AsyncCatcher.catchOp("ChunkMapDistance::removeTicket"); // Paper
|
|
ArraySetSorted<Ticket<?>> arraysetsorted = this.e(i);
|
|
+ int oldLevel = a(arraysetsorted); // Paper
|
|
|
|
boolean removed = false; // CraftBukkit
|
|
if (arraysetsorted.remove(ticket)) {
|
|
@@ -172,8 +181,8 @@ public abstract class ChunkMapDistance {
|
|
if (arraysetsorted.isEmpty()) {
|
|
this.tickets.remove(i);
|
|
}
|
|
-
|
|
- this.e.b(i, a(arraysetsorted), false);
|
|
+ int newLevel = a(arraysetsorted); // Paper
|
|
+ if (newLevel > oldLevel) this.e.b(i, newLevel, false); // Paper
|
|
return removed; // CraftBukkit
|
|
}
|
|
|
|
@@ -182,6 +191,113 @@ public abstract class ChunkMapDistance {
|
|
this.addTicketAtLevel(tickettype, chunkcoordintpair, i, t0);
|
|
}
|
|
|
|
+ // Paper start
|
|
+ public static final int PRIORITY_TICKET_LEVEL = PlayerChunkMap.GOLDEN_TICKET;
|
|
+ public static final int URGENT_PRIORITY = 29;
|
|
+ public boolean delayDistanceManagerTick = false;
|
|
+ public boolean markUrgent(ChunkCoordIntPair coords) {
|
|
+ return addPriorityTicket(coords, TicketType.URGENT, URGENT_PRIORITY);
|
|
+ }
|
|
+ public boolean markHighPriority(ChunkCoordIntPair coords, int priority) {
|
|
+ priority = Math.min(URGENT_PRIORITY - 1, Math.max(1, priority));
|
|
+ return addPriorityTicket(coords, TicketType.PRIORITY, priority);
|
|
+ }
|
|
+
|
|
+ public void markAreaHighPriority(ChunkCoordIntPair center, int priority, int radius) {
|
|
+ delayDistanceManagerTick = true;
|
|
+ MCUtil.getSpiralOutChunks(center.asPosition(), radius).forEach(coords -> {
|
|
+ addPriorityTicket(coords, TicketType.PRIORITY, priority);
|
|
+ });
|
|
+ delayDistanceManagerTick = false;
|
|
+ chunkMap.world.getChunkProvider().tickDistanceManager();
|
|
+ }
|
|
+
|
|
+ public void clearAreaPriorityTickets(ChunkCoordIntPair center, int radius) {
|
|
+ delayDistanceManagerTick = true;
|
|
+ MCUtil.getSpiralOutChunks(center.asPosition(), radius).forEach(coords -> {
|
|
+ this.removeTicket(coords.pair(), new Ticket<ChunkCoordIntPair>(TicketType.PRIORITY, PRIORITY_TICKET_LEVEL, coords));
|
|
+ });
|
|
+ delayDistanceManagerTick = false;
|
|
+ chunkMap.world.getChunkProvider().tickDistanceManager();
|
|
+ }
|
|
+
|
|
+ private boolean addPriorityTicket(ChunkCoordIntPair coords, TicketType<ChunkCoordIntPair> ticketType, int priority) {
|
|
+ AsyncCatcher.catchOp("ChunkMapDistance::addPriorityTicket");
|
|
+ long pair = coords.pair();
|
|
+ PlayerChunk chunk = chunkMap.getUpdatingChunk(pair);
|
|
+ if (chunk != null && chunk.isFullChunkReady() && chunk.getTicketLevel() <= 33) {
|
|
+ return false;
|
|
+ }
|
|
+ if (chunk != null && chunk.getTicketLevel() > 33 && chunkMap.playerViewDistanceNoTickMap.getObjectsInRange(pair) != null) {
|
|
+ Ticket<?> ticket = new Ticket<>(TicketType.PLAYER, 33, coords);
|
|
+ addTicket(pair, ticket);
|
|
+ }
|
|
+
|
|
+ boolean success;
|
|
+ if (!(success = updatePriorityTicket(coords, ticketType, priority))) {
|
|
+ Ticket<ChunkCoordIntPair> ticket = new Ticket<ChunkCoordIntPair>(ticketType, PRIORITY_TICKET_LEVEL, coords);
|
|
+ ticket.priority = priority;
|
|
+ success = this.addTicket(pair, ticket);
|
|
+ } else {
|
|
+ if (chunk == null) {
|
|
+ chunk = chunkMap.getUpdatingChunk(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.world.getChunkProvider().tickDistanceManager();
|
|
+
|
|
+ return success;
|
|
+ }
|
|
+
|
|
+ private boolean updatePriorityTicket(ChunkCoordIntPair coords, TicketType<ChunkCoordIntPair> type, int priority) {
|
|
+ ArraySetSorted<Ticket<?>> tickets = this.tickets.get(coords.pair());
|
|
+ if (tickets == null) {
|
|
+ return false;
|
|
+ }
|
|
+ for (Ticket<?> ticket : tickets) {
|
|
+ if (ticket.getTicketType() == type) {
|
|
+ // We only support increasing, not decreasing, too complicated
|
|
+ ticket.setCurrentTick(this.currentTick);
|
|
+ ticket.priority = Math.max(ticket.priority, priority);
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ public int getChunkPriority(ChunkCoordIntPair coords) {
|
|
+ AsyncCatcher.catchOp("ChunkMapDistance::getChunkPriority");
|
|
+ ArraySetSorted<Ticket<?>> tickets = this.tickets.get(coords.pair());
|
|
+ if (tickets == null) {
|
|
+ return 0;
|
|
+ }
|
|
+ for (Ticket<?> ticket : tickets) {
|
|
+ if (ticket.getTicketType() == TicketType.URGENT) {
|
|
+ return URGENT_PRIORITY;
|
|
+ }
|
|
+ }
|
|
+ for (Ticket<?> ticket : tickets) {
|
|
+ if (ticket.getTicketType() == TicketType.PRIORITY && ticket.priority > 0) {
|
|
+ return ticket.priority;
|
|
+ }
|
|
+ }
|
|
+ return 0;
|
|
+ }
|
|
+
|
|
+ public void clearPriorityTickets(ChunkCoordIntPair coords) {
|
|
+ AsyncCatcher.catchOp("ChunkMapDistance::clearPriority");
|
|
+ this.removeTicket(coords.pair(), new Ticket<ChunkCoordIntPair>(TicketType.PRIORITY, PRIORITY_TICKET_LEVEL, coords));
|
|
+ }
|
|
+
|
|
+ public void clearUrgent(ChunkCoordIntPair coords) {
|
|
+ AsyncCatcher.catchOp("ChunkMapDistance::clearUrgent");
|
|
+ this.removeTicket(coords.pair(), new Ticket<ChunkCoordIntPair>(TicketType.URGENT, PRIORITY_TICKET_LEVEL, coords));
|
|
+ }
|
|
+ // Paper end
|
|
public <T> boolean addTicketAtLevel(TicketType<T> ticketType, ChunkCoordIntPair chunkcoordintpair, int level, T identifier) {
|
|
return this.addTicket(chunkcoordintpair.pair(), new Ticket<>(ticketType, level, identifier));
|
|
// CraftBukkit end
|
|
@@ -384,24 +500,26 @@ public abstract class ChunkMapDistance {
|
|
Ticket<?> ticket = new Ticket<>(TicketType.PLAYER, 33, new ChunkCoordIntPair(i)); // Paper - no-tick view distance
|
|
|
|
if (flag1) {
|
|
- ChunkMapDistance.this.j.a(ChunkTaskQueueSorter.a(() -> { // CraftBukkit - decompile error
|
|
- ChunkMapDistance.this.m.execute(() -> {
|
|
- if (this.c(this.c(i))) {
|
|
+ // Paper start - smarter ticket delay based on frustum and distance
|
|
+ scheduleChunkLoad(i, MinecraftServer.currentTick, j, (priority) -> {
|
|
+ //ChunkMapDistance.this.j.a(ChunkTaskQueueSorter.a(() -> { // CraftBukkit - decompile error
|
|
+ if (this.c(this.c(i))) { // Copy c(c()) stuff below
|
|
+ // Paper end
|
|
ChunkMapDistance.this.addTicket(i, ticket);
|
|
ChunkMapDistance.this.l.add(i);
|
|
} else {
|
|
ChunkMapDistance.this.k.a(ChunkTaskQueueSorter.a(() -> { // CraftBukkit - decompile error
|
|
}, i, false));
|
|
}
|
|
-
|
|
- });
|
|
- }, i, () -> {
|
|
- return j;
|
|
- }));
|
|
+ //}, i, () -> {
|
|
+ //return Math.min(PlayerChunkMap.GOLDEN_TICKET, (priority <= 6 ? 20 : 30) + priority); // Paper - delay new ticket adds to avoid spamming the queue
|
|
+ //})); // Paper
|
|
+ }); // Paper
|
|
} else {
|
|
ChunkMapDistance.this.k.a(ChunkTaskQueueSorter.a(() -> { // CraftBukkit - decompile error
|
|
ChunkMapDistance.this.m.execute(() -> {
|
|
ChunkMapDistance.this.removeTicket(i, ticket);
|
|
+ ChunkMapDistance.this.clearPriorityTickets(new ChunkCoordIntPair(i)); // Paper
|
|
});
|
|
}, i, true));
|
|
}
|
|
@@ -409,6 +527,99 @@ public abstract class ChunkMapDistance {
|
|
|
|
}
|
|
|
|
+ // Paper start - smart scheduling of player tickets
|
|
+ public void scheduleChunkLoad(long i, long startTick, int initialDistance, java.util.function.Consumer<Integer> task) {
|
|
+ long elapsed = MinecraftServer.currentTick - startTick;
|
|
+ ChunkCoordIntPair chunkPos = new ChunkCoordIntPair(i);
|
|
+ PlayerChunk updatingChunk = chunkMap.getUpdatingChunk(i);
|
|
+ if ((updatingChunk != null && updatingChunk.isFullChunkReady()) || !this.c(this.c(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<EntityPlayer> 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();
|
|
+
|
|
+ BlockPosition blockPos = chunkPos.asPosition();
|
|
+
|
|
+ boolean isFront = false;
|
|
+ BlockPosition.PooledBlockPosition pos = BlockPosition.PooledBlockPosition.acquire();
|
|
+ for (int index = 0, len = backingSet.length; index < len; ++index) {
|
|
+ if (!(backingSet[index] instanceof EntityPlayer)) {
|
|
+ continue;
|
|
+ }
|
|
+ EntityPlayer player = (EntityPlayer) backingSet[index];
|
|
+
|
|
+ ChunkCoordIntPair pointInFront = player.getChunkInFront(5);
|
|
+ pos.setValues(pointInFront.x << 4, 0, pointInFront.z << 4);
|
|
+ double frontDist = MCUtil.distanceSq(pos, blockPos);
|
|
+
|
|
+ pos.setValues(player.locX(), 0, player.locZ());
|
|
+ double center = MCUtil.distanceSq(pos, blockPos);
|
|
+
|
|
+ double dist = Math.min(frontDist, center);
|
|
+ if (!isFront) {
|
|
+
|
|
+ ChunkCoordIntPair pointInBack = player.getChunkInFront(-5);
|
|
+ pos.setValues(pointInBack.x << 4, 0, pointInBack.z << 4);
|
|
+ double backDist = MCUtil.distanceSq(pos, blockPos);
|
|
+ if (frontDist < backDist) {
|
|
+ isFront = true;
|
|
+ }
|
|
+ }
|
|
+ if (dist < minDist) {
|
|
+ minDist = dist;
|
|
+ }
|
|
+ }
|
|
+ pos.close();
|
|
+ if (minDist == Double.MAX_VALUE) {
|
|
+ minDist = 15;
|
|
+ } else {
|
|
+ minDist = Math.sqrt(minDist) / 16;
|
|
+ }
|
|
+ if (minDist > 4) {
|
|
+ int desiredTimeDelayMax = isFront ?
|
|
+ (minDist < 10 ? 10 : 20) : // Front
|
|
+ (minDist < 10 ? 20 : 40); // 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 = new ChunkCoordIntPair(chunkPos.x + x, chunkPos.z + z).pair();
|
|
+ PlayerChunk neighbor = chunkMap.getUpdatingChunk(pair);
|
|
+ ChunkStatus current = neighbor != null ? neighbor.getChunkHolderStatus() : null;
|
|
+ if (current != null && current.isAtLeastStatus(ChunkStatus.LIGHT)) {
|
|
+ hasAnyNeighbor = true;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ if (!hasAnyNeighbor) {
|
|
+ delay += 10;
|
|
+ }
|
|
+ }
|
|
+ if (delay <= 0) {
|
|
+ task.accept((int) minDist);
|
|
+ } else {
|
|
+ MCUtil.scheduleTask((int) Math.min(delay, minDist >= 10 ? 40 : (minDist < 6 ? 5 : 20)), () -> scheduleChunkLoad(i, startTick, initialDistance, task), "Player Ticket Delayer");
|
|
+ }
|
|
+ }
|
|
+ // Paper end
|
|
+
|
|
@Override
|
|
public void a() {
|
|
super.a();
|
|
diff --git a/src/main/java/net/minecraft/server/ChunkProviderServer.java b/src/main/java/net/minecraft/server/ChunkProviderServer.java
|
|
index 7a275bf3260f9fbefc41883c5ebdc1eb2196daf0..81a200ea4c533744890b6e19dd0d83f57351906c 100644
|
|
--- a/src/main/java/net/minecraft/server/ChunkProviderServer.java
|
|
+++ b/src/main/java/net/minecraft/server/ChunkProviderServer.java
|
|
@@ -432,6 +432,26 @@ public class ChunkProviderServer extends IChunkProvider {
|
|
public <T> void removeTicketAtLevel(TicketType<T> ticketType, ChunkCoordIntPair chunkPos, int ticketLevel, T identifier) {
|
|
this.chunkMapDistance.removeTicketAtLevel(ticketType, chunkPos, ticketLevel, identifier);
|
|
}
|
|
+
|
|
+ public boolean markUrgent(ChunkCoordIntPair coords) {
|
|
+ return this.chunkMapDistance.markUrgent(coords);
|
|
+ }
|
|
+
|
|
+ public boolean markHighPriority(ChunkCoordIntPair coords, int priority) {
|
|
+ return this.chunkMapDistance.markHighPriority(coords, priority);
|
|
+ }
|
|
+
|
|
+ public void markAreaHighPriority(ChunkCoordIntPair center, int priority, int radius) {
|
|
+ this.chunkMapDistance.markAreaHighPriority(center, priority, radius);
|
|
+ }
|
|
+
|
|
+ public void clearAreaPriorityTickets(ChunkCoordIntPair center, int radius) {
|
|
+ this.chunkMapDistance.clearAreaPriorityTickets(center, radius);
|
|
+ }
|
|
+
|
|
+ public void clearPriorityTickets(ChunkCoordIntPair coords) {
|
|
+ this.chunkMapDistance.clearPriorityTickets(coords);
|
|
+ }
|
|
// Paper end
|
|
|
|
@Nullable
|
|
@@ -470,6 +490,8 @@ public class ChunkProviderServer extends IChunkProvider {
|
|
|
|
if (!completablefuture.isDone()) { // Paper
|
|
// Paper start - async chunk io/loading
|
|
+ ChunkCoordIntPair pair = new ChunkCoordIntPair(x, z);
|
|
+ this.chunkMapDistance.markUrgent(pair);
|
|
this.world.asyncChunkTaskManager.raisePriority(x, z, com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGHEST_PRIORITY);
|
|
com.destroystokyo.paper.io.chunk.ChunkTaskManager.pushChunkWait(this.world, x, z);
|
|
// Paper end
|
|
@@ -478,6 +500,8 @@ public class ChunkProviderServer extends IChunkProvider {
|
|
this.serverThreadQueue.awaitTasks(completablefuture::isDone);
|
|
com.destroystokyo.paper.io.chunk.ChunkTaskManager.popChunkWait(); // Paper - async chunk debug
|
|
this.world.timings.syncChunkLoad.stopTiming(); // Paper
|
|
+ this.chunkMapDistance.clearPriorityTickets(pair); // Paper
|
|
+ this.chunkMapDistance.clearUrgent(pair); // Paper
|
|
} // Paper
|
|
ichunkaccess = (IChunkAccess) ((Either) completablefuture.join()).map((ichunkaccess1) -> {
|
|
return ichunkaccess1;
|
|
@@ -527,13 +551,15 @@ public class ChunkProviderServer extends IChunkProvider {
|
|
PlayerChunk.State currentChunkState = PlayerChunk.getChunkState(playerchunk.getTicketLevel());
|
|
currentlyUnloading = (oldChunkState.isAtLeast(PlayerChunk.State.BORDER) && !currentChunkState.isAtLeast(PlayerChunk.State.BORDER));
|
|
}
|
|
- if (flag && !currentlyUnloading) {
|
|
+ if (flag) { // Paper - don't care about unloading state
|
|
// CraftBukkit end
|
|
this.chunkMapDistance.a(TicketType.UNKNOWN, chunkcoordintpair, l, chunkcoordintpair);
|
|
+ if (isUrgent) this.chunkMapDistance.markUrgent(chunkcoordintpair); // Paper
|
|
if (this.a(playerchunk, l)) {
|
|
GameProfilerFiller gameprofilerfiller = this.world.getMethodProfiler();
|
|
|
|
gameprofilerfiller.enter("chunkLoad");
|
|
+ chunkMapDistance.delayDistanceManagerTick = false; // Paper - ensure this is never false
|
|
this.tickDistanceManager();
|
|
playerchunk = this.getChunk(k);
|
|
gameprofilerfiller.exit();
|
|
@@ -542,12 +568,17 @@ public class ChunkProviderServer extends IChunkProvider {
|
|
}
|
|
}
|
|
}
|
|
-
|
|
- return this.a(playerchunk, l) ? PlayerChunk.UNLOADED_CHUNK_ACCESS_FUTURE : playerchunk.a(chunkstatus, this.playerChunkMap);
|
|
+ // Paper start
|
|
+ CompletableFuture<Either<IChunkAccess, PlayerChunk.Failure>> future = this.a(playerchunk, l) ? PlayerChunk.UNLOADED_CHUNK_ACCESS_FUTURE : playerchunk.a(chunkstatus, this.playerChunkMap);
|
|
+ if (isUrgent) {
|
|
+ future.thenAccept(either -> this.chunkMapDistance.clearUrgent(chunkcoordintpair));
|
|
+ }
|
|
+ return future;
|
|
+ // Paper end
|
|
}
|
|
|
|
private boolean a(@Nullable PlayerChunk playerchunk, int i) {
|
|
- return playerchunk == null || playerchunk.oldTicketLevel > i; // CraftBukkit using oldTicketLevel for isLoaded checks
|
|
+ return playerchunk == null || playerchunk.getTicketLevel() > i; // CraftBukkit using oldTicketLevel for isLoaded checks - Paper - keep vanilla check
|
|
}
|
|
|
|
public boolean isLoaded(int i, int j) {
|
|
@@ -593,7 +624,8 @@ public class ChunkProviderServer extends IChunkProvider {
|
|
return this.serverThreadQueue.executeNext();
|
|
}
|
|
|
|
- private boolean tickDistanceManager() {
|
|
+ public boolean tickDistanceManager() { // Paper - public
|
|
+ if (chunkMapDistance.delayDistanceManagerTick) return false; // Paper
|
|
boolean flag = this.chunkMapDistance.a(this.playerChunkMap);
|
|
boolean flag1 = this.playerChunkMap.b();
|
|
|
|
diff --git a/src/main/java/net/minecraft/server/EntityPlayer.java b/src/main/java/net/minecraft/server/EntityPlayer.java
|
|
index 07a6fc3d88e7d44bfab7f3d6a0eef7dc132ab422..4ed4ad6bc3b4ab6702ca500dc26e889dca6ed2d7 100644
|
|
--- a/src/main/java/net/minecraft/server/EntityPlayer.java
|
|
+++ b/src/main/java/net/minecraft/server/EntityPlayer.java
|
|
@@ -55,6 +55,7 @@ public class EntityPlayer extends EntityHuman implements ICrafting {
|
|
private int lastArmorScored = Integer.MIN_VALUE;
|
|
private int lastExpLevelScored = Integer.MIN_VALUE;
|
|
private int lastExpTotalScored = Integer.MIN_VALUE;
|
|
+ public long lastHighPriorityChecked; // Paper
|
|
private float lastHealthSent = -1.0E8F;
|
|
private int lastFoodSent = -99999999;
|
|
private boolean lastSentSaturationZero = true;
|
|
@@ -132,6 +133,21 @@ public class EntityPlayer extends EntityHuman implements ICrafting {
|
|
this.maxHealthCache = this.getMaxHealth();
|
|
this.cachedSingleMobDistanceMap = new com.destroystokyo.paper.util.PooledHashSets.PooledObjectLinkedOpenHashSet<>(this); // Paper
|
|
}
|
|
+ // Paper start
|
|
+ public BlockPosition getPointInFront(double inFront) {
|
|
+ double rads = Math.toRadians(MCUtil.normalizeYaw(this.yaw+90)); // MC rotates yaw 90 for some odd reason
|
|
+ final double x = locX() + inFront * Math.cos(rads);
|
|
+ final double z = locZ() + inFront * Math.sin(rads);
|
|
+ return new BlockPosition(x, locY(), z);
|
|
+ }
|
|
+
|
|
+ public ChunkCoordIntPair getChunkInFront(double inFront) {
|
|
+ double rads = Math.toRadians(MCUtil.normalizeYaw(this.yaw+90)); // MC rotates yaw 90 for some odd reason
|
|
+ final double x = locX() + (inFront * 16) * Math.cos(rads);
|
|
+ final double z = locZ() + (inFront * 16) * Math.sin(rads);
|
|
+ return new ChunkCoordIntPair(MathHelper.floor(x) >> 4, MathHelper.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
|
|
@@ -441,6 +457,7 @@ public class EntityPlayer extends EntityHuman implements ICrafting {
|
|
if (valid && (!this.isSpectator() || this.world.isLoaded(new BlockPosition(this)))) { // Paper - don't tick dead players that are not in the world currently (pending respawn)
|
|
super.tick();
|
|
}
|
|
+ if (valid && isAlive()) ((WorldServer)world).getChunkProvider().playerChunkMap.checkHighPriorityChunks(this); // Paper
|
|
|
|
for (int i = 0; i < this.inventory.getSize(); ++i) {
|
|
ItemStack itemstack = this.inventory.getItem(i);
|
|
diff --git a/src/main/java/net/minecraft/server/MCUtil.java b/src/main/java/net/minecraft/server/MCUtil.java
|
|
index ce0bf608b71cf492fc31e89a360ecd83fa5c23a6..87d58002116f361d8255d79fc0dbd1200f442168 100644
|
|
--- a/src/main/java/net/minecraft/server/MCUtil.java
|
|
+++ b/src/main/java/net/minecraft/server/MCUtil.java
|
|
@@ -666,6 +666,7 @@ public final class MCUtil {
|
|
chunkData.addProperty("x", playerChunk.location.x);
|
|
chunkData.addProperty("z", playerChunk.location.z);
|
|
chunkData.addProperty("ticket-level", playerChunk.getTicketLevel());
|
|
+ chunkData.addProperty("priority", playerChunk.getCurrentPriority());
|
|
chunkData.addProperty("state", PlayerChunk.getChunkState(playerChunk.getTicketLevel()).toString());
|
|
chunkData.addProperty("queued-for-unload", chunkMap.unloadQueue.contains(playerChunk.location.pair()));
|
|
chunkData.addProperty("status", status == null ? "unloaded" : status.toString());
|
|
diff --git a/src/main/java/net/minecraft/server/PlayerChunk.java b/src/main/java/net/minecraft/server/PlayerChunk.java
|
|
index aeca6b2b9d5d73aeb6dc639b5cad2f2533a2de44..bc5e380aa37101382c359f1c9a25803e09cd238c 100644
|
|
--- a/src/main/java/net/minecraft/server/PlayerChunk.java
|
|
+++ b/src/main/java/net/minecraft/server/PlayerChunk.java
|
|
@@ -26,8 +26,8 @@ public class PlayerChunk {
|
|
private CompletableFuture<IChunkAccess> chunkSave;
|
|
public int oldTicketLevel;
|
|
private int ticketLevel;
|
|
- private int n;
|
|
- final ChunkCoordIntPair location; // Paper - private -> package
|
|
+ volatile int n; public final int getCurrentPriority() { return n; } // Paper - OBFHELPER - make volatile since this is concurrently accessed
|
|
+ public final ChunkCoordIntPair location; // Paper - private -> public
|
|
private final short[] dirtyBlocks;
|
|
private int dirtyCount;
|
|
private int r;
|
|
@@ -40,6 +40,7 @@ public class PlayerChunk {
|
|
private boolean hasBeenLoaded;
|
|
|
|
private final PlayerChunkMap chunkMap; // Paper
|
|
+ public WorldServer getWorld() { return chunkMap.world; } // Paper
|
|
|
|
long lastAutoSaveTime; // Paper - incremental autosave
|
|
long inactiveTimeStart; // Paper - incremental autosave
|
|
@@ -67,6 +68,120 @@ public class PlayerChunk {
|
|
return null;
|
|
}
|
|
// Paper end - no-tick view distance
|
|
+ // Paper start - Chunk gen/load priority system
|
|
+ volatile int neighborPriority = -1;
|
|
+ volatile int priorityBoost = 0;
|
|
+ public final java.util.concurrent.ConcurrentHashMap<PlayerChunk, ChunkStatus> neighbors = new java.util.concurrent.ConcurrentHashMap<>();
|
|
+ public final it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<Integer> neighborPriorities = new it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<>();
|
|
+
|
|
+ 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, PlayerChunkMap.GOLDEN_TICKET), priority));
|
|
+ }
|
|
+
|
|
+ private int getMyPriority() {
|
|
+ if (priorityBoost == ChunkMapDistance.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();
|
|
+ }
|
|
+
|
|
+ public void onNeighborRequest(PlayerChunk neighbor, ChunkStatus status) {
|
|
+ neighbor.setNeighborPriority(this, getNeighborsPriority());
|
|
+ this.neighbors.compute(neighbor, (playerChunk, currentWantedStatus) -> {
|
|
+ if (currentWantedStatus == null || !currentWantedStatus.isAtLeastStatus(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(PlayerChunk neighbor, ChunkStatus chunkstatus, IChunkAccess chunk) {
|
|
+ this.neighbors.compute(neighbor, (playerChunk, wantedStatus) -> {
|
|
+ if (wantedStatus != null && chunkstatus.isAtLeastStatus(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(PlayerChunk requester) {
|
|
+ synchronized (neighborPriorities) {
|
|
+ neighborPriorities.remove(requester.location.pair());
|
|
+ recalcNeighborPriority();
|
|
+ }
|
|
+ checkPriority();
|
|
+ }
|
|
+
|
|
+
|
|
+ private void setNeighborPriority(PlayerChunk requester, int priority) {
|
|
+ synchronized (neighborPriorities) {
|
|
+ neighborPriorities.put(requester.location.pair(), 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 (getCurrentPriority() != getDemandedPriority()) this.chunkMap.queueHolderUpdate(this);
|
|
+ }
|
|
+
|
|
+ public final double getDistance(EntityPlayer player) {
|
|
+ return getDistance(player.locX(), player.locZ());
|
|
+ }
|
|
+ public final double getDistance(double blockX, double blockZ) {
|
|
+ int cx = MCUtil.fastFloor(blockX) >> 4;
|
|
+ int cz = MCUtil.fastFloor(blockZ) >> 4;
|
|
+ final double x = location.x - cx;
|
|
+ final double z = location.z - cz;
|
|
+ return (x * x) + (z * z);
|
|
+ }
|
|
+
|
|
+ public final double getDistanceFrom(BlockPosition pos) {
|
|
+ return getDistance(pos.getX(), pos.getZ());
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public String toString() {
|
|
+ return "PlayerChunk{" +
|
|
+ "location=" + location +
|
|
+ ", ticketLevel=" + ticketLevel + "/" + getChunkStatus(this.ticketLevel) +
|
|
+ ", chunkHolderStatus=" + getChunkHolderStatus() +
|
|
+ ", neighborPriority=" + getNeighborsPriority() +
|
|
+ ", priority=(" + ticketLevel + " - " + priorityBoost +" vs N " + neighborPriority + ") = " + getDemandedPriority() + " A " + getCurrentPriority() +
|
|
+ '}';
|
|
+ }
|
|
+ // Paper end
|
|
|
|
public PlayerChunk(ChunkCoordIntPair chunkcoordintpair, int i, LightEngine lightengine, PlayerChunk.c playerchunk_c, PlayerChunk.d playerchunk_d) {
|
|
this.statusFutures = new AtomicReferenceArray(PlayerChunk.CHUNK_STATUSES.size());
|
|
@@ -165,6 +280,15 @@ public class PlayerChunk {
|
|
}
|
|
return null;
|
|
}
|
|
+ public static ChunkStatus getNextStatus(ChunkStatus status) {
|
|
+ if (status == ChunkStatus.FULL) {
|
|
+ return status;
|
|
+ }
|
|
+ return CHUNK_STATUSES.get(status.getStatusIndex() + 1);
|
|
+ }
|
|
+ public CompletableFuture<Either<IChunkAccess, PlayerChunk.Failure>> getStatusFutureUncheckedMain(ChunkStatus chunkstatus) {
|
|
+ return MCUtil.ensureMain(getStatusFutureUnchecked(chunkstatus));
|
|
+ }
|
|
// Paper end
|
|
|
|
public CompletableFuture<Either<IChunkAccess, PlayerChunk.Failure>> getStatusFutureUnchecked(ChunkStatus chunkstatus) {
|
|
@@ -418,6 +542,7 @@ public class PlayerChunk {
|
|
return this.n;
|
|
}
|
|
|
|
+ private void setPriority(int i) { d(i); } // Paper - OBFHELPER
|
|
private void d(int i) {
|
|
this.n = i;
|
|
}
|
|
@@ -436,7 +561,7 @@ public class PlayerChunk {
|
|
// CraftBukkit start
|
|
// ChunkUnloadEvent: Called before the chunk is unloaded: isChunkLoaded is still true and chunk can still be modified by plugins.
|
|
if (playerchunk_state.isAtLeast(PlayerChunk.State.BORDER) && !playerchunk_state1.isAtLeast(PlayerChunk.State.BORDER)) {
|
|
- this.getStatusFutureUnchecked(ChunkStatus.FULL).thenAccept((either) -> {
|
|
+ this.getStatusFutureUncheckedMain(ChunkStatus.FULL).thenAccept((either) -> { // Paper - ensure main
|
|
Chunk chunk = (Chunk)either.left().orElse(null);
|
|
if (chunk != null) {
|
|
playerchunkmap.callbackExecutor.execute(() -> {
|
|
@@ -501,12 +626,13 @@ public class PlayerChunk {
|
|
if (!flag2 && flag3) {
|
|
// Paper start - cache ticking ready status
|
|
int expectCreateCount = ++this.fullChunkCreateCount;
|
|
- this.fullChunkFuture = playerchunkmap.b(this); this.fullChunkFuture.thenAccept((either) -> {
|
|
+ this.fullChunkFuture = playerchunkmap.b(this); MCUtil.ensureMain(this.fullChunkFuture).thenAccept((either) -> { // Paper - ensure main
|
|
if (either.left().isPresent() && PlayerChunk.this.fullChunkCreateCount == expectCreateCount) {
|
|
// note: Here is a very good place to add callbacks to logic waiting on this.
|
|
Chunk fullChunk = either.left().get();
|
|
PlayerChunk.this.isFullChunkReady = true;
|
|
fullChunk.playerChunk = PlayerChunk.this;
|
|
+ this.chunkMap.chunkDistanceManager.clearPriorityTickets(location);
|
|
|
|
|
|
}
|
|
@@ -531,7 +657,7 @@ public class PlayerChunk {
|
|
|
|
if (!flag4 && flag5) {
|
|
// Paper start - cache ticking ready status
|
|
- this.tickingFuture = playerchunkmap.a(this); this.tickingFuture.thenAccept((either) -> {
|
|
+ this.tickingFuture = playerchunkmap.a(this); MCUtil.ensureMain(this.tickingFuture).thenAccept((either) -> { // Paper - ensure main
|
|
if (either.left().isPresent()) {
|
|
// note: Here is a very good place to add callbacks to logic waiting on this.
|
|
Chunk tickingChunk = either.left().get();
|
|
@@ -562,7 +688,7 @@ public class PlayerChunk {
|
|
}
|
|
|
|
// Paper start - cache ticking ready status
|
|
- this.entityTickingFuture = playerchunkmap.b(this.location); this.entityTickingFuture.thenAccept((either) -> {
|
|
+ this.entityTickingFuture = playerchunkmap.b(this.location); MCUtil.ensureMain(this.entityTickingFuture).thenAccept((either) -> { // Paper ensureMain
|
|
if (either.left().isPresent()) {
|
|
// note: Here is a very good place to add callbacks to logic waiting on this.
|
|
Chunk entityTickingChunk = either.left().get();
|
|
@@ -581,13 +707,29 @@ public class PlayerChunk {
|
|
this.entityTickingFuture.complete(PlayerChunk.UNLOADED_CHUNK); this.isEntityTickingReady = false; // Paper - cache chunk ticking stage
|
|
this.entityTickingFuture = PlayerChunk.UNLOADED_CHUNK_FUTURE;
|
|
}
|
|
-
|
|
- this.w.a(this.location, this::k, this.ticketLevel, this::d);
|
|
+ // Paper start - raise IO/load priority if priority changes, use our preferred priority
|
|
+ priorityBoost = chunkMap.chunkDistanceManager.getChunkPriority(location);
|
|
+ int priority = getDemandedPriority();
|
|
+ if (getCurrentPriority() > 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.world.asyncChunkTaskManager.raisePriority(location.x, location.z, ioPriority);
|
|
+ }
|
|
+ if (getCurrentPriority() != priority) {
|
|
+ this.w.a(this.location, this::getCurrentPriority, priority, this::setPriority); // 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.isAtLeast(PlayerChunk.State.BORDER) && playerchunk_state1.isAtLeast(PlayerChunk.State.BORDER)) {
|
|
- this.getStatusFutureUnchecked(ChunkStatus.FULL).thenAccept((either) -> {
|
|
+ this.getStatusFutureUncheckedMain(ChunkStatus.FULL).thenAccept((either) -> { // Paper - ensure main
|
|
Chunk chunk = (Chunk)either.left().orElse(null);
|
|
if (chunk != null) {
|
|
playerchunkmap.callbackExecutor.execute(() -> {
|
|
@@ -669,6 +811,7 @@ public class PlayerChunk {
|
|
|
|
public interface c {
|
|
|
|
+ default void changePriority(ChunkCoordIntPair chunkcoordintpair, IntSupplier intsupplier, int i, IntConsumer intconsumer) { a(chunkcoordintpair, intsupplier, i, intconsumer); } // Paper - OBFHELPER
|
|
void a(ChunkCoordIntPair chunkcoordintpair, IntSupplier intsupplier, int i, IntConsumer intconsumer);
|
|
}
|
|
|
|
diff --git a/src/main/java/net/minecraft/server/PlayerChunkMap.java b/src/main/java/net/minecraft/server/PlayerChunkMap.java
|
|
index 0aa14bfca6e1845eb6e9f5bd4e0e36335fa7f532..6c0dbad8e06b02b32dcff518cc2a5f7c8c1c316c 100644
|
|
--- a/src/main/java/net/minecraft/server/PlayerChunkMap.java
|
|
+++ b/src/main/java/net/minecraft/server/PlayerChunkMap.java
|
|
@@ -50,6 +50,7 @@ import org.apache.commons.lang3.mutable.MutableBoolean;
|
|
import org.apache.logging.log4j.LogManager;
|
|
import org.apache.logging.log4j.Logger;
|
|
import org.bukkit.entity.Player; // CraftBukkit
|
|
+import org.spigotmc.AsyncCatcher;
|
|
|
|
public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
|
|
|
|
@@ -123,6 +124,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
|
|
|
|
@Override
|
|
public void execute(Runnable runnable) {
|
|
+ AsyncCatcher.catchOp("Callback Executor execute");
|
|
if (queued == null) {
|
|
queued = new java.util.ArrayDeque<>();
|
|
}
|
|
@@ -131,6 +133,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
|
|
|
|
@Override
|
|
public void run() {
|
|
+ AsyncCatcher.catchOp("Callback Executor run");
|
|
if (queued == null) {
|
|
return;
|
|
}
|
|
@@ -375,6 +378,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
|
|
this.playerViewDistanceTickMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets,
|
|
(EntityPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ,
|
|
com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> newState) -> {
|
|
+ checkHighPriorityChunks(player);
|
|
if (newState.size() != 1) {
|
|
return;
|
|
}
|
|
@@ -393,7 +397,8 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
|
|
}
|
|
ChunkCoordIntPair chunkPos = new ChunkCoordIntPair(rangeX, rangeZ);
|
|
PlayerChunkMap.this.world.getChunkProvider().removeTicketAtLevel(TicketType.PLAYER, chunkPos, 31, chunkPos); // entity ticking level, TODO check on update
|
|
- });
|
|
+ PlayerChunkMap.this.world.getChunkProvider().clearPriorityTickets(chunkPos);
|
|
+ }, (player, prevPos, newPos) -> checkHighPriorityChunks(player));
|
|
this.playerViewDistanceNoTickMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets);
|
|
this.playerViewDistanceBroadcastMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets,
|
|
(EntityPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ,
|
|
@@ -410,6 +415,101 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
|
|
});
|
|
// Paper end - no-tick view distance
|
|
}
|
|
+ // Paper start - Chunk Prioritization
|
|
+ public void queueHolderUpdate(PlayerChunk playerchunk) {
|
|
+ Runnable runnable = () -> {
|
|
+ if (isUnloading(playerchunk)) {
|
|
+ return; // unloaded
|
|
+ }
|
|
+ chunkDistanceManager.pendingChunkUpdates.add(playerchunk);
|
|
+ if (!chunkDistanceManager.pollingPendingChunkUpdates) {
|
|
+ world.getChunkProvider().tickDistanceManager();
|
|
+ }
|
|
+ };
|
|
+ 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 {
|
|
+ executor.execute(runnable);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private boolean isUnloading(PlayerChunk playerchunk) {
|
|
+ return playerchunk == null || unloadQueue.contains(playerchunk.location.pair());
|
|
+ }
|
|
+
|
|
+ public void checkHighPriorityChunks(EntityPlayer player) {
|
|
+ int currentTick = MinecraftServer.currentTick;
|
|
+ if (currentTick - player.lastHighPriorityChecked < 20) {
|
|
+ return;
|
|
+ }
|
|
+ player.lastHighPriorityChecked = currentTick;
|
|
+
|
|
+ int viewDistance = getEffectiveNoTickViewDistance();
|
|
+ chunkDistanceManager.delayDistanceManagerTick = true;
|
|
+ BlockPosition.PooledBlockPosition pos = BlockPosition.PooledBlockPosition.acquire();
|
|
+
|
|
+ // Prioritize circular near
|
|
+ double playerChunkX = MathHelper.floor(player.locX()) >> 4;
|
|
+ double playerChunkZ = MathHelper.floor(player.locZ()) >> 4;
|
|
+ pos.setValues(player.locX(), 0, player.locZ());
|
|
+ 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 * 4) {
|
|
+ chunkDistanceManager.markHighPriority(coord, (int) (27 - Math.sqrt(dist)));
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ // Prioritize nearby chunks
|
|
+ chunkDistanceManager.markHighPriority(coord, (int) (16 - Math.sqrt(dist*(2D/3D))));
|
|
+ });
|
|
+
|
|
+ // Prioritize Frustum near 3
|
|
+ ChunkCoordIntPair front3 = player.getChunkInFront(3);
|
|
+ pos.setValues(front3.x << 4, 0, front3.z << 4);
|
|
+ MCUtil.getSpiralOutChunks(pos, Math.min(5, viewDistance)).forEach(coord -> {
|
|
+ if (shouldSkipPrioritization(coord)) return;
|
|
+
|
|
+ chunkDistanceManager.markHighPriority(coord, 26);
|
|
+ });
|
|
+
|
|
+ // Prioritize Frustum near 5
|
|
+ if (viewDistance > 4) {
|
|
+ ChunkCoordIntPair front5 = player.getChunkInFront(5);
|
|
+ pos.setValues(front5.x << 4, 0, front5.z << 4);
|
|
+ MCUtil.getSpiralOutChunks(pos, 4).forEach(coord -> {
|
|
+ if (shouldSkipPrioritization(coord)) return;
|
|
+
|
|
+ chunkDistanceManager.markHighPriority(coord, 20);
|
|
+ });
|
|
+ }
|
|
+
|
|
+ // Prioritize Frustum far 7
|
|
+ if (viewDistance > 6) {
|
|
+ ChunkCoordIntPair front7 = player.getChunkInFront(7);
|
|
+ pos.setValues(front7.x << 4, 0, front7.z << 4);
|
|
+ MCUtil.getSpiralOutChunks(pos, 3).forEach(coord -> {
|
|
+ if (shouldSkipPrioritization(coord)) {
|
|
+ return;
|
|
+ }
|
|
+ chunkDistanceManager.markHighPriority(coord, 15);
|
|
+ });
|
|
+ }
|
|
+
|
|
+ pos.close();
|
|
+ chunkDistanceManager.delayDistanceManagerTick = false;
|
|
+ world.getChunkProvider().tickDistanceManager();
|
|
+ }
|
|
+
|
|
+ private boolean shouldSkipPrioritization(ChunkCoordIntPair coord) {
|
|
+ if (playerViewDistanceNoTickMap.getObjectsInRange(coord.pair()) == null) return true;
|
|
+ PlayerChunk chunk = getUpdatingChunk(coord.pair());
|
|
+ return chunk != null && (chunk.isFullChunkReady());
|
|
+ }
|
|
+ // Paper end
|
|
|
|
public void updatePlayerMobTypeMap(Entity entity) {
|
|
if (!this.world.paperConfig.perPlayerMobSpawns) {
|
|
@@ -539,6 +639,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
|
|
List<CompletableFuture<Either<IChunkAccess, PlayerChunk.Failure>>> list = Lists.newArrayList();
|
|
int j = chunkcoordintpair.x;
|
|
int k = chunkcoordintpair.z;
|
|
+ PlayerChunk requestingNeighbor = getUpdatingChunk(chunkcoordintpair.pair()); // Paper
|
|
|
|
for (int l = -i; l <= i; ++l) {
|
|
for (int i1 = -i; i1 <= i; ++i1) {
|
|
@@ -557,6 +658,14 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
|
|
|
|
ChunkStatus chunkstatus = (ChunkStatus) intfunction.apply(j1);
|
|
CompletableFuture<Either<IChunkAccess, PlayerChunk.Failure>> completablefuture = playerchunk.a(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);
|
|
}
|
|
@@ -1022,14 +1131,22 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
|
|
};
|
|
|
|
CompletableFuture<NBTTagCompound> chunkSaveFuture = this.world.asyncChunkTaskManager.getChunkSaveFuture(chunkcoordintpair.x, chunkcoordintpair.z);
|
|
+ PlayerChunk playerChunk = getUpdatingChunk(chunkcoordintpair.pair());
|
|
+ int chunkPriority = playerChunk != null ? playerChunk.getCurrentPriority() : 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;
|
|
if (chunkSaveFuture != null) {
|
|
- this.world.asyncChunkTaskManager.scheduleChunkLoad(chunkcoordintpair.x, chunkcoordintpair.z,
|
|
- com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGH_PRIORITY, chunkHolderConsumer, false, chunkSaveFuture);
|
|
- this.world.asyncChunkTaskManager.raisePriority(chunkcoordintpair.x, chunkcoordintpair.z, com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGH_PRIORITY);
|
|
+ this.world.asyncChunkTaskManager.scheduleChunkLoad(chunkcoordintpair.x, chunkcoordintpair.z, priority, chunkHolderConsumer, isHighestPriority, chunkSaveFuture);
|
|
} else {
|
|
- this.world.asyncChunkTaskManager.scheduleChunkLoad(chunkcoordintpair.x, chunkcoordintpair.z,
|
|
- com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY, chunkHolderConsumer, false);
|
|
+ this.world.asyncChunkTaskManager.scheduleChunkLoad(chunkcoordintpair.x, chunkcoordintpair.z, priority, chunkHolderConsumer, isHighestPriority);
|
|
}
|
|
+ this.world.asyncChunkTaskManager.raisePriority(chunkcoordintpair.x, chunkcoordintpair.z, priority);
|
|
return ret;
|
|
// Paper end
|
|
}
|
|
@@ -1158,7 +1275,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
|
|
long i = playerchunk.i().pair();
|
|
|
|
playerchunk.getClass();
|
|
- mailbox.a(ChunkTaskQueueSorter.a(runnable, i, playerchunk::getTicketLevel)); // CraftBukkit - decompile error
|
|
+ mailbox.a(ChunkTaskQueueSorter.a(runnable, i, () -> 1)); // CraftBukkit - decompile error // Paper - final loads are always urgent!
|
|
});
|
|
}
|
|
|
|
diff --git a/src/main/java/net/minecraft/server/PlayerConnection.java b/src/main/java/net/minecraft/server/PlayerConnection.java
|
|
index d52fbda79fe1c52d3ddb53c0f1c1f521d7620702..7123e197c7ed01afd4fbf7aa0760611373039a13 100644
|
|
--- a/src/main/java/net/minecraft/server/PlayerConnection.java
|
|
+++ b/src/main/java/net/minecraft/server/PlayerConnection.java
|
|
@@ -1277,6 +1277,7 @@ public class PlayerConnection implements PacketListenerPlayIn {
|
|
// CraftBukkit end
|
|
|
|
this.A = this.e;
|
|
+ this.player.getWorldServer().getChunkProvider().markAreaHighPriority(new ChunkCoordIntPair(MathHelper.floor(d1) >> 4, MathHelper.floor(d3) >> 4), 28, 3); // Paper - load area high priority
|
|
this.player.setLocation(d0, d1, d2, f, f1);
|
|
this.syncPosition(); // Paper
|
|
this.player.playerConnection.sendPacket(new PacketPlayOutPosition(d0 - d3, d1 - d4, d2 - d5, f - f2, f1 - f3, set, this.teleportAwait));
|
|
diff --git a/src/main/java/net/minecraft/server/PlayerList.java b/src/main/java/net/minecraft/server/PlayerList.java
|
|
index 6daca5c0ffd1d84f9a25cd106e8992a055dfb912..b0585346bf5125bebc482246bbb91c4b08c55816 100644
|
|
--- a/src/main/java/net/minecraft/server/PlayerList.java
|
|
+++ b/src/main/java/net/minecraft/server/PlayerList.java
|
|
@@ -174,8 +174,8 @@ public abstract class PlayerList {
|
|
final ChunkCoordIntPair pos = new ChunkCoordIntPair(chunkX, chunkZ);
|
|
PlayerChunkMap playerChunkMap = finalWorldserver.getChunkProvider().playerChunkMap;
|
|
playerChunkMap.chunkDistanceManager.addTicketAtLevel(TicketType.LOGIN, pos, 31, pos.pair());
|
|
- worldserver.getChunkProvider().tickDistanceManager();
|
|
- worldserver.getChunkProvider().getChunkAtAsynchronously(chunkX, chunkZ, true, true).thenApply(chunk -> {
|
|
+ worldserver.getChunkProvider().markAreaHighPriority(pos, 28, 3);
|
|
+ worldserver.getChunkProvider().getChunkAtAsynchronously(chunkX, chunkZ, true, false).thenApply(chunk -> {
|
|
PlayerChunk updatingChunk = playerChunkMap.getUpdatingChunk(pos.pair());
|
|
if (updatingChunk != null) {
|
|
return updatingChunk.getEntityTickingFuture();
|
|
@@ -188,7 +188,6 @@ public abstract class PlayerList {
|
|
entityplayer, finalWorldserver, networkmanager, playerconnection,
|
|
nbttagcompound, networkmanager.getSocketAddress().toString(), lastKnownName
|
|
);
|
|
- //playerChunkMap.chunkDistanceManager.removeTicketAtLevel(TicketType.LOGIN, pos, 31, pos.pair());
|
|
};
|
|
});
|
|
}
|
|
@@ -764,6 +763,7 @@ public abstract class PlayerList {
|
|
// CraftBukkit end
|
|
|
|
worldserver.getChunkProvider().addTicket(TicketType.POST_TELEPORT, new ChunkCoordIntPair(location.getBlockX() >> 4, location.getBlockZ() >> 4), 1, entityplayer.getId()); // Paper
|
|
+ worldserver.getChunkProvider().markAreaHighPriority(new ChunkCoordIntPair(location.getBlockX() >> 4, location.getBlockZ() >> 4), 28, 3); // Paper - load area at high priority
|
|
while (avoidSuffocation && !worldserver.getCubes(entityplayer1) && entityplayer1.locY() < 256.0D) {
|
|
entityplayer1.setPosition(entityplayer1.locX(), entityplayer1.locY() + 1.0D, entityplayer1.locZ());
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/server/Ticket.java b/src/main/java/net/minecraft/server/Ticket.java
|
|
index 7a8397815a5b7f79f3e3a0348aeedf63fe879f8f..0d6e0f2ddaa85c04e626980591e9a78ac27fb42d 100644
|
|
--- a/src/main/java/net/minecraft/server/Ticket.java
|
|
+++ b/src/main/java/net/minecraft/server/Ticket.java
|
|
@@ -8,6 +8,7 @@ public final class Ticket<T> implements Comparable<Ticket<?>> {
|
|
private final int b;
|
|
public final T identifier; public final T getObjectReason() { return this.identifier; } // Paper - OBFHELPER
|
|
private long d; public final long getCreationTick() { return this.d; } // Paper - OBFHELPER
|
|
+ public int priority = 0; // Paper
|
|
|
|
protected Ticket(TicketType<T> tickettype, int i, T t0) {
|
|
this.a = tickettype;
|
|
diff --git a/src/main/java/net/minecraft/server/TicketType.java b/src/main/java/net/minecraft/server/TicketType.java
|
|
index d7b9d9fd3a3b607278a3d72b0b306b0be2aa30ad..6fd852db6bcfbfbf84ec2acf6d23b08a6051165c 100644
|
|
--- a/src/main/java/net/minecraft/server/TicketType.java
|
|
+++ b/src/main/java/net/minecraft/server/TicketType.java
|
|
@@ -24,6 +24,8 @@ public class TicketType<T> {
|
|
public static final TicketType<org.bukkit.plugin.Plugin> PLUGIN_TICKET = a("plugin_ticket", (plugin1, plugin2) -> plugin1.getClass().getName().compareTo(plugin2.getClass().getName())); // CraftBukkit
|
|
public static final TicketType<Long> FUTURE_AWAIT = a("future_await", Long::compareTo); // Paper
|
|
public static final TicketType<Long> ASYNC_LOAD = a("async_load", Long::compareTo); // Paper
|
|
+ public static final TicketType<ChunkCoordIntPair> PRIORITY = a("priority", Comparator.comparingLong(ChunkCoordIntPair::pair), 300); // Paper
|
|
+ public static final TicketType<ChunkCoordIntPair> URGENT = a("urgent", Comparator.comparingLong(ChunkCoordIntPair::pair), 300); // Paper
|
|
|
|
public static <T> TicketType<T> a(String s, Comparator<T> comparator) {
|
|
return new TicketType<>(s, comparator, 0L);
|
|
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
|
|
index 5f180bb77b736220c848357b2cac4d0d2b99e3df..d3c5b7d1904a6cbd65db406639ed2ba90ec9fd2a 100644
|
|
--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
|
|
+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
|
|
@@ -2484,6 +2484,10 @@ public class CraftWorld implements World {
|
|
return future;
|
|
}
|
|
|
|
+ if (!urgent) {
|
|
+ // if not urgent, at least use a slightly boosted priority
|
|
+ world.getChunkProvider().markHighPriority(new ChunkCoordIntPair(x, z), 1);
|
|
+ }
|
|
return this.world.getChunkProvider().getChunkAtAsynchronously(x, z, gen, urgent).thenComposeAsync((either) -> {
|
|
net.minecraft.server.Chunk chunk = (net.minecraft.server.Chunk) either.left().orElse(null);
|
|
return CompletableFuture.completedFuture(chunk == null ? null : chunk.getBukkitChunk());
|
|
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
|
|
index bd4d5184b607db09c8ff2687ceaf47fb94368a28..141003ca718cc6ba113de8e4855b18252b293ef6 100644
|
|
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
|
|
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
|
|
@@ -738,6 +738,14 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
|
|
throw new UnsupportedOperationException("Cannot set rotation of players. Consider teleporting instead.");
|
|
}
|
|
|
|
+ // Paper start
|
|
+ @Override
|
|
+ public java.util.concurrent.CompletableFuture<Boolean> teleportAsync(Location loc, PlayerTeleportEvent.TeleportCause cause) {
|
|
+ getHandle().getWorldServer().getChunkProvider().markAreaHighPriority(new net.minecraft.server.ChunkCoordIntPair(net.minecraft.server.MathHelper.floor(loc.getX()) >> 4, net.minecraft.server.MathHelper.floor(loc.getZ()) >> 4), 28, 3); // Paper - 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");
|