From a096540d9464a01d2e645ca5a62c7eb596da772b Mon Sep 17 00:00:00 2001 From: Spottedleaf Date: Thu, 31 Mar 2022 06:04:23 -0700 Subject: [PATCH] Add per player chunk loading limits Configurable under "settings.chunk-loading.player-max-chunk-load-rate", defaults to -1. This commit also changes the chunk loading to be distributed equally for all players, rather than distance based. This is to ensure players flying around do not take priority over everyone else. The exception to this new rule is the min-load-radius, which still has priority over everything else. --- ...cle-movement-from-players-while-tele.patch | 2 +- ...assenger-world-matches-ridden-entity.patch | 23 ++++++++++ ...ard-against-invalid-entity-positions.patch | 45 +++++++++++++++++++ patches/server/MC-Utils.patch | 45 ++++++++++++------- ...efault-CustomSpawners-in-custom-worl.patch | 2 +- .../Replace-player-chunk-loader-system.patch | 26 ++++++++++- 6 files changed, 124 insertions(+), 19 deletions(-) create mode 100644 patches/server/Ensure-entity-passenger-world-matches-ridden-entity.patch create mode 100644 patches/server/Guard-against-invalid-entity-positions.patch diff --git a/patches/server/Don-t-allow-vehicle-movement-from-players-while-tele.patch b/patches/server/Don-t-allow-vehicle-movement-from-players-while-tele.patch index 2ff188fa0a..3f4ecbc0eb 100644 --- a/patches/server/Don-t-allow-vehicle-movement-from-players-while-tele.patch +++ b/patches/server/Don-t-allow-vehicle-movement-from-players-while-tele.patch @@ -15,7 +15,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 Entity entity = this.player.getRootVehicle(); + // Paper start -+ if (this.awaitingPositionFromClient != null) { ++ if (this.awaitingPositionFromClient != null || this.player.isImmobile() || entity.isRemoved()) { + return; + } + // Paper end diff --git a/patches/server/Ensure-entity-passenger-world-matches-ridden-entity.patch b/patches/server/Ensure-entity-passenger-world-matches-ridden-entity.patch new file mode 100644 index 0000000000..64e58a0a7a --- /dev/null +++ b/patches/server/Ensure-entity-passenger-world-matches-ridden-entity.patch @@ -0,0 +1,23 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Thu, 31 Mar 2022 05:11:37 -0700 +Subject: [PATCH] Ensure entity passenger world matches ridden entity + +Bad plugins doing this would cause some obvious problems... + +diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/net/minecraft/world/entity/Entity.java ++++ b/src/main/java/net/minecraft/world/entity/Entity.java +@@ -0,0 +0,0 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + } + + protected boolean addPassenger(Entity entity) { // CraftBukkit ++ // Paper start ++ if (entity.level != this.level) { ++ throw new IllegalArgumentException("Entity passenger world must match"); ++ } ++ // Paper end + if (entity == this) throw new IllegalArgumentException("Entities cannot become a passenger of themselves"); // Paper - issue 572 + if (entity.getVehicle() != this) { + throw new IllegalStateException("Use x.startRiding(y), not y.addPassenger(x)"); diff --git a/patches/server/Guard-against-invalid-entity-positions.patch b/patches/server/Guard-against-invalid-entity-positions.patch new file mode 100644 index 0000000000..b4c9556cbb --- /dev/null +++ b/patches/server/Guard-against-invalid-entity-positions.patch @@ -0,0 +1,45 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Thu, 31 Mar 2022 05:18:28 -0700 +Subject: [PATCH] Guard against invalid entity positions + +Anything not finite should be blocked and logged + +diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/net/minecraft/world/entity/Entity.java ++++ b/src/main/java/net/minecraft/world/entity/Entity.java +@@ -0,0 +0,0 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + return this.getZ((2.0D * this.random.nextDouble() - 1.0D) * widthScale); + } + ++ // Paper start - block invalid positions ++ public static boolean checkPosition(Entity entity, double newX, double newY, double newZ) { ++ if (Double.isFinite(newX) && Double.isFinite(newY) && Double.isFinite(newZ)) { ++ return true; ++ } ++ ++ String entityInfo = null; ++ try { ++ entityInfo = entity.toString(); ++ } catch (Exception ex) { ++ entityInfo = "[Entity info unavailable] "; ++ } ++ LOGGER.error("New entity position is invalid! Tried to set invalid position (" + newX + "," + newY + "," + newZ + ") for entity " + entity.getClass().getName() + " located at " + entity.position + ", entity info: " + entityInfo, new Throwable()); ++ return false; ++ } ++ // Paper end - block invalid positions ++ + public final void setPosRaw(double x, double y, double z) { + // Paper start + this.setPosRaw(x, y, z, false); + } + public final void setPosRaw(double x, double y, double z, boolean forceBoundingBoxUpdate) { ++ // Paper start - block invalid positions ++ if (!checkPosition(this, x, y, z)) { ++ return; ++ } ++ // Paper end - block invalid positions + // Paper end + // Paper start - fix MC-4 + if (this instanceof ItemEntity) { diff --git a/patches/server/MC-Utils.patch b/patches/server/MC-Utils.patch index ca92ec1be3..8235d1b0e6 100644 --- a/patches/server/MC-Utils.patch +++ b/patches/server/MC-Utils.patch @@ -2899,14 +2899,16 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 +public final class IntervalledCounter { + + protected long[] times; ++ protected long[] counts; + protected final long interval; + protected long minTime; -+ protected int sum; ++ protected long sum; + protected int head; // inclusive + protected int tail; // exclusive + + public IntervalledCounter(final long interval) { + this.times = new long[8]; ++ this.counts = new long[8]; + this.interval = interval; + } + @@ -2915,7 +2917,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + } + + public void updateCurrentTime(final long currentTime) { -+ int sum = this.sum; ++ long sum = this.sum; + int head = this.head; + final int tail = this.tail; + final long minTime = currentTime - this.interval; @@ -2924,8 +2926,15 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + + // guard against overflow by using subtraction + while (head != tail && this.times[head] - minTime < 0) { -+ head = (head + 1) % arrayLen; -+ --sum; ++ sum -= this.counts[head]; ++ // there are two ways we can do this: ++ // 1. free the count when adding ++ // 2. free it now ++ // option #2 ++ this.counts[head] = 0; ++ if (++head >= arrayLen) { ++ head = 0; ++ } + } + + this.sum = sum; @@ -2934,6 +2943,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + } + + public void addTime(final long currTime) { ++ this.addTime(currTime, 1L); ++ } ++ ++ public void addTime(final long currTime, final long count) { + // guard against overflow by using subtraction + if (currTime - this.minTime < 0) { + return; @@ -2945,28 +2958,29 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + } + + this.times[this.tail] = currTime; ++ this.counts[this.tail] += count; ++ this.sum += count; + this.tail = nextTail; + } + + public void updateAndAdd(final int count) { + final long currTime = System.nanoTime(); + this.updateCurrentTime(currTime); -+ for (int i = 0; i < count; ++i) { -+ this.addTime(currTime); -+ } ++ this.addTime(currTime, count); + } + + public void updateAndAdd(final int count, final long currTime) { + this.updateCurrentTime(currTime); -+ for (int i = 0; i < count; ++i) { -+ this.addTime(currTime); -+ } ++ this.addTime(currTime, count); + } + + private void resize() { + final long[] oldElements = this.times; ++ final long[] oldCounts = this.counts; + final long[] newElements = new long[this.times.length * 2]; ++ final long[] newCounts = new long[this.times.length * 2]; + this.times = newElements; ++ this.counts = newCounts; + + final int head = this.head; + final int tail = this.tail; @@ -2976,9 +2990,13 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + + if (tail >= head) { + System.arraycopy(oldElements, head, newElements, 0, size); ++ System.arraycopy(oldCounts, head, newCounts, 0, size); + } else { + System.arraycopy(oldElements, head, newElements, 0, oldElements.length - head); + System.arraycopy(oldElements, 0, newElements, oldElements.length - head, tail); ++ ++ System.arraycopy(oldCounts, head, newCounts, 0, oldCounts.length - head); ++ System.arraycopy(oldCounts, 0, newCounts, oldCounts.length - head, tail); + } + } + @@ -2987,11 +3005,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + return this.size() / (this.interval * 1.0e-9); + } + -+ public int size() { -+ final int head = this.head; -+ final int tail = this.tail; -+ -+ return tail >= head ? (tail - head) : (tail + (this.times.length - head)); ++ public long size() { ++ return this.sum; + } +} diff --git a/src/main/java/io/papermc/paper/util/WorldUtil.java b/src/main/java/io/papermc/paper/util/WorldUtil.java diff --git a/patches/server/Option-to-have-default-CustomSpawners-in-custom-worl.patch b/patches/server/Option-to-have-default-CustomSpawners-in-custom-worl.patch index a4fd7ab9ba..3f607945ff 100644 --- a/patches/server/Option-to-have-default-CustomSpawners-in-custom-worl.patch +++ b/patches/server/Option-to-have-default-CustomSpawners-in-custom-worl.patch @@ -14,8 +14,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 --- a/src/main/java/com/destroystokyo/paper/PaperConfig.java +++ b/src/main/java/com/destroystokyo/paper/PaperConfig.java @@ -0,0 +0,0 @@ public class PaperConfig { - } globalMaxConcurrentChunkLoads = getDouble("settings.chunk-loading.global-max-concurrent-loads", 500.0); + playerMaxChunkLoadRate = getDouble("settings.chunk-loading.player-max-chunk-load-rate", -1.0); } + + public static boolean useDimensionTypeForCustomSpawners; diff --git a/patches/server/Replace-player-chunk-loader-system.patch b/patches/server/Replace-player-chunk-loader-system.patch index 98edb38cc5..e6504a2e54 100644 --- a/patches/server/Replace-player-chunk-loader-system.patch +++ b/patches/server/Replace-player-chunk-loader-system.patch @@ -102,6 +102,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + public static double globalMaxChunkLoadRate; + public static double playerMaxConcurrentChunkLoads; + public static double globalMaxConcurrentChunkLoads; ++ public static double playerMaxChunkLoadRate; + + private static void newPlayerChunkManagement() { + playerMinChunkLoadRadius = getInt("settings.chunk-loading.min-load-radius", 2); @@ -119,6 +120,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + set("settings.chunk-loading.player-max-concurrent-loads", playerMaxConcurrentChunkLoads = 20.0); + } + globalMaxConcurrentChunkLoads = getDouble("settings.chunk-loading.global-max-concurrent-loads", 500.0); ++ playerMaxChunkLoadRate = getDouble("settings.chunk-loading.player-max-chunk-load-rate", -1.0); + } } diff --git a/src/main/java/io/papermc/paper/chunk/PlayerChunkLoader.java b/src/main/java/io/papermc/paper/chunk/PlayerChunkLoader.java @@ -218,10 +220,16 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + + final int priorityCompare = Double.compare(holder1 == null ? Double.MAX_VALUE : holder1.priority, holder2 == null ? Double.MAX_VALUE : holder2.priority); + -+ if (priorityCompare != 0) { ++ final int lastLoadTimeCompare = Long.compare(p1.lastChunkLoad, p2.lastChunkLoad); ++ ++ if ((holder1 == null || holder2 == null || lastLoadTimeCompare == 0 || holder1.priority < 0.0 || holder2.priority < 0.0) && priorityCompare != 0) { + return priorityCompare; + } + ++ if (lastLoadTimeCompare != 0) { ++ return lastLoadTimeCompare; ++ } ++ + final int idCompare = Integer.compare(p1.player.getId(), p2.player.getId()); + + if (idCompare != 0) { @@ -744,6 +752,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + for (;;) { + final PlayerLoaderData data = this.chunkLoadQueue.pollFirst(); + ++ data.lastChunkLoad = time; ++ + final ChunkPriorityHolder queuedLoad = data.loadQueue.peekFirst(); + if (queuedLoad == null) { + if (this.chunkLoadQueue.isEmpty()) { @@ -756,6 +766,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + updatedCounters = true; + TICKET_ADDITION_COUNTER_SHORT.updateCurrentTime(time); + TICKET_ADDITION_COUNTER_LONG.updateCurrentTime(time); ++ data.ticketAdditionCounterShort.updateCurrentTime(time); ++ data.ticketAdditionCounterLong.updateCurrentTime(time); + } + + if (this.isChunkPlayerLoaded(queuedLoad.chunkX, queuedLoad.chunkZ)) { @@ -791,7 +803,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + // priority >= 0.0 implies rate limited chunks + + final int currentChunkLoads = this.concurrentChunkLoads; -+ if (currentChunkLoads >= maxLoads || (PaperConfig.globalMaxChunkLoadRate > 0 && (TICKET_ADDITION_COUNTER_SHORT.getRate() >= PaperConfig.globalMaxChunkLoadRate || TICKET_ADDITION_COUNTER_LONG.getRate() >= PaperConfig.globalMaxChunkLoadRate))) { ++ if (currentChunkLoads >= maxLoads || (PaperConfig.globalMaxChunkLoadRate > 0 && (TICKET_ADDITION_COUNTER_SHORT.getRate() >= PaperConfig.globalMaxChunkLoadRate || TICKET_ADDITION_COUNTER_LONG.getRate() >= PaperConfig.globalMaxChunkLoadRate)) ++ || (PaperConfig.playerMaxChunkLoadRate > 0.0 && (data.ticketAdditionCounterShort.getRate() >= PaperConfig.playerMaxChunkLoadRate || data.ticketAdditionCounterLong.getRate() >= PaperConfig.playerMaxChunkLoadRate))) { + // don't poll, we didn't load it + this.chunkLoadQueue.add(data); + break; @@ -821,6 +834,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + ++this.concurrentChunkLoads; + TICKET_ADDITION_COUNTER_SHORT.addTime(time); + TICKET_ADDITION_COUNTER_LONG.addTime(time); ++ data.ticketAdditionCounterShort.addTime(time); ++ data.ticketAdditionCounterLong.addTime(time); + } + } + } @@ -926,6 +941,13 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + + protected long nextChunkSendTarget; + ++ // this interval prevents bursting a lot of chunk loads ++ protected final IntervalledCounter ticketAdditionCounterShort = new IntervalledCounter((long)(1.0e6 * 50.0)); // 50ms ++ // this ensures the rate is kept between ticks correctly ++ protected final IntervalledCounter ticketAdditionCounterLong = new IntervalledCounter((long)(1.0e6 * 1000.0)); // 1000ms ++ ++ public long lastChunkLoad; ++ + public PlayerLoaderData(final ServerPlayer player, final PlayerChunkLoader loader) { + this.player = player; + this.loader = loader;