even even even even even even even even even even even even even even even even even even even even even even even even even even even even even even even even even even even even even even even even even even even even even even even more patches

This commit is contained in:
Jason Penilla 2021-11-24 21:13:29 -08:00 committed by MiniDigger | Martin
parent fdde23eecf
commit dc58f85df2
14 changed files with 227 additions and 267 deletions

View file

@ -0,0 +1,40 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Spottedleaf <Spottedleaf@users.noreply.github.com>
Date: Sun, 20 Jun 2021 00:08:13 -0700
Subject: [PATCH] Do not allow ticket level changes when updating chunk ticking
state
This WILL cause state corruption if it happens. So, don't
allow it.
diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java
index be8e63fb3e5c65157ea4ed9c0e3910aaba8c3d45..7418245d5d08706ca2a1378e769abfb0de1076ed 100644
--- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
+++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java
@@ -385,7 +385,13 @@ public class ChunkHolder {
CompletableFuture<Void> completablefuture1 = new CompletableFuture();
completablefuture1.thenRunAsync(() -> {
+ // Paper start - do not allow ticket level changes
+ boolean unloadingBefore = this.chunkMap.unloadingPlayerChunk;
+ this.chunkMap.unloadingPlayerChunk = true;
+ try {
+ // Paper end - do not allow ticket level changes
playerchunkmap.onFullChunkStatusChange(this.pos, playerchunk_state);
+ } finally { this.chunkMap.unloadingPlayerChunk = unloadingBefore; } // Paper - do not allow ticket level changes
}, executor);
this.pendingFullStateConfirmation = completablefuture1;
completablefuture.thenAccept((either) -> {
@@ -397,7 +403,12 @@ public class ChunkHolder {
private void demoteFullChunk(ChunkMap playerchunkmap, ChunkHolder.FullChunkStatus playerchunk_state) {
this.pendingFullStateConfirmation.cancel(false);
+ // Paper start - do not allow ticket level changes
+ boolean unloadingBefore = this.chunkMap.unloadingPlayerChunk;
+ this.chunkMap.unloadingPlayerChunk = true;
+ try { // Paper end - do not allow ticket level changes
playerchunkmap.onFullChunkStatusChange(this.pos, playerchunk_state);
+ } finally { this.chunkMap.unloadingPlayerChunk = unloadingBefore; } // Paper - do not allow ticket level changes
}
protected long updateCount; // Paper - correctly handle recursion

View file

@ -0,0 +1,64 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Spottedleaf <Spottedleaf@users.noreply.github.com>
Date: Sun, 8 Aug 2021 16:26:46 -0700
Subject: [PATCH] Do not submit profile lookups to worldgen threads
They block. On network I/O.
If enough tasks are submitted the server will eventually stall
out due to a sync load, as the worldgen threads will be
stalling on profile lookups.
diff --git a/src/main/java/net/minecraft/Util.java b/src/main/java/net/minecraft/Util.java
index 6288ffdaad147a10f25cce00c13820903b4cc7d7..f4b8dca0a3cbccb55b23b2408e9a17185fd2896f 100644
--- a/src/main/java/net/minecraft/Util.java
+++ b/src/main/java/net/minecraft/Util.java
@@ -70,6 +70,22 @@ public class Util {
private static final AtomicInteger WORKER_COUNT = new AtomicInteger(1);
private static final ExecutorService BOOTSTRAP_EXECUTOR = makeExecutor("Bootstrap", -2); // Paper - add -2 priority
private static final ExecutorService BACKGROUND_EXECUTOR = makeExecutor("Main", -1); // Paper - add -1 priority
+ // Paper start - don't submit BLOCKING PROFILE LOOKUPS to the world gen thread
+ public static final ExecutorService PROFILE_EXECUTOR = Executors.newFixedThreadPool(2, new java.util.concurrent.ThreadFactory() {
+
+ private final AtomicInteger count = new AtomicInteger();
+
+ @Override
+ public Thread newThread(Runnable run) {
+ Thread ret = new Thread(run);
+ ret.setName("Profile Lookup Executor #" + this.count.getAndIncrement());
+ ret.setUncaughtExceptionHandler((Thread thread, Throwable throwable) -> {
+ LOGGER.fatal("Uncaught exception in thread " + thread.getName(), throwable);
+ });
+ return ret;
+ }
+ });
+ // Paper end - don't submit BLOCKING PROFILE LOOKUPS to the world gen thread
private static final ExecutorService IO_POOL = makeIoExecutor();
public static LongSupplier timeSource = System::nanoTime;
public static final UUID NIL_UUID = new UUID(0L, 0L);
diff --git a/src/main/java/net/minecraft/server/players/GameProfileCache.java b/src/main/java/net/minecraft/server/players/GameProfileCache.java
index a157f71cf55b9e97fac56c7c55b552da86000fd5..4e2833dc941863cc54416c81f09c688b5616d7a4 100644
--- a/src/main/java/net/minecraft/server/players/GameProfileCache.java
+++ b/src/main/java/net/minecraft/server/players/GameProfileCache.java
@@ -200,7 +200,7 @@ public class GameProfileCache {
} else {
this.requests.put(username, CompletableFuture.supplyAsync(() -> {
return this.get(username);
- }, Util.backgroundExecutor()).whenCompleteAsync((optional, throwable) -> {
+ }, Util.PROFILE_EXECUTOR).whenCompleteAsync((optional, throwable) -> { // Paper - not a good idea to use BLOCKING OPERATIONS on the worldgen executor
this.requests.remove(username);
}, this.executor).whenCompleteAsync((optional, throwable) -> {
consumer.accept(optional);
diff --git a/src/main/java/net/minecraft/world/level/block/entity/SkullBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/SkullBlockEntity.java
index e3efea8623c7d34915069a6b9b7da9f2b1694c28..118472b83a21a250f398c088c91ac4560c19c749 100644
--- a/src/main/java/net/minecraft/world/level/block/entity/SkullBlockEntity.java
+++ b/src/main/java/net/minecraft/world/level/block/entity/SkullBlockEntity.java
@@ -148,7 +148,7 @@ public class SkullBlockEntity extends BlockEntity {
public static void updateGameprofile(@Nullable GameProfile owner, Consumer<GameProfile> callback) {
if (owner != null && !StringUtil.isNullOrEmpty(owner.getName()) && (!owner.isComplete() || !owner.getProperties().containsKey("textures")) && profileCache != null && sessionService != null) {
profileCache.getAsync(owner.getName(), (profile) -> {
- Util.backgroundExecutor().execute(() -> {
+ Util.PROFILE_EXECUTOR.execute(() -> { // Paper - not a good idea to use BLOCKING OPERATIONS on the worldgen executor
Util.ifElse(profile, (profilex) -> {
Property property = Iterables.getFirst(profilex.getProperties().get("textures"), (Property)null);
if (property == null) {

View file

@ -0,0 +1,20 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Spottedleaf <Spottedleaf@users.noreply.github.com>
Date: Wed, 25 Aug 2021 20:17:12 -0700
Subject: [PATCH] Log when the async catcher is tripped
The chunk system can swallow the exception given it's all
built with completablefuture, so ensure it is at least printed.
diff --git a/src/main/java/org/spigotmc/AsyncCatcher.java b/src/main/java/org/spigotmc/AsyncCatcher.java
index 7585a30e8f063ac2656b5de519b1e9edaceffbc7..41ddd9e0517571c7bffb494766f7097198b50842 100644
--- a/src/main/java/org/spigotmc/AsyncCatcher.java
+++ b/src/main/java/org/spigotmc/AsyncCatcher.java
@@ -12,6 +12,7 @@ public class AsyncCatcher
{
if ( (AsyncCatcher.enabled || io.papermc.paper.util.TickThread.STRICT_THREAD_CHECKS) && Thread.currentThread() != MinecraftServer.getServer().serverThread ) // Paper
{
+ MinecraftServer.LOGGER.fatal("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); // Paper
throw new IllegalStateException( "Asynchronous " + reason + "!" );
}
}

View file

@ -0,0 +1,22 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: kennytv <jahnke.nassim@gmail.com>
Date: Thu, 26 Aug 2021 12:09:47 +0200
Subject: [PATCH] Sanitize ResourceLocation error logging
diff --git a/src/main/java/net/minecraft/resources/ResourceLocation.java b/src/main/java/net/minecraft/resources/ResourceLocation.java
index bf78cfc3b577e256ae68f219224447531619abd6..6e5d13e63c97cb95b93af1d997dc0eb53f966566 100644
--- a/src/main/java/net/minecraft/resources/ResourceLocation.java
+++ b/src/main/java/net/minecraft/resources/ResourceLocation.java
@@ -32,9 +32,9 @@ public class ResourceLocation implements Comparable<ResourceLocation> {
this.namespace = StringUtils.isEmpty(id[0]) ? "minecraft" : id[0];
this.path = id[1];
if (!isValidNamespace(this.namespace)) {
- throw new ResourceLocationException("Non [a-z0-9_.-] character in namespace of location: " + this.namespace + ":" + this.path);
+ throw new ResourceLocationException("Non [a-z0-9_.-] character in namespace of location: " + org.apache.commons.lang3.StringUtils.normalizeSpace(this.namespace) + ":" + org.apache.commons.lang3.StringUtils.normalizeSpace(this.path)); // Paper
} else if (!isValidPath(this.path)) {
- throw new ResourceLocationException("Non [a-z0-9/._-] character in path of location: " + this.namespace + ":" + this.path);
+ throw new ResourceLocationException("Non [a-z0-9/._-] character in path of location: " + this.namespace + ":" + org.apache.commons.lang3.StringUtils.normalizeSpace(this.path)); // Paper
}
}

View file

@ -0,0 +1,997 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Spottedleaf <Spottedleaf@users.noreply.github.com>
Date: Sun, 31 Jan 2021 02:29:24 -0800
Subject: [PATCH] Optimise general POI access
There are a couple of problems with mojang's POI code.
Firstly, it's all streams. Unsurprisingly, stacking
streams on top of each other is horrible for performance
and ultimately took up half of a villager's tick!
Secondly, sometime's the search radius is large and there are
a significant number of poi entries per chunk section. Even
removing streams at this point doesn't help much. The only solution
is to start at the search point and iterate outwards. This
type of approach shows massive gains for portals, simply because
we can avoid sync loading a large area of chunks. I also tested
a massive farm I found in JellySquid's discord, which showed
to benefit significantly simply because the farm had so many
portal blocks that searching through them all was very slow.
Great care has been taken so that behavior remains identical to
vanilla, however I cannot account for oddball Stream API
implementations, if they even exist (streams can technically
be loose with iteration order in a sorted stream given its
source stream is not tagged with ordered, and mojang does not
tag the source stream as ordered). However in my testing on openjdk
there showed no difference, as expected.
This patch also specifically optimises other areas of code to
use PoiAccess. For example, some villager AI and portaling code
had to be specifically modified.
diff --git a/src/main/java/io/papermc/paper/util/PoiAccess.java b/src/main/java/io/papermc/paper/util/PoiAccess.java
new file mode 100644
index 0000000000000000000000000000000000000000..0a88c60161b04a733151c15046358f4b3b8b3280
--- /dev/null
+++ b/src/main/java/io/papermc/paper/util/PoiAccess.java
@@ -0,0 +1,748 @@
+package io.papermc.paper.util;
+
+import it.unimi.dsi.fastutil.doubles.Double2ObjectMap;
+import it.unimi.dsi.fastutil.doubles.Double2ObjectRBTreeMap;
+import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue;
+import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
+import net.minecraft.core.BlockPos;
+import net.minecraft.util.Mth;
+import net.minecraft.world.entity.ai.village.poi.PoiManager;
+import net.minecraft.world.entity.ai.village.poi.PoiRecord;
+import net.minecraft.world.entity.ai.village.poi.PoiSection;
+import net.minecraft.world.entity.ai.village.poi.PoiType;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Predicate;
+
+/**
+ * Provides optimised access to POI data. All returned values will be identical to vanilla.
+ */
+public final class PoiAccess {
+
+ protected static double clamp(final double val, final double min, final double max) {
+ return (val < min ? min : (val > max ? max : val));
+ }
+
+ protected static double getSmallestDistanceSquared(final double boxMinX, final double boxMinY, final double boxMinZ,
+ final double boxMaxX, final double boxMaxY, final double boxMaxZ,
+
+ final double circleX, final double circleY, final double circleZ) {
+ // is the circle center inside the box?
+ if (circleX >= boxMinX && circleX <= boxMaxX && circleY >= boxMinY && circleY <= boxMaxY && circleZ >= boxMinZ && circleZ <= boxMaxZ) {
+ return 0.0;
+ }
+
+ final double boxWidthX = (boxMaxX - boxMinX) / 2.0;
+ final double boxWidthY = (boxMaxY - boxMinY) / 2.0;
+ final double boxWidthZ = (boxMaxZ - boxMinZ) / 2.0;
+
+ final double boxCenterX = (boxMinX + boxMaxX) / 2.0;
+ final double boxCenterY = (boxMinY + boxMaxY) / 2.0;
+ final double boxCenterZ = (boxMinZ + boxMaxZ) / 2.0;
+
+ double centerDiffX = circleX - boxCenterX;
+ double centerDiffY = circleY - boxCenterY;
+ double centerDiffZ = circleZ - boxCenterZ;
+
+ centerDiffX = circleX - (clamp(centerDiffX, -boxWidthX, boxWidthX) + boxCenterX);
+ centerDiffY = circleY - (clamp(centerDiffY, -boxWidthY, boxWidthY) + boxCenterY);
+ centerDiffZ = circleZ - (clamp(centerDiffZ, -boxWidthZ, boxWidthZ) + boxCenterZ);
+
+ return (centerDiffX * centerDiffX) + (centerDiffY * centerDiffY) + (centerDiffZ * centerDiffZ);
+ }
+
+
+ // key is:
+ // upper 32 bits:
+ // upper 16 bits: max y section
+ // lower 16 bits: min y section
+ // lower 32 bits:
+ // upper 16 bits: section
+ // lower 16 bits: radius
+ protected static long getKey(final int minSection, final int maxSection, final int section, final int radius) {
+ return (
+ (maxSection & 0xFFFFL) << (64 - 16)
+ | (minSection & 0xFFFFL) << (64 - 32)
+ | (section & 0xFFFFL) << (64 - 48)
+ | (radius & 0xFFFFL) << (64 - 64)
+ );
+ }
+
+ // only includes x/z axis
+ // finds the closest poi data by distance.
+ public static BlockPos findClosestPoiDataPosition(final PoiManager poiStorage,
+ final Predicate<PoiType> villagePlaceType,
+ // position predicate must not modify chunk POI
+ final Predicate<BlockPos> positionPredicate,
+ final BlockPos sourcePosition,
+ final int range, // distance on x y z axis
+ final double maxDistance,
+ final PoiManager.Occupancy occupancy,
+ final boolean load) {
+ final PoiRecord ret = findClosestPoiDataRecord(
+ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistance, occupancy, load
+ );
+
+ return ret == null ? null : ret.getPos();
+ }
+
+ // only includes x/z axis
+ // finds the closest poi data by distance. if multiple match the same distance, then they all are returned.
+ public static void findClosestPoiDataPositions(final PoiManager poiStorage,
+ final Predicate<PoiType> villagePlaceType,
+ // position predicate must not modify chunk POI
+ final Predicate<BlockPos> positionPredicate,
+ final BlockPos sourcePosition,
+ final int range, // distance on x y z axis
+ final double maxDistance,
+ final PoiManager.Occupancy occupancy,
+ final boolean load,
+ final Set<BlockPos> ret) {
+ final Set<BlockPos> positions = new HashSet<>();
+ // pos predicate is last thing that runs before adding to ret.
+ final Predicate<BlockPos> newPredicate = (final BlockPos pos) -> {
+ if (positionPredicate != null && !positionPredicate.test(pos)) {
+ return false;
+ }
+ return positions.add(pos.immutable());
+ };
+
+ final List<PoiRecord> toConvert = new ArrayList<>();
+ findClosestPoiDataRecords(
+ poiStorage, villagePlaceType, newPredicate, sourcePosition, range, maxDistance, occupancy, load, toConvert
+ );
+
+ for (final PoiRecord record : toConvert) {
+ ret.add(record.getPos());
+ }
+ }
+
+ // only includes x/z axis
+ // finds the closest poi data by distance.
+ public static PoiRecord findClosestPoiDataRecord(final PoiManager poiStorage,
+ final Predicate<PoiType> villagePlaceType,
+ // position predicate must not modify chunk POI
+ final Predicate<BlockPos> positionPredicate,
+ final BlockPos sourcePosition,
+ final int range, // distance on x y z axis
+ final double maxDistance,
+ final PoiManager.Occupancy occupancy,
+ final boolean load) {
+ final List<PoiRecord> ret = new ArrayList<>();
+ findClosestPoiDataRecords(
+ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistance, occupancy, load, ret
+ );
+ return ret.isEmpty() ? null : ret.get(0);
+ }
+
+ // only includes x/z axis
+ // finds the closest poi data by distance. if multiple match the same distance, then they all are returned.
+ public static void findClosestPoiDataRecords(final PoiManager poiStorage,
+ final Predicate<PoiType> villagePlaceType,
+ // position predicate must not modify chunk POI
+ final Predicate<BlockPos> positionPredicate,
+ final BlockPos sourcePosition,
+ final int range, // distance on x y z axis
+ final double maxDistance,
+ final PoiManager.Occupancy occupancy,
+ final boolean load,
+ final List<PoiRecord> ret) {
+ final Predicate<? super PoiRecord> occupancyFilter = occupancy.getTest();
+
+ final List<PoiRecord> closestRecords = new ArrayList<>();
+ double closestDistanceSquared = maxDistance * maxDistance;
+
+ final int lowerX = Mth.floor(sourcePosition.getX() - range) >> 4;
+ final int lowerY = WorldUtil.getMinSection(poiStorage.world);
+ final int lowerZ = Mth.floor(sourcePosition.getZ() - range) >> 4;
+ final int upperX = Mth.floor(sourcePosition.getX() + range) >> 4;
+ final int upperY = WorldUtil.getMaxSection(poiStorage.world);
+ final int upperZ = Mth.floor(sourcePosition.getZ() + range) >> 4;
+
+ final int centerX = sourcePosition.getX() >> 4;
+ final int centerY = Mth.clamp(sourcePosition.getY() >> 4, lowerY, upperY);
+ final int centerZ = sourcePosition.getZ() >> 4;
+
+ final LongArrayFIFOQueue queue = new LongArrayFIFOQueue();
+ queue.enqueue(CoordinateUtils.getChunkSectionKey(centerX, centerY, centerZ));
+ final LongOpenHashSet seen = new LongOpenHashSet();
+
+ while (!queue.isEmpty()) {
+ final long key = queue.dequeueLong();
+ final int sectionX = CoordinateUtils.getChunkSectionX(key);
+ final int sectionY = CoordinateUtils.getChunkSectionY(key);
+ final int sectionZ = CoordinateUtils.getChunkSectionZ(key);
+
+ if (sectionX < lowerX || sectionX > upperX || sectionY < lowerY || sectionY > upperY || sectionZ < lowerZ || sectionZ > upperZ) {
+ // out of bound chunk
+ continue;
+ }
+
+ final double sectionDistanceSquared = getSmallestDistanceSquared(
+ (sectionX << 4) + 0.5,
+ (sectionY << 4) + 0.5,
+ (sectionZ << 4) + 0.5,
+ (sectionX << 4) + 15.5,
+ (sectionY << 4) + 15.5,
+ (sectionZ << 4) + 15.5,
+ (double)sourcePosition.getX(), (double)sourcePosition.getY(), (double)sourcePosition.getZ()
+ );
+ if (sectionDistanceSquared > closestDistanceSquared) {
+ continue;
+ }
+
+ // queue all neighbours
+ for (int dz = -1; dz <= 1; ++dz) {
+ for (int dx = -1; dx <= 1; ++dx) {
+ for (int dy = -1; dy <= 1; ++dy) {
+ // -1 and 1 have the 1st bit set. so just add up the first bits, and it will tell us how many
+ // values are set. we only care about cardinal neighbours, so, we only care if one value is set
+ if ((dx & 1) + (dy & 1) + (dz & 1) != 1) {
+ continue;
+ }
+
+ final int neighbourX = sectionX + dx;
+ final int neighbourY = sectionY + dy;
+ final int neighbourZ = sectionZ + dz;
+
+ final long neighbourKey = CoordinateUtils.getChunkSectionKey(neighbourX, neighbourY, neighbourZ);
+ if (seen.add(neighbourKey)) {
+ queue.enqueue(neighbourKey);
+ }
+ }
+ }
+ }
+
+ final Optional<PoiSection> poiSectionOptional = load ? poiStorage.getOrLoad(key) : poiStorage.get(key);
+
+ if (poiSectionOptional == null || !poiSectionOptional.isPresent()) {
+ continue;
+ }
+
+ final PoiSection poiSection = poiSectionOptional.orElse(null);
+
+ final Map<PoiType, Set<PoiRecord>> sectionData = poiSection.getData();
+ if (sectionData.isEmpty()) {
+ continue;
+ }
+
+ // now we search the section data
+ for (final Map.Entry<PoiType, Set<PoiRecord>> entry : sectionData.entrySet()) {
+ if (!villagePlaceType.test(entry.getKey())) {
+ // filter out by poi type
+ continue;
+ }
+
+ // now we can look at the poi data
+ for (final PoiRecord poiData : entry.getValue()) {
+ if (!occupancyFilter.test(poiData)) {
+ // filter by occupancy
+ continue;
+ }
+
+ final BlockPos poiPosition = poiData.getPos();
+
+ if (Math.abs(poiPosition.getX() - sourcePosition.getX()) > range
+ || Math.abs(poiPosition.getZ() - sourcePosition.getZ()) > range) {
+ // out of range for square radius
+ continue;
+ }
+
+ // it's important that it's poiPosition.distSqr(source) : the value actually is different IF the values are swapped!
+ final double dataRange = poiPosition.distSqr(sourcePosition);
+
+ if (dataRange > closestDistanceSquared) {
+ // out of range for distance check
+ continue;
+ }
+
+ if (positionPredicate != null && !positionPredicate.test(poiPosition)) {
+ // filter by position
+ continue;
+ }
+
+ if (dataRange < closestDistanceSquared) {
+ closestRecords.clear();
+ closestDistanceSquared = dataRange;
+ }
+ closestRecords.add(poiData);
+ }
+ }
+ }
+
+ // uh oh! we might have multiple records that match the distance sorting!
+ // we need to re-order our results by the way vanilla would have iterated over them.
+ closestRecords.sort((record1, record2) -> {
+ // vanilla iterates the same way we do for data inside sections, so we know the ordering inside a section
+ // is fine and should be preserved (this sort is stable so we're good there)
+ // but they iterate sections by x then by z (like the following)
+ // for (int x = -dx; x <= dx; ++x)
+ // for (int z = -dz; z <= dz; ++z)
+ // ....
+ // so we need to reorder such that records with lower chunk z, then lower chunk x come first
+ final BlockPos pos1 = record1.getPos();
+ final BlockPos pos2 = record2.getPos();
+
+ final int cx1 = pos1.getX() >> 4;
+ final int cz1 = pos1.getZ() >> 4;
+
+ final int cx2 = pos2.getX() >> 4;
+ final int cz2 = pos2.getZ() >> 4;
+
+ if (cz2 != cz1) {
+ // want smaller z
+ return Integer.compare(cz1, cz2);
+ }
+
+ if (cx2 != cx1) {
+ // want smaller x
+ return Integer.compare(cx1, cx2);
+ }
+
+ // same chunk
+ // once vanilla has the chunk, it will iterate from all of the chunk sections starting from smaller y
+ // so now we just compare section y, wanting smaller y
+
+ return Integer.compare(pos1.getY() >> 4, pos2.getY() >> 4);
+ });
+
+ // now we match perfectly what vanilla would have outputted, without having to search the whole radius (hopefully).
+ ret.addAll(closestRecords);
+ }
+
+ // finds the closest poi entry pos.
+ public static BlockPos findNearestPoiPosition(final PoiManager poiStorage,
+ final Predicate<PoiType> villagePlaceType,
+ // position predicate must not modify chunk POI
+ final Predicate<BlockPos> positionPredicate,
+ final BlockPos sourcePosition,
+ final int range, // distance on x y z axis
+ final double maxDistance,
+ final PoiManager.Occupancy occupancy,
+ final boolean load) {
+ final PoiRecord ret = findNearestPoiRecord(
+ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistance, occupancy, load
+ );
+ return ret == null ? null : ret.getPos();
+ }
+
+ // finds the closest `max` poi entry positions.
+ public static void findNearestPoiPositions(final PoiManager poiStorage,
+ final Predicate<PoiType> villagePlaceType,
+ // position predicate must not modify chunk POI
+ final Predicate<BlockPos> positionPredicate,
+ final BlockPos sourcePosition,
+ final int range, // distance on x y z axis
+ final double maxDistance,
+ final PoiManager.Occupancy occupancy,
+ final boolean load,
+ final int max,
+ final List<BlockPos> ret) {
+ final Set<BlockPos> positions = new HashSet<>();
+ // pos predicate is last thing that runs before adding to ret.
+ final Predicate<BlockPos> newPredicate = (final BlockPos pos) -> {
+ if (positionPredicate != null && !positionPredicate.test(pos)) {
+ return false;
+ }
+ return positions.add(pos.immutable());
+ };
+
+ final List<PoiRecord> toConvert = new ArrayList<>();
+ findNearestPoiRecords(
+ poiStorage, villagePlaceType, newPredicate, sourcePosition, range, maxDistance, occupancy, load, max, toConvert
+ );
+
+ for (final PoiRecord record : toConvert) {
+ ret.add(record.getPos());
+ }
+ }
+
+ // finds the closest poi entry.
+ public static PoiRecord findNearestPoiRecord(final PoiManager poiStorage,
+ final Predicate<PoiType> villagePlaceType,
+ // position predicate must not modify chunk POI
+ final Predicate<BlockPos> positionPredicate,
+ final BlockPos sourcePosition,
+ final int range, // distance on x y z axis
+ final double maxDistance,
+ final PoiManager.Occupancy occupancy,
+ final boolean load) {
+ final List<PoiRecord> ret = new ArrayList<>();
+ findNearestPoiRecords(
+ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistance, occupancy, load,
+ 1, ret
+ );
+ return ret.isEmpty() ? null : ret.get(0);
+ }
+
+ // finds the closest `max` poi entries.
+ public static void findNearestPoiRecords(final PoiManager poiStorage,
+ final Predicate<PoiType> villagePlaceType,
+ // position predicate must not modify chunk POI
+ final Predicate<BlockPos> positionPredicate,
+ final BlockPos sourcePosition,
+ final int range, // distance on x y z axis
+ final double maxDistance,
+ final PoiManager.Occupancy occupancy,
+ final boolean load,
+ final int max,
+ final List<PoiRecord> ret) {
+ final Predicate<? super PoiRecord> occupancyFilter = occupancy.getTest();
+
+ final double maxDistanceSquared = maxDistance * maxDistance;
+ final Double2ObjectRBTreeMap<List<PoiRecord>> closestRecords = new Double2ObjectRBTreeMap<>();
+ int totalRecords = 0;
+ double furthestDistanceSquared = maxDistanceSquared;
+
+ final int lowerX = Mth.floor(sourcePosition.getX() - range) >> 4;
+ final int lowerY = WorldUtil.getMinSection(poiStorage.world);
+ final int lowerZ = Mth.floor(sourcePosition.getZ() - range) >> 4;
+ final int upperX = Mth.floor(sourcePosition.getX() + range) >> 4;
+ final int upperY = WorldUtil.getMaxSection(poiStorage.world);
+ final int upperZ = Mth.floor(sourcePosition.getZ() + range) >> 4;
+
+ final int centerX = sourcePosition.getX() >> 4;
+ final int centerY = Mth.clamp(sourcePosition.getY() >> 4, lowerY, upperY);
+ final int centerZ = sourcePosition.getZ() >> 4;
+
+ final LongArrayFIFOQueue queue = new LongArrayFIFOQueue();
+ queue.enqueue(CoordinateUtils.getChunkSectionKey(centerX, centerY, centerZ));
+ final LongOpenHashSet seen = new LongOpenHashSet();
+
+ while (!queue.isEmpty()) {
+ final long key = queue.dequeueLong();
+ final int sectionX = CoordinateUtils.getChunkSectionX(key);
+ final int sectionY = CoordinateUtils.getChunkSectionY(key);
+ final int sectionZ = CoordinateUtils.getChunkSectionZ(key);
+
+ if (sectionX < lowerX || sectionX > upperX || sectionY < lowerY || sectionY > upperY || sectionZ < lowerZ || sectionZ > upperZ) {
+ // out of bound chunk
+ continue;
+ }
+
+ final double sectionDistanceSquared = getSmallestDistanceSquared(
+ (sectionX << 4) + 0.5,
+ (sectionY << 4) + 0.5,
+ (sectionZ << 4) + 0.5,
+ (sectionX << 4) + 15.5,
+ (sectionY << 4) + 15.5,
+ (sectionZ << 4) + 15.5,
+ (double) sourcePosition.getX(), (double) sourcePosition.getY(), (double) sourcePosition.getZ()
+ );
+
+ if (sectionDistanceSquared > (totalRecords >= max ? furthestDistanceSquared : maxDistanceSquared)) {
+ continue;
+ }
+
+ // queue all neighbours
+ for (int dz = -1; dz <= 1; ++dz) {
+ for (int dx = -1; dx <= 1; ++dx) {
+ for (int dy = -1; dy <= 1; ++dy) {
+ // -1 and 1 have the 1st bit set. so just add up the first bits, and it will tell us how many
+ // values are set. we only care about cardinal neighbours, so, we only care if one value is set
+ if ((dx & 1) + (dy & 1) + (dz & 1) != 1) {
+ continue;
+ }
+
+ final int neighbourX = sectionX + dx;
+ final int neighbourY = sectionY + dy;
+ final int neighbourZ = sectionZ + dz;
+
+ final long neighbourKey = CoordinateUtils.getChunkSectionKey(neighbourX, neighbourY, neighbourZ);
+ if (seen.add(neighbourKey)) {
+ queue.enqueue(neighbourKey);
+ }
+ }
+ }
+ }
+
+ final Optional<PoiSection> poiSectionOptional = load ? poiStorage.getOrLoad(key) : poiStorage.get(key);
+
+ if (poiSectionOptional == null || !poiSectionOptional.isPresent()) {
+ continue;
+ }
+
+ final PoiSection poiSection = poiSectionOptional.orElse(null);
+
+ final Map<PoiType, Set<PoiRecord>> sectionData = poiSection.getData();
+ if (sectionData.isEmpty()) {
+ continue;
+ }
+
+ // now we search the section data
+ for (final Map.Entry<PoiType, Set<PoiRecord>> entry : sectionData.entrySet()) {
+ if (!villagePlaceType.test(entry.getKey())) {
+ // filter out by poi type
+ continue;
+ }
+
+ // now we can look at the poi data
+ for (final PoiRecord poiData : entry.getValue()) {
+ if (!occupancyFilter.test(poiData)) {
+ // filter by occupancy
+ continue;
+ }
+
+ final BlockPos poiPosition = poiData.getPos();
+
+ if (Math.abs(poiPosition.getX() - sourcePosition.getX()) > range
+ || Math.abs(poiPosition.getZ() - sourcePosition.getZ()) > range) {
+ // out of range for square radius
+ continue;
+ }
+
+ // it's important that it's poiPosition.distSqr(source) : the value actually is different IF the values are swapped!
+ final double dataRange = poiPosition.distSqr(sourcePosition);
+
+ if (dataRange > maxDistanceSquared) {
+ // out of range for distance check
+ continue;
+ }
+
+ if (dataRange > furthestDistanceSquared && totalRecords >= max) {
+ // out of range for distance check
+ continue;
+ }
+
+ if (positionPredicate != null && !positionPredicate.test(poiPosition)) {
+ // filter by position
+ continue;
+ }
+
+ if (dataRange > furthestDistanceSquared) {
+ // we know totalRecords < max, so this entry is now our furthest
+ furthestDistanceSquared = dataRange;
+ }
+
+ closestRecords.computeIfAbsent(dataRange, (final double unused) -> {
+ return new ArrayList<>();
+ }).add(poiData);
+
+ if (++totalRecords >= max) {
+ if (closestRecords.size() >= 2) {
+ int entriesInClosest = 0;
+ final Iterator<Double2ObjectMap.Entry<List<PoiRecord>>> iterator = closestRecords.double2ObjectEntrySet().iterator();
+ double nextFurthestDistanceSquared = 0.0;
+
+ for (int i = 0, len = closestRecords.size() - 1; i < len; ++i) {
+ final Double2ObjectMap.Entry<List<PoiRecord>> recordEntry = iterator.next();
+ entriesInClosest += recordEntry.getValue().size();
+ nextFurthestDistanceSquared = recordEntry.getDoubleKey();
+ }
+
+ if (entriesInClosest >= max) {
+ // the last set of entries at range wont even be considered for sure... nuke em
+ final Double2ObjectMap.Entry<List<PoiRecord>> recordEntry = iterator.next();
+ totalRecords -= recordEntry.getValue().size();
+ iterator.remove();
+
+ furthestDistanceSquared = nextFurthestDistanceSquared;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ final List<PoiRecord> closestRecordsUnsorted = new ArrayList<>();
+
+ // we're done here, so now just flatten the map and sort it.
+
+ for (final List<PoiRecord> records : closestRecords.values()) {
+ closestRecordsUnsorted.addAll(records);
+ }
+
+ // uh oh! we might have multiple records that match the distance sorting!
+ // we need to re-order our results by the way vanilla would have iterated over them.
+ closestRecordsUnsorted.sort((record1, record2) -> {
+ // vanilla iterates the same way we do for data inside sections, so we know the ordering inside a section
+ // is fine and should be preserved (this sort is stable so we're good there)
+ // but they iterate sections by x then by z (like the following)
+ // for (int x = -dx; x <= dx; ++x)
+ // for (int z = -dz; z <= dz; ++z)
+ // ....
+ // so we need to reorder such that records with lower chunk z, then lower chunk x come first
+ final BlockPos pos1 = record1.getPos();
+ final BlockPos pos2 = record2.getPos();
+
+ final int cx1 = pos1.getX() >> 4;
+ final int cz1 = pos1.getZ() >> 4;
+
+ final int cx2 = pos2.getX() >> 4;
+ final int cz2 = pos2.getZ() >> 4;
+
+ if (cz2 != cz1) {
+ // want smaller z
+ return Integer.compare(cz1, cz2);
+ }
+
+ if (cx2 != cx1) {
+ // want smaller x
+ return Integer.compare(cx1, cx2);
+ }
+
+ // same chunk
+ // once vanilla has the chunk, it will iterate from all of the chunk sections starting from smaller y
+ // so now we just compare section y, wanting smaller section y
+
+ return Integer.compare(pos1.getY() >> 4, pos2.getY() >> 4);
+ });
+
+ // trim out any entries exceeding our maximum
+ for (int i = closestRecordsUnsorted.size() - 1; i >= max; --i) {
+ closestRecordsUnsorted.remove(i);
+ }
+
+ // now we match perfectly what vanilla would have outputted, without having to search the whole radius (hopefully).
+ ret.addAll(closestRecordsUnsorted);
+ }
+
+ public static BlockPos findAnyPoiPosition(final PoiManager poiStorage,
+ final Predicate<PoiType> villagePlaceType,
+ final Predicate<BlockPos> positionPredicate,
+ final BlockPos sourcePosition,
+ final int range, // distance on x y z axis
+ final PoiManager.Occupancy occupancy,
+ final boolean load) {
+ final PoiRecord ret = findAnyPoiRecord(
+ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, occupancy, load
+ );
+
+ return ret == null ? null : ret.getPos();
+ }
+
+ public static void findAnyPoiPositions(final PoiManager poiStorage,
+ final Predicate<PoiType> villagePlaceType,
+ final Predicate<BlockPos> positionPredicate,
+ final BlockPos sourcePosition,
+ final int range, // distance on x y z axis
+ final PoiManager.Occupancy occupancy,
+ final boolean load,
+ final int max,
+ final List<BlockPos> ret) {
+ final Set<BlockPos> positions = new HashSet<>();
+ // pos predicate is last thing that runs before adding to ret.
+ final Predicate<BlockPos> newPredicate = (final BlockPos pos) -> {
+ if (positionPredicate != null && !positionPredicate.test(pos)) {
+ return false;
+ }
+ return positions.add(pos.immutable());
+ };
+
+ final List<PoiRecord> toConvert = new ArrayList<>();
+ findAnyPoiRecords(
+ poiStorage, villagePlaceType, newPredicate, sourcePosition, range, occupancy, load, max, toConvert
+ );
+
+ for (final PoiRecord record : toConvert) {
+ ret.add(record.getPos());
+ }
+ }
+
+ public static PoiRecord findAnyPoiRecord(final PoiManager poiStorage,
+ final Predicate<PoiType> villagePlaceType,
+ final Predicate<BlockPos> positionPredicate,
+ final BlockPos sourcePosition,
+ final int range, // distance on x y z axis
+ final PoiManager.Occupancy occupancy,
+ final boolean load) {
+ final List<PoiRecord> ret = new ArrayList<>();
+ findAnyPoiRecords(poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, occupancy, load, 1, ret);
+ return ret.isEmpty() ? null : ret.get(0);
+ }
+
+ public static void findAnyPoiRecords(final PoiManager poiStorage,
+ final Predicate<PoiType> villagePlaceType,
+ final Predicate<BlockPos> positionPredicate,
+ final BlockPos sourcePosition,
+ final int range, // distance on x y z axis
+ final PoiManager.Occupancy occupancy,
+ final boolean load,
+ final int max,
+ final List<PoiRecord> ret) {
+ // the biggest issue with the original mojang implementation is that they chain so many streams together
+ // the amount of streams chained just rolls performance, even if nothing is iterated over
+ final Predicate<? super PoiRecord> occupancyFilter = occupancy.getTest();
+ final double rangeSquared = range * range;
+
+ int added = 0;
+
+ // First up, we need to iterate the chunks
+ // all the values here are in chunk sections
+ final int lowerX = Mth.floor(sourcePosition.getX() - range) >> 4;
+ final int lowerY = Math.max(WorldUtil.getMinSection(poiStorage.world), Mth.floor(sourcePosition.getY() - range) >> 4);
+ final int lowerZ = Mth.floor(sourcePosition.getZ() - range) >> 4;
+ final int upperX = Mth.floor(sourcePosition.getX() + range) >> 4;
+ final int upperY = Math.min(WorldUtil.getMaxSection(poiStorage.world), Mth.floor(sourcePosition.getY() + range) >> 4);
+ final int upperZ = Mth.floor(sourcePosition.getZ() + range) >> 4;
+
+ // Vanilla iterates by x until max is reached then increases z
+ // vanilla also searches by increasing Y section value
+ for (int currZ = lowerZ; currZ <= upperZ; ++currZ) {
+ for (int currX = lowerX; currX <= upperX; ++currX) {
+ for (int currY = lowerY; currY <= upperY; ++currY) { // vanilla searches the entire chunk because they're actually stupid. just search the sections we need
+ final Optional<PoiSection> poiSectionOptional = load ? poiStorage.getOrLoad(CoordinateUtils.getChunkSectionKey(currX, currY, currZ)) :
+ poiStorage.get(CoordinateUtils.getChunkSectionKey(currX, currY, currZ));
+ final PoiSection poiSection = poiSectionOptional == null ? null : poiSectionOptional.orElse(null);
+ if (poiSection == null) {
+ continue;
+ }
+
+ final Map<PoiType, Set<PoiRecord>> sectionData = poiSection.getData();
+ if (sectionData.isEmpty()) {
+ continue;
+ }
+
+ // now we search the section data
+ for (final Map.Entry<PoiType, Set<PoiRecord>> entry : sectionData.entrySet()) {
+ if (!villagePlaceType.test(entry.getKey())) {
+ // filter out by poi type
+ continue;
+ }
+
+ // now we can look at the poi data
+ for (final PoiRecord poiData : entry.getValue()) {
+ if (!occupancyFilter.test(poiData)) {
+ // filter by occupancy
+ continue;
+ }
+
+ final BlockPos poiPosition = poiData.getPos();
+
+ if (Math.abs(poiPosition.getX() - sourcePosition.getX()) > range
+ || Math.abs(poiPosition.getZ() - sourcePosition.getZ()) > range) {
+ // out of range for square radius
+ continue;
+ }
+
+ if (poiPosition.distSqr(sourcePosition) > rangeSquared) {
+ // out of range for distance check
+ continue;
+ }
+
+ if (positionPredicate != null && !positionPredicate.test(poiPosition)) {
+ // filter by position
+ continue;
+ }
+
+ // found one!
+ ret.add(poiData);
+ if (++added >= max) {
+ return;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private PoiAccess() {
+ throw new RuntimeException();
+ }
+}
diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java b/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java
index 84a0ee595bebcc1947c602c4c06e7437706ce37c..afbb2acd27416c801af3d718850b82a170734cd3 100644
--- a/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java
+++ b/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java
@@ -83,7 +83,11 @@ public class AcquirePoi extends Behavior<PathfinderMob> {
return true;
}
};
- Set<BlockPos> set = poiManager.findAllClosestFirst(this.poiType.getPredicate(), predicate, entity.blockPosition(), 48, PoiManager.Occupancy.HAS_SPACE).limit(5L).collect(Collectors.toSet());
+ // Paper start - optimise POI access
+ java.util.List<BlockPos> poiposes = new java.util.ArrayList<>();
+ io.papermc.paper.util.PoiAccess.findNearestPoiPositions(poiManager, this.poiType.getPredicate(), predicate, entity.blockPosition(), 48, 48*48, PoiManager.Occupancy.HAS_SPACE, false, 5, poiposes);
+ Set<BlockPos> set = new java.util.HashSet<>(poiposes);
+ // Paper end - optimise POI access
Path path = entity.getNavigation().createPath(set, this.poiType.getValidRange());
if (path != null && path.canReach()) {
BlockPos blockPos = path.getTarget();
diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java b/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java
index 0eea3e39616e40e15d1662b973c097cda3b2cee7..3ccc1421f4a5a08dadb9fe3c9fa3ac3131e6ba1e 100644
--- a/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java
+++ b/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java
@@ -49,8 +49,12 @@ public class NearestBedSensor extends Sensor<Mob> {
return true;
}
};
- Stream<BlockPos> stream = poiManager.findAll(PoiType.HOME.getPredicate(), predicate, entity.blockPosition(), 48, PoiManager.Occupancy.ANY);
- Path path = entity.getNavigation().createPath(stream, PoiType.HOME.getValidRange());
+ // Paper start - optimise POI access
+ java.util.List<BlockPos> poiposes = new java.util.ArrayList<>();
+ // don't ask me why it's unbounded. ask mojang.
+ io.papermc.paper.util.PoiAccess.findAnyPoiPositions(poiManager, PoiType.HOME.getPredicate(), predicate, entity.blockPosition(), 48, PoiManager.Occupancy.ANY, false, Integer.MAX_VALUE, poiposes);
+ Path path = entity.getNavigation().createPath(new java.util.HashSet<>(poiposes), PoiType.HOME.getValidRange());
+ // Paper end - optimise POI access
if (path != null && path.canReach()) {
BlockPos blockPos = path.getTarget();
Optional<PoiType> optional = poiManager.getType(blockPos);
diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java
index 4a972b26242cf4c9d7e8f655cb1264cddad5f143..8a569e3300543cb171c3befae59969628adc424c 100644
--- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java
+++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java
@@ -37,7 +37,7 @@ public class PoiManager extends SectionStorage<PoiSection> {
public static final int VILLAGE_SECTION_SIZE = 1;
private final PoiManager.DistanceTracker distanceTracker;
private final LongSet loadedChunks = new LongOpenHashSet();
- private final net.minecraft.server.level.ServerLevel world; // Paper
+ public final net.minecraft.server.level.ServerLevel world; // Paper // Paper public
public PoiManager(Path path, DataFixer dataFixer, boolean dsync, LevelHeightAccessor world) {
super(path, PoiSection::codec, PoiSection::new, dataFixer, DataFixTypes.POI_CHUNK, dsync, world);
@@ -100,36 +100,55 @@ public class PoiManager extends SectionStorage<PoiSection> {
}
public Optional<BlockPos> find(Predicate<PoiType> typePredicate, Predicate<BlockPos> posPredicate, BlockPos pos, int radius, PoiManager.Occupancy occupationStatus) {
- return this.findAll(typePredicate, posPredicate, pos, radius, occupationStatus).findFirst();
+ // Paper start - re-route to faster logic
+ BlockPos ret = io.papermc.paper.util.PoiAccess.findAnyPoiPosition(this, typePredicate, posPredicate, pos, radius, occupationStatus, false);
+ return Optional.ofNullable(ret);
+ // Paper end - re-route to faster logic
}
public Optional<BlockPos> findClosest(Predicate<PoiType> typePredicate, BlockPos pos, int radius, PoiManager.Occupancy occupationStatus) {
- return this.getInRange(typePredicate, pos, radius, occupationStatus).map(PoiRecord::getPos).min(Comparator.comparingDouble((blockPos2) -> {
- return blockPos2.distSqr(pos);
- }));
+ // Paper start - re-route to faster logic
+ BlockPos ret = io.papermc.paper.util.PoiAccess.findClosestPoiDataPosition(this, typePredicate, null, pos, radius, radius*radius, occupationStatus, false);
+ return Optional.ofNullable(ret);
+ // Paper end - re-route to faster logic
}
public Optional<BlockPos> findClosest(Predicate<PoiType> typePredicate, Predicate<BlockPos> posPredicate, BlockPos pos, int radius, PoiManager.Occupancy occupationStatus) {
- return this.getInRange(typePredicate, pos, radius, occupationStatus).map(PoiRecord::getPos).filter(posPredicate).min(Comparator.comparingDouble((blockPos2) -> {
- return blockPos2.distSqr(pos);
- }));
+ // Paper start - re-route to faster logic
+ BlockPos ret = io.papermc.paper.util.PoiAccess.findClosestPoiDataPosition(this, typePredicate, posPredicate, pos, radius, radius * radius, occupationStatus, false);
+ return Optional.ofNullable(ret);
+ // Paper end - re-route to faster logic
}
public Optional<BlockPos> take(Predicate<PoiType> typePredicate, Predicate<BlockPos> positionPredicate, BlockPos pos, int radius) {
- return this.getInRange(typePredicate, pos, radius, PoiManager.Occupancy.HAS_SPACE).filter((poi) -> {
- return positionPredicate.test(poi.getPos());
- }).findFirst().map((poi) -> {
- poi.acquireTicket();
- return poi.getPos();
- });
+ // Paper start - re-route to faster logic
+ PoiRecord ret = io.papermc.paper.util.PoiAccess.findAnyPoiRecord(
+ this, typePredicate, positionPredicate, pos, radius, PoiManager.Occupancy.HAS_SPACE, false
+ );
+ if (ret == null) {
+ return Optional.empty();
+ }
+ ret.acquireTicket();
+ return Optional.of(ret.getPos());
+ // Paper end - re-route to faster logic
}
public Optional<BlockPos> getRandom(Predicate<PoiType> typePredicate, Predicate<BlockPos> positionPredicate, PoiManager.Occupancy occupationStatus, BlockPos pos, int radius, Random random) {
- List<PoiRecord> list = this.getInRange(typePredicate, pos, radius, occupationStatus).collect(Collectors.toList());
- Collections.shuffle(list, random);
- return list.stream().filter((poi) -> {
- return positionPredicate.test(poi.getPos());
- }).findFirst().map(PoiRecord::getPos);
+ // Paper start - re-route to faster logic
+ List<PoiRecord> list = new java.util.ArrayList<>();
+ io.papermc.paper.util.PoiAccess.findAnyPoiRecords(
+ this, typePredicate, positionPredicate, pos, radius, occupationStatus, false, Integer.MAX_VALUE, list
+ );
+
+ // the old method shuffled the list and then tried to find the first element in it that
+ // matched positionPredicate, however we moved positionPredicate into the poi search. This means we can avoid a
+ // shuffle entirely, and just pick a random element from list
+ if (list.isEmpty()) {
+ return Optional.empty();
+ }
+
+ return Optional.of(list.get(random.nextInt(list.size())).getPos());
+ // Paper end - re-route to faster logic
}
public boolean release(BlockPos pos) {
diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java
index 63f283f32bdad02299d4a16c305a28c3bfbce9a8..de94f25792261c6c89986ad3dee3255c2a89357b 100644
--- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java
+++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java
@@ -25,7 +25,7 @@ import org.apache.logging.log4j.Logger;
public class PoiSection {
private static final Logger LOGGER = LogManager.getLogger();
private final Short2ObjectMap<PoiRecord> records = new Short2ObjectOpenHashMap<>();
- private final Map<PoiType, Set<PoiRecord>> byType = Maps.newHashMap();
+ private final Map<PoiType, Set<PoiRecord>> byType = Maps.newHashMap(); public final Map<PoiType, Set<PoiRecord>> getData() { return this.byType; } // Paper - public accessor
private final Runnable setDirty;
private boolean isValid;
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java
index ff6cadec530dedf9efc5d6226e48a096a1073ad6..d73b99d7fde724da4503b5176c3ad7b013197c6a 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java
@@ -61,11 +61,11 @@ public class SectionStorage<R> extends RegionFileStorage implements AutoCloseabl
}
@Nullable
- protected Optional<R> get(long pos) {
+ public Optional<R> get(long pos) { // Paper - public
return this.storage.get(pos);
}
- protected Optional<R> getOrLoad(long pos) {
+ public Optional<R> getOrLoad(long pos) { // Paper - public
if (this.outsideStoredRange(pos)) {
return Optional.empty();
} else {
diff --git a/src/main/java/net/minecraft/world/level/portal/PortalForcer.java b/src/main/java/net/minecraft/world/level/portal/PortalForcer.java
index ed79058696eb26a89b9d4116821840dbad9ea449..d990d1652b71205816d678618bf360a60f309ad2 100644
--- a/src/main/java/net/minecraft/world/level/portal/PortalForcer.java
+++ b/src/main/java/net/minecraft/world/level/portal/PortalForcer.java
@@ -51,18 +51,40 @@ public class PortalForcer {
// int i = flag ? 16 : 128;
// CraftBukkit end
- villageplace.ensureLoadedAndValid(this.level, blockposition, i);
- Optional<PoiRecord> optional = villageplace.getInSquare((villageplacetype) -> {
- return villageplacetype == PoiType.NETHER_PORTAL;
- }, blockposition, i, PoiManager.Occupancy.ANY).filter((villageplacerecord) -> {
- return worldborder.isWithinBounds(villageplacerecord.getPos());
- }).sorted(Comparator.comparingDouble((PoiRecord villageplacerecord) -> { // CraftBukkit - decompile error
- return villageplacerecord.getPos().distSqr(blockposition);
- }).thenComparingInt((villageplacerecord) -> {
- return villageplacerecord.getPos().getY();
- })).filter((villageplacerecord) -> {
- return this.level.getBlockState(villageplacerecord.getPos()).hasProperty(BlockStateProperties.HORIZONTAL_AXIS);
- }).findFirst();
+ // Paper start - optimise portals
+ Optional<PoiRecord> optional;
+ java.util.List<PoiRecord> records = new java.util.ArrayList<>();
+ io.papermc.paper.util.PoiAccess.findClosestPoiDataRecords(
+ villageplace,
+ (PoiType type) -> {
+ return type == PoiType.NETHER_PORTAL;
+ },
+ (BlockPos pos) -> {
+ net.minecraft.world.level.chunk.ChunkAccess lowest = this.level.getChunk(pos.getX() >> 4, pos.getZ() >> 4, net.minecraft.world.level.chunk.ChunkStatus.EMPTY);
+ if (!lowest.getStatus().isOrAfter(net.minecraft.world.level.chunk.ChunkStatus.FULL)) {
+ // why would we generate the chunk?
+ return false;
+ }
+ if (!worldborder.isWithinBounds(pos)) {
+ return false;
+ }
+ return lowest.getBlockState(pos).hasProperty(BlockStateProperties.HORIZONTAL_AXIS);
+ },
+ blockposition, i, Double.MAX_VALUE, PoiManager.Occupancy.ANY, true, records
+ );
+
+ // this gets us most of the way there, but we bias towards lower y values.
+ PoiRecord lowestYRecord = null;
+ for (PoiRecord record : records) {
+ if (lowestYRecord == null) {
+ lowestYRecord = record;
+ } else if (lowestYRecord.getPos().getY() > record.getPos().getY()) {
+ lowestYRecord = record;
+ }
+ }
+ // now we're done
+ optional = Optional.ofNullable(lowestYRecord);
+ // Paper end - optimise portals
return optional.map((villageplacerecord) -> {
BlockPos blockposition1 = villageplacerecord.getPos();

View file

@ -0,0 +1,147 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Spottedleaf <spottedleaf@spottedleaf.dev>
Date: Sat, 4 Apr 2020 15:27:44 -0700
Subject: [PATCH] Allow controlled flushing for network manager
Only make one flush call when emptying the packet queue too
This patch will be used to optimise out flush calls in later
patches.
diff --git a/src/main/java/net/minecraft/network/Connection.java b/src/main/java/net/minecraft/network/Connection.java
index 16954170ffeeedf18d8f8079b5e75915e0c682ba..a6b438543a12f5ecf05fb631ef53b18d4d253dff 100644
--- a/src/main/java/net/minecraft/network/Connection.java
+++ b/src/main/java/net/minecraft/network/Connection.java
@@ -94,6 +94,39 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
public ConnectionProtocol protocol;
// Paper end
+ // Paper start - allow controlled flushing
+ volatile boolean canFlush = true;
+ private final java.util.concurrent.atomic.AtomicInteger packetWrites = new java.util.concurrent.atomic.AtomicInteger();
+ private int flushPacketsStart;
+ private final Object flushLock = new Object();
+
+ public void disableAutomaticFlush() {
+ synchronized (this.flushLock) {
+ this.flushPacketsStart = this.packetWrites.get(); // must be volatile and before canFlush = false
+ this.canFlush = false;
+ }
+ }
+
+ public void enableAutomaticFlush() {
+ synchronized (this.flushLock) {
+ this.canFlush = true;
+ if (this.packetWrites.get() != this.flushPacketsStart) { // must be after canFlush = true
+ this.flush(); // only make the flush call if we need to
+ }
+ }
+ }
+
+ private final void flush() {
+ if (this.channel.eventLoop().inEventLoop()) {
+ this.channel.flush();
+ } else {
+ this.channel.eventLoop().execute(() -> {
+ this.channel.flush();
+ });
+ }
+ }
+ // Paper end - allow controlled flushing
+
public Connection(PacketFlow side) {
this.receiving = side;
}
@@ -255,7 +288,7 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
net.minecraft.server.MCUtil.isMainThread() && packet.isReady() && this.queue.isEmpty() &&
(packet.getExtraPackets() == null || packet.getExtraPackets().isEmpty())
))) {
- this.sendPacket(packet, callback);
+ this.writePacket(packet, callback, null); // Paper
return;
}
// write the packets to the queue, then flush - antixray hooks there already
@@ -279,6 +312,14 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
}
private void sendPacket(Packet<?> packet, @Nullable GenericFutureListener<? extends Future<? super Void>> callback) {
+ // Paper start - add flush parameter
+ this.writePacket(packet, callback, Boolean.TRUE);
+ }
+ private void writePacket(Packet<?> packet, @Nullable GenericFutureListener<? extends Future<? super Void>> callback, Boolean flushConditional) {
+ this.packetWrites.getAndIncrement(); // must be befeore using canFlush
+ boolean effectiveFlush = flushConditional == null ? this.canFlush : flushConditional.booleanValue();
+ final boolean flush = effectiveFlush || packet instanceof net.minecraft.network.protocol.game.ClientboundKeepAlivePacket || packet instanceof ClientboundDisconnectPacket; // no delay for certain packets
+ // Paper end - add flush parameter
ConnectionProtocol enumprotocol = ConnectionProtocol.getProtocolForPacket(packet);
ConnectionProtocol enumprotocol1 = this.getCurrentProtocol();
@@ -289,16 +330,21 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
}
if (this.channel.eventLoop().inEventLoop()) {
- this.doSendPacket(packet, callback, enumprotocol, enumprotocol1);
+ this.doSendPacket(packet, callback, enumprotocol, enumprotocol1, flush); // Paper - add flush parameter
} else {
this.channel.eventLoop().execute(() -> {
- this.doSendPacket(packet, callback, enumprotocol, enumprotocol1);
+ this.doSendPacket(packet, callback, enumprotocol, enumprotocol1, flush); // Paper - add flush parameter
});
}
}
private void doSendPacket(Packet<?> packet, @Nullable GenericFutureListener<? extends Future<? super Void>> callback, ConnectionProtocol packetState, ConnectionProtocol currentState) {
+ // Paper start - add flush parameter
+ this.doSendPacket(packet, callback, packetState, currentState, true);
+ }
+ private void doSendPacket(Packet<?> packet, @Nullable GenericFutureListener<? extends Future<? super Void>> callback, ConnectionProtocol packetState, ConnectionProtocol currentState, boolean flush) {
+ // Paper end - add flush parameter
if (packetState != currentState) {
this.setProtocol(packetState);
}
@@ -312,7 +358,7 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
try {
// Paper end
- ChannelFuture channelfuture = this.channel.writeAndFlush(packet);
+ ChannelFuture channelfuture = flush ? this.channel.writeAndFlush(packet) : this.channel.write(packet); // Paper - add flush parameter
if (callback != null) {
channelfuture.addListener(callback);
@@ -354,6 +400,10 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
}
private boolean processQueue() {
if (this.queue.isEmpty()) return true;
+ // Paper start - make only one flush call per sendPacketQueue() call
+ final boolean needsFlush = this.canFlush;
+ boolean hasWrotePacket = false;
+ // Paper end - make only one flush call per sendPacketQueue() call
// If we are on main, we are safe here in that nothing else should be processing queue off main anymore
// But if we are not on main due to login/status, the parent is synchronized on packetQueue
java.util.Iterator<PacketHolder> iterator = this.queue.iterator();
@@ -361,16 +411,22 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
PacketHolder queued = iterator.next(); // poll -> peek
// Fix NPE (Spigot bug caused by handleDisconnection())
- if (queued == null) {
+ if (false && queued == null) { // Paper - diff on change, this logic is redundant: iterator guarantees ret of an element - on change, hook the flush logic here
return true;
}
Packet<?> packet = queued.packet;
if (!packet.isReady()) {
+ // Paper start - make only one flush call per sendPacketQueue() call
+ if (hasWrotePacket && (needsFlush || this.canFlush)) {
+ this.flush();
+ }
+ // Paper end - make only one flush call per sendPacketQueue() call
return false;
} else {
iterator.remove();
- this.sendPacket(packet, queued.listener);
+ this.writePacket(packet, queued.listener, (!iterator.hasNext() && (needsFlush || this.canFlush)) ? Boolean.TRUE : Boolean.FALSE); // Paper - make only one flush call per sendPacketQueue() call
+ hasWrotePacket = true; // Paper - make only one flush call per sendPacketQueue() call
}
}
return true;

View file

@ -0,0 +1,44 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Spottedleaf <Spottedleaf@users.noreply.github.com>
Date: Thu, 15 Jul 2021 01:41:53 -0700
Subject: [PATCH] Add more async catchers
diff --git a/src/main/java/net/minecraft/world/level/entity/EntityTickList.java b/src/main/java/net/minecraft/world/level/entity/EntityTickList.java
index f01182a0ac8a14bcd5b1deb778306e7bf1bf70ed..b27c8db914cca3ff0ea8a24acddb9cb9870ce21d 100644
--- a/src/main/java/net/minecraft/world/level/entity/EntityTickList.java
+++ b/src/main/java/net/minecraft/world/level/entity/EntityTickList.java
@@ -30,11 +30,13 @@ public class EntityTickList {
}
public void add(Entity entity) {
+ io.papermc.paper.util.TickThread.ensureTickThread("Asynchronous entity ticklist addition"); // Paper
this.ensureActiveIsNotIterated();
this.active.put(entity.getId(), entity);
}
public void remove(Entity entity) {
+ io.papermc.paper.util.TickThread.ensureTickThread("Asynchronous entity ticklist removal"); // Paper
this.ensureActiveIsNotIterated();
this.active.remove(entity.getId());
}
@@ -44,6 +46,7 @@ public class EntityTickList {
}
public void forEach(Consumer<Entity> action) {
+ io.papermc.paper.util.TickThread.ensureTickThread("Asynchronous entity ticklist iteration"); // Paper
if (this.iterated != null) {
throw new UnsupportedOperationException("Only one concurrent iteration supported");
} else {
diff --git a/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java b/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java
index e19f5b2c8f485d596a64d5d96e75fa1f4a8255b5..14487f7b1f684ae17fd77aa0632fc61829ee691b 100644
--- a/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java
+++ b/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java
@@ -166,6 +166,7 @@ public class PersistentEntitySectionManager<T extends EntityAccess> implements A
}
public void updateChunkStatus(ChunkPos chunkPos, ChunkHolder.FullChunkStatus levelType) {
+ io.papermc.paper.util.TickThread.ensureTickThread("Asynchronous chunk ticking status update"); // Paper
Visibility visibility = Visibility.fromFullChunkStatus(levelType);
this.updateChunkStatus(chunkPos, visibility);

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,136 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Spottedleaf <spottedleaf@spottedleaf.dev>
Date: Mon, 6 Apr 2020 04:20:44 -0700
Subject: [PATCH] Execute chunk tasks mid-tick
This will help the server load chunks if tick times are high.
diff --git a/src/main/java/co/aikar/timings/MinecraftTimings.java b/src/main/java/co/aikar/timings/MinecraftTimings.java
index b27021a42cbed3f0648a8d0903d00d03922ae221..eada966d7f108a6081be7a848f5c1dfcb1eed676 100644
--- a/src/main/java/co/aikar/timings/MinecraftTimings.java
+++ b/src/main/java/co/aikar/timings/MinecraftTimings.java
@@ -45,6 +45,8 @@ public final class MinecraftTimings {
public static final Timing antiXrayUpdateTimer = Timings.ofSafe("anti-xray - update");
public static final Timing antiXrayObfuscateTimer = Timings.ofSafe("anti-xray - obfuscate");
+ public static final Timing midTickChunkTasks = Timings.ofSafe("Mid Tick Chunk Tasks");
+
private static final Map<Class<?>, String> taskNameCache = new MapMaker().weakKeys().makeMap();
private MinecraftTimings() {}
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
index 983bc6b8600489696899b5aaa09e7f7b674d2e42..86b57776a42261053237d62f3b666793457c5e2f 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
@@ -331,6 +331,76 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
return s0;
}
+ // Paper start - execute chunk tasks mid tick
+ static final long CHUNK_TASK_QUEUE_BACKOFF_MIN_TIME = 25L * 1000L; // 25us
+ static final long MAX_CHUNK_EXEC_TIME = 1000L; // 1us
+
+ static final long TASK_EXECUTION_FAILURE_BACKOFF = 5L * 1000L; // 5us
+
+ private static long lastMidTickExecute;
+ private static long lastMidTickExecuteFailure;
+
+ private boolean tickMidTickTasks() {
+ // give all worlds a fair chance at by targetting them all.
+ // if we execute too many tasks, that's fine - we have logic to correctly handle overuse of allocated time.
+ boolean executed = false;
+ for (ServerLevel world : this.getAllLevels()) {
+ long currTime = System.nanoTime();
+ if (currTime - world.lastMidTickExecuteFailure <= TASK_EXECUTION_FAILURE_BACKOFF) {
+ continue;
+ }
+ if (!world.getChunkSource().pollTask()) {
+ // we need to back off if this fails
+ world.lastMidTickExecuteFailure = currTime;
+ } else {
+ executed = true;
+ }
+ }
+
+ return executed;
+ }
+
+ public final void executeMidTickTasks() {
+ org.spigotmc.AsyncCatcher.catchOp("mid tick chunk task execution");
+ long startTime = System.nanoTime();
+ if ((startTime - lastMidTickExecute) <= CHUNK_TASK_QUEUE_BACKOFF_MIN_TIME || (startTime - lastMidTickExecuteFailure) <= TASK_EXECUTION_FAILURE_BACKOFF) {
+ // it's shown to be bad to constantly hit the queue (chunk loads slow to a crawl), even if no tasks are executed.
+ // so, backoff to prevent this
+ return;
+ }
+
+ co.aikar.timings.MinecraftTimings.midTickChunkTasks.startTiming();
+ try {
+ for (;;) {
+ boolean moreTasks = this.tickMidTickTasks();
+ long currTime = System.nanoTime();
+ long diff = currTime - startTime;
+
+ if (!moreTasks || diff >= MAX_CHUNK_EXEC_TIME) {
+ if (!moreTasks) {
+ lastMidTickExecuteFailure = currTime;
+ }
+
+ // note: negative values reduce the time
+ long overuse = diff - MAX_CHUNK_EXEC_TIME;
+ if (overuse >= (10L * 1000L * 1000L)) { // 10ms
+ // make sure something like a GC or dumb plugin doesn't screw us over...
+ overuse = 10L * 1000L * 1000L; // 10ms
+ }
+
+ double overuseCount = (double)overuse/(double)MAX_CHUNK_EXEC_TIME;
+ long extraSleep = (long)Math.round(overuseCount*CHUNK_TASK_QUEUE_BACKOFF_MIN_TIME);
+
+ lastMidTickExecute = currTime + extraSleep;
+ return;
+ }
+ }
+ } finally {
+ co.aikar.timings.MinecraftTimings.midTickChunkTasks.stopTiming();
+ }
+ }
+ // Paper end - execute chunk tasks mid tick
+
public MinecraftServer(OptionSet options, DataPackConfig datapackconfiguration, Thread thread, RegistryAccess.RegistryHolder iregistrycustom_dimension, LevelStorageSource.LevelStorageAccess convertable_conversionsession, WorldData savedata, PackRepository resourcepackrepository, Proxy proxy, DataFixer datafixer, ServerResources datapackresources, @Nullable MinecraftSessionService minecraftsessionservice, @Nullable GameProfileRepository gameprofilerepository, @Nullable GameProfileCache usercache, ChunkProgressListenerFactory worldloadlistenerfactory) {
super("Server");
SERVER = this; // Paper - better singleton
@@ -1311,6 +1381,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
private boolean pollTaskInternal() {
if (super.pollTask()) {
+ this.executeMidTickTasks(); // Paper - execute chunk tasks mid tick
return true;
} else {
if (this.haveTime()) {
diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
index 0ffd53590c883c0090c913356665058b6c5d3f3f..5d4f20a31ad99b4e808bb9a7aaa2153666af493f 100644
--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
@@ -198,7 +198,9 @@ public class ServerLevel extends Level implements WorldGenLevel {
private final StructureFeatureManager structureFeatureManager;
private final StructureCheck structureCheck;
private final boolean tickTime;
-
+ // Paper start - execute chunk tasks mid tick
+ public long lastMidTickExecuteFailure;
+ // Paper end - execute chunk tasks mid tick
// CraftBukkit start
private int tickPosition;
diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java
index 4484c455f4be73763f5aa1112be5969e18c092bc..9cc34ff8ec5db5a87faf0afb574543f01c8381c2 100644
--- a/src/main/java/net/minecraft/world/level/Level.java
+++ b/src/main/java/net/minecraft/world/level/Level.java
@@ -893,6 +893,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
public <T extends Entity> void guardEntityTick(Consumer<T> tickConsumer, T entity) {
try {
tickConsumer.accept(entity);
+ MinecraftServer.getServer().executeMidTickTasks(); // Paper - execute chunk tasks mid tick
} catch (Throwable throwable) {
if (throwable instanceof ThreadDeath) throw throwable; // Paper
// Paper start - Prevent tile entity and entity crashes

View file

@ -0,0 +1,218 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Spottedleaf <Spottedleaf@users.noreply.github.com>
Date: Sun, 21 Mar 2021 11:22:10 -0700
Subject: [PATCH] Do not copy visible chunks
For servers with a lot of chunk holders, copying for each
tickDistanceManager call can take up quite a bit in
the function. I saw approximately 1/3rd of the function
on the copy.
diff --git a/src/main/java/com/destroystokyo/paper/PaperCommand.java b/src/main/java/com/destroystokyo/paper/PaperCommand.java
index f436ab35798c9b6e6cb2eb60d2c02cbf9b742e69..85beb460aa59313cf2ace2d6a6bf24938e3e2b80 100644
--- a/src/main/java/com/destroystokyo/paper/PaperCommand.java
+++ b/src/main/java/com/destroystokyo/paper/PaperCommand.java
@@ -277,7 +277,7 @@ public class PaperCommand extends Command {
int ticking = 0;
int entityTicking = 0;
- for (ChunkHolder chunk : world.getChunkSource().chunkMap.updatingChunkMap.values()) {
+ for (ChunkHolder chunk : world.getChunkSource().chunkMap.updatingChunks.getUpdatingMap().values()) { // Paper - change updating chunks map
if (chunk.getFullChunkUnchecked() == null) {
continue;
}
diff --git a/src/main/java/net/minecraft/server/MCUtil.java b/src/main/java/net/minecraft/server/MCUtil.java
index 2fe519d4059fac06781c30e140895b604e13104f..7082c61b2dbe524c334efa56a73b24565b996b42 100644
--- a/src/main/java/net/minecraft/server/MCUtil.java
+++ b/src/main/java/net/minecraft/server/MCUtil.java
@@ -619,7 +619,7 @@ public final class MCUtil {
ServerLevel world = ((org.bukkit.craftbukkit.CraftWorld)bukkitWorld).getHandle();
ChunkMap chunkMap = world.getChunkSource().chunkMap;
- Long2ObjectLinkedOpenHashMap<ChunkHolder> visibleChunks = chunkMap.visibleChunkMap;
+ Long2ObjectLinkedOpenHashMap<ChunkHolder> visibleChunks = chunkMap.updatingChunks.getVisibleMap(); // Paper
DistanceManager chunkMapDistance = chunkMap.distanceManager;
List<ChunkHolder> allChunks = new ArrayList<>(visibleChunks.values());
List<ServerPlayer> players = world.players;
diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
index 0a3aa9808c7308917b990b8aee3740ada23a4b24..b244713d4a5bc6eb3d9536a56fdc9d688ca0d756 100644
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
@@ -117,9 +117,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
private static final int MIN_VIEW_DISTANCE = 3;
public static final int MAX_VIEW_DISTANCE = 33;
public static final int MAX_CHUNK_DISTANCE = 33 + ChunkStatus.maxDistance();
+ // Paper start - Don't copy
+ public final com.destroystokyo.paper.util.map.QueuedChangesMapLong2Object<ChunkHolder> updatingChunks = new com.destroystokyo.paper.util.map.QueuedChangesMapLong2Object<>();
+ // Paper end - Don't copy
public static final int FORCED_TICKET_LEVEL = 31;
- public final Long2ObjectLinkedOpenHashMap<ChunkHolder> updatingChunkMap = new Long2ObjectLinkedOpenHashMap();
- public volatile Long2ObjectLinkedOpenHashMap<ChunkHolder> visibleChunkMap;
+ // Paper - Don't copy
private final Long2ObjectLinkedOpenHashMap<ChunkHolder> pendingUnloads;
public final LongSet entitiesInLevel;
public final ServerLevel level;
@@ -238,7 +240,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
boolean unloadingPlayerChunk = false; // Paper - do not allow ticket level changes while unloading chunks
public ChunkMap(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureManager structureManager, Executor executor, BlockableEventLoop<Runnable> mainThreadExecutor, LightChunkGetter chunkProvider, ChunkGenerator chunkGenerator, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier<DimensionDataStorage> persistentStateManagerFactory, int viewDistance, boolean dsync) {
super(session.getDimensionPath(world.dimension()).resolve("region"), dataFixer, dsync);
- this.visibleChunkMap = this.updatingChunkMap.clone();
+ // Paper - don't copy
this.pendingUnloads = new Long2ObjectLinkedOpenHashMap();
this.entitiesInLevel = new LongOpenHashSet();
this.toDrop = new LongOpenHashSet();
@@ -382,12 +384,17 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
@Nullable
public ChunkHolder getUpdatingChunkIfPresent(long pos) {
- return (ChunkHolder) this.updatingChunkMap.get(pos);
+ return this.updatingChunks.getUpdating(pos); // Paper - Don't copy
}
@Nullable
public ChunkHolder getVisibleChunkIfPresent(long pos) {
- return (ChunkHolder) this.visibleChunkMap.get(pos);
+ // Paper start - Don't copy
+ if (Thread.currentThread() == this.level.thread) {
+ return this.updatingChunks.getVisible(pos);
+ }
+ return this.updatingChunks.getVisibleAsync(pos);
+ // Paper end - Don't copy
}
protected IntSupplier getChunkQueueLevel(long pos) {
@@ -529,7 +536,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
// Paper end
}
- this.updatingChunkMap.put(pos, holder);
+ this.updatingChunks.queueUpdate(pos, holder); // Paper - Don't copy
this.modified = true;
}
@@ -551,7 +558,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
protected void saveAllChunks(boolean flush) {
if (flush) {
- List<ChunkHolder> list = (List) this.visibleChunkMap.values().stream().filter(ChunkHolder::wasAccessibleSinceLastSave).peek(ChunkHolder::refreshAccessibility).collect(Collectors.toList());
+ List<ChunkHolder> list = (List) this.updatingChunks.getVisibleValuesCopy().stream().filter(ChunkHolder::wasAccessibleSinceLastSave).peek(ChunkHolder::refreshAccessibility).collect(Collectors.toList()); // Paper
MutableBoolean mutableboolean = new MutableBoolean();
do {
@@ -582,7 +589,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
//this.flushWorker(); // Paper - nuke IOWorker
this.level.asyncChunkTaskManager.flush(); // Paper - flush to preserve behavior compat with pre-async behaviour
} else {
- this.visibleChunkMap.values().forEach(this::saveChunkIfNeeded);
+ this.updatingChunks.getVisibleValuesCopy().forEach(this::saveChunkIfNeeded); // Paper
}
}
@@ -616,7 +623,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
while (longiterator.hasNext()) { // Spigot
long j = longiterator.nextLong();
longiterator.remove(); // Spigot
- ChunkHolder playerchunk = (ChunkHolder) this.updatingChunkMap.remove(j);
+ ChunkHolder playerchunk = this.updatingChunks.queueRemove(j); // Paper - Don't copy
if (playerchunk != null) {
this.pendingUnloads.put(j, playerchunk);
@@ -642,7 +649,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
int l = 0;
- ObjectIterator objectiterator = this.visibleChunkMap.values().iterator();
+ Iterator objectiterator = this.updatingChunks.getVisibleValuesCopy().iterator(); // Paper
while (l < 20 && shouldKeepTicking.getAsBoolean() && objectiterator.hasNext()) {
if (this.saveChunkIfNeeded((ChunkHolder) objectiterator.next())) {
@@ -720,7 +727,12 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
if (!this.modified) {
return false;
} else {
- this.visibleChunkMap = this.updatingChunkMap.clone();
+ // Paper start - Don't copy
+ synchronized (this.updatingChunks) {
+ this.updatingChunks.performUpdates();
+ }
+ // Paper end - Don't copy
+
this.modified = false;
return true;
}
@@ -1174,7 +1186,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
this.viewDistance = j;
this.distanceManager.updatePlayerTickets(this.viewDistance);
- ObjectIterator objectiterator = this.updatingChunkMap.values().iterator();
+ Iterator objectiterator = this.updatingChunks.getVisibleValuesCopy().iterator(); // Paper
while (objectiterator.hasNext()) {
ChunkHolder playerchunk = (ChunkHolder) objectiterator.next();
@@ -1216,7 +1228,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
public int size() {
- return this.visibleChunkMap.size();
+ return this.updatingChunks.getVisibleMap().size(); // Paper - Don't copy
}
public DistanceManager getDistanceManager() {
@@ -1224,13 +1236,13 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
protected Iterable<ChunkHolder> getChunks() {
- return Iterables.unmodifiableIterable(this.visibleChunkMap.values());
+ return Iterables.unmodifiableIterable(this.updatingChunks.getVisibleValuesCopy()); // Paper
}
void dumpChunks(Writer writer) throws IOException {
CsvOutput csvwriter = CsvOutput.builder().addColumn("x").addColumn("z").addColumn("level").addColumn("in_memory").addColumn("status").addColumn("full_status").addColumn("accessible_ready").addColumn("ticking_ready").addColumn("entity_ticking_ready").addColumn("ticket").addColumn("spawning").addColumn("block_entity_count").addColumn("ticking_ticket").addColumn("ticking_level").addColumn("block_ticks").addColumn("fluid_ticks").build(writer);
TickingTracker tickingtracker = this.distanceManager.tickingTracker();
- ObjectBidirectionalIterator objectbidirectionaliterator = this.visibleChunkMap.long2ObjectEntrySet().iterator();
+ ObjectBidirectionalIterator objectbidirectionaliterator = this.updatingChunks.getVisibleMap().clone().long2ObjectEntrySet().fastIterator(); // Paper
while (objectbidirectionaliterator.hasNext()) {
Entry<ChunkHolder> entry = (Entry) objectbidirectionaliterator.next();
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
index 69a4572c0eb4019491e099cf75049728a9aa4f99..a4188556cc6e657d9b288f2a410c716ca7b100db 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
@@ -149,7 +149,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
@Override
public int getTileEntityCount() {
// We don't use the full world tile entity list, so we must iterate chunks
- Long2ObjectLinkedOpenHashMap<ChunkHolder> chunks = world.getChunkSource().chunkMap.visibleChunkMap;
+ Long2ObjectLinkedOpenHashMap<ChunkHolder> chunks = world.getChunkSource().chunkMap.updatingChunks.getVisibleMap(); // Paper - change updating chunks map
int size = 0;
for (ChunkHolder playerchunk : chunks.values()) {
net.minecraft.world.level.chunk.LevelChunk chunk = playerchunk.getTickingChunk();
@@ -170,7 +170,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
public int getChunkCount() {
int ret = 0;
- for (ChunkHolder chunkHolder : world.getChunkSource().chunkMap.visibleChunkMap.values()) {
+ for (ChunkHolder chunkHolder : world.getChunkSource().chunkMap.updatingChunks.getVisibleMap().values()) { // Paper - change updating chunks map
if (chunkHolder.getTickingChunk() != null) {
++ret;
}
@@ -344,7 +344,18 @@ public class CraftWorld extends CraftRegionAccessor implements World {
@Override
public Chunk[] getLoadedChunks() {
- Long2ObjectLinkedOpenHashMap<ChunkHolder> chunks = this.world.getChunkSource().chunkMap.visibleChunkMap;
+ // Paper start
+ if (Thread.currentThread() != world.getLevel().thread) {
+ // Paper start - change updating chunks map
+ Long2ObjectLinkedOpenHashMap<ChunkHolder> chunks;
+ synchronized (world.getChunkSource().chunkMap.updatingChunks) {
+ chunks = world.getChunkSource().chunkMap.updatingChunks.getVisibleMap().clone();
+ }
+ return chunks.values().stream().map(ChunkHolder::getFullChunk).filter(Objects::nonNull).map(net.minecraft.world.level.chunk.LevelChunk::getBukkitChunk).toArray(Chunk[]::new);
+ // Paper end - change updating chunks map
+ }
+ // Paper end
+ Long2ObjectLinkedOpenHashMap<ChunkHolder> chunks = world.getChunkSource().chunkMap.updatingChunks.getVisibleMap(); // Paper - change updating chunks map
return chunks.values().stream().map(ChunkHolder::getFullChunk).filter(Objects::nonNull).map(net.minecraft.world.level.chunk.LevelChunk::getBukkitChunk).toArray(Chunk[]::new);
}

View file

@ -0,0 +1,759 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Spottedleaf <Spottedleaf@users.noreply.github.com>
Date: Sun, 2 Feb 2020 02:25:10 -0800
Subject: [PATCH] Attempt to recalculate regionfile header if it is corrupt
Instead of trying to relocate the chunk, which is seems to never
be the correct choice, so we end up duplicating or swapping chunks,
we instead drop the current regionfile header and recalculate -
hoping that at least then we don't swap chunks, and maybe recover
them all.
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
index f58050eaa1354ace7b3558d528ab8effdd1432aa..23b3203d04cf1dd2fc3c42e2c7b287a949c81699 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
@@ -66,6 +66,12 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class ChunkSerializer {
+ // Paper start
+ // TODO: Check on update
+ public static long getLastWorldSaveTime(CompoundTag chunkData) {
+ return chunkData.getLong("LastUpdate");
+ }
+ // Paper end
public static final Codec<PalettedContainer<BlockState>> BLOCK_STATE_CODEC = PalettedContainer.codec(Block.BLOCK_STATE_REGISTRY, BlockState.CODEC, PalettedContainer.Strategy.SECTION_STATES, Blocks.AIR.defaultBlockState());
private static final Logger LOGGER = LogManager.getLogger();
@@ -437,7 +443,7 @@ public class ChunkSerializer {
nbttagcompound.putInt("xPos", chunkcoordintpair.x);
nbttagcompound.putInt("yPos", chunk.getMinSection());
nbttagcompound.putInt("zPos", chunkcoordintpair.z);
- nbttagcompound.putLong("LastUpdate", asyncsavedata != null ? asyncsavedata.worldTime : world.getGameTime()); // Paper - async chunk unloading
+ nbttagcompound.putLong("LastUpdate", asyncsavedata != null ? asyncsavedata.worldTime : world.getGameTime()); // Paper - async chunk unloading // Paper - diff on change
nbttagcompound.putLong("InhabitedTime", chunk.getInhabitedTime());
nbttagcompound.putString("Status", chunk.getStatus().getName());
BlendingData blendingdata = chunk.getBlendingData();
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java
index a750b40be3ba5ba258ca2540ab0398deac5a6c5e..3c1724a86cccd3d66459f6c21ed358b47d2d0eac 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java
@@ -38,7 +38,7 @@ public class ChunkStorage implements AutoCloseable {
this.fixerUpper = dataFixer;
// Paper start - async chunk io
// remove IO worker
- this.regionFileCache = new RegionFileStorage(directory, dsync); // Paper - nuke IOWorker
+ this.regionFileCache = new RegionFileStorage(directory, dsync, true); // Paper - nuke IOWorker // Paper
// Paper end - async chunk io
}
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionBitmap.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionBitmap.java
index c8298a597818227de33a4afce4698ec0666cf758..6baceb6ce9021c489be6e79d338a9704285afa26 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionBitmap.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionBitmap.java
@@ -9,6 +9,27 @@ import java.util.BitSet;
public class RegionBitmap {
private final BitSet used = new BitSet();
+ // Paper start
+ public final void copyFrom(RegionBitmap other) {
+ BitSet thisBitset = this.used;
+ BitSet otherBitset = other.used;
+
+ for (int i = 0; i < Math.max(thisBitset.size(), otherBitset.size()); ++i) {
+ thisBitset.set(i, otherBitset.get(i));
+ }
+ }
+
+ public final boolean tryAllocate(int from, int length) {
+ BitSet bitset = this.used;
+ int firstSet = bitset.nextSetBit(from);
+ if (firstSet > 0 && firstSet < (from + length)) {
+ return false;
+ }
+ bitset.set(from, from + length);
+ return true;
+ }
+ // Paper end
+
public void force(int start, int size) {
this.used.set(start, start + size);
}
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java
index 293cce2c80fbdc18480977f5f6b24d6b4fa8dcf3..834fa7048e3affb4fcc734d56526b9fba5fa69ca 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java
@@ -52,6 +52,355 @@ public class RegionFile implements AutoCloseable {
public final java.util.concurrent.locks.ReentrantLock fileLock = new java.util.concurrent.locks.ReentrantLock(true); // Paper
public final Path regionFile; // Paper
+ // Paper start - try to recover from RegionFile header corruption
+ private static long roundToSectors(long bytes) {
+ long sectors = bytes >>> 12; // 4096 = 2^12
+ long remainingBytes = bytes & 4095;
+ long sign = -remainingBytes; // sign is 1 if nonzero
+ return sectors + (sign >>> 63);
+ }
+
+ private static final CompoundTag OVERSIZED_COMPOUND = new CompoundTag();
+
+ private CompoundTag attemptRead(long sector, int chunkDataLength, long fileLength) throws IOException {
+ try {
+ if (chunkDataLength < 0) {
+ return null;
+ }
+
+ long offset = sector * 4096L + 4L; // offset for chunk data
+
+ if ((offset + chunkDataLength) > fileLength) {
+ return null;
+ }
+
+ ByteBuffer chunkData = ByteBuffer.allocate(chunkDataLength);
+ if (chunkDataLength != this.file.read(chunkData, offset)) {
+ return null;
+ }
+
+ ((java.nio.Buffer)chunkData).flip();
+
+ byte compressionType = chunkData.get();
+ if (compressionType < 0) { // compressionType & 128 != 0
+ // oversized chunk
+ return OVERSIZED_COMPOUND;
+ }
+
+ RegionFileVersion compression = RegionFileVersion.fromId(compressionType);
+ if (compression == null) {
+ return null;
+ }
+
+ InputStream input = compression.wrap(new ByteArrayInputStream(chunkData.array(), chunkData.position(), chunkDataLength - chunkData.position()));
+
+ return NbtIo.read(new DataInputStream(input));
+ } catch (Exception ex) {
+ return null;
+ }
+ }
+
+ private int getLength(long sector) throws IOException {
+ ByteBuffer length = ByteBuffer.allocate(4);
+ if (4 != this.file.read(length, sector * 4096L)) {
+ return -1;
+ }
+
+ return length.getInt(0);
+ }
+
+ private void backupRegionFile() {
+ Path backup = this.regionFile.getParent().resolve(this.regionFile.getFileName() + "." + new java.util.Random().nextLong() + ".backup");
+ this.backupRegionFile(backup);
+ }
+
+ private void backupRegionFile(Path to) {
+ try {
+ this.file.force(true);
+ LOGGER.warn("Backing up regionfile \"" + this.regionFile.toAbsolutePath() + "\" to " + to.toAbsolutePath());
+ java.nio.file.Files.copy(this.regionFile, to);
+ LOGGER.warn("Backed up the regionfile to " + to.toAbsolutePath());
+ } catch (IOException ex) {
+ LOGGER.error("Failed to backup to " + to.toAbsolutePath(), ex);
+ }
+ }
+
+ private static boolean inSameRegionfile(ChunkPos first, ChunkPos second) {
+ return (first.x & ~31) == (second.x & ~31) && (first.z & ~31) == (second.z & ~31);
+ }
+
+ // note: only call for CHUNK regionfiles
+ boolean recalculateHeader() throws IOException {
+ if (!this.canRecalcHeader) {
+ return false;
+ }
+ ChunkPos ourLowerLeftPosition = RegionFileStorage.getRegionFileCoordinates(this.regionFile);
+ if (ourLowerLeftPosition == null) {
+ LOGGER.fatal("Unable to get chunk location of regionfile " + this.regionFile.toAbsolutePath() + ", cannot recover header");
+ return false;
+ }
+ synchronized (this) {
+ LOGGER.warn("Corrupt regionfile header detected! Attempting to re-calculate header offsets for regionfile " + this.regionFile.toAbsolutePath(), new Throwable());
+
+ // try to backup file so maybe it could be sent to us for further investigation
+
+ this.backupRegionFile();
+ CompoundTag[] compounds = new CompoundTag[32 * 32]; // only in the regionfile (i.e exclude mojang/aikar oversized data)
+ int[] rawLengths = new int[32 * 32]; // length of chunk data including 4 byte length field, bytes
+ int[] sectorOffsets = new int[32 * 32]; // in sectors
+ boolean[] hasAikarOversized = new boolean[32 * 32];
+
+ long fileLength = this.file.size();
+ long totalSectors = roundToSectors(fileLength);
+
+ // search the regionfile from start to finish for the most up-to-date chunk data
+
+ for (long i = 2, maxSector = Math.min((long)(Integer.MAX_VALUE >>> 8), totalSectors); i < maxSector; ++i) { // first two sectors are header, skip
+ int chunkDataLength = this.getLength(i);
+ CompoundTag compound = this.attemptRead(i, chunkDataLength, fileLength);
+ if (compound == null || compound == OVERSIZED_COMPOUND) {
+ continue;
+ }
+
+ ChunkPos chunkPos = ChunkSerializer.getChunkCoordinate(compound);
+ if (!inSameRegionfile(ourLowerLeftPosition, chunkPos)) {
+ LOGGER.error("Ignoring absolute chunk " + chunkPos + " in regionfile as it is not contained in the bounds of the regionfile '" + this.regionFile.toAbsolutePath() + "'. It should be in regionfile (" + (chunkPos.x >> 5) + "," + (chunkPos.z >> 5) + ")");
+ continue;
+ }
+ int location = (chunkPos.x & 31) | ((chunkPos.z & 31) << 5);
+
+ CompoundTag otherCompound = compounds[location];
+
+ if (otherCompound != null && ChunkSerializer.getLastWorldSaveTime(otherCompound) > ChunkSerializer.getLastWorldSaveTime(compound)) {
+ continue; // don't overwrite newer data.
+ }
+
+ // aikar oversized?
+ Path aikarOversizedFile = this.getOversizedFile(chunkPos.x, chunkPos.z);
+ boolean isAikarOversized = false;
+ if (Files.exists(aikarOversizedFile)) {
+ try {
+ CompoundTag aikarOversizedCompound = this.getOversizedData(chunkPos.x, chunkPos.z);
+ if (ChunkSerializer.getLastWorldSaveTime(compound) == ChunkSerializer.getLastWorldSaveTime(aikarOversizedCompound)) {
+ // best we got for an id. hope it's good enough
+ isAikarOversized = true;
+ }
+ } catch (Exception ex) {
+ LOGGER.error("Failed to read aikar oversized data for absolute chunk (" + chunkPos.x + "," + chunkPos.z + ") in regionfile " + this.regionFile.toAbsolutePath() + ", oversized data for this chunk will be lost", ex);
+ // fall through, if we can't read aikar oversized we can't risk corrupting chunk data
+ }
+ }
+
+ hasAikarOversized[location] = isAikarOversized;
+ compounds[location] = compound;
+ rawLengths[location] = chunkDataLength + 4;
+ sectorOffsets[location] = (int)i;
+
+ int chunkSectorLength = (int)roundToSectors(rawLengths[location]);
+ i += chunkSectorLength;
+ --i; // gets incremented next iteration
+ }
+
+ // forge style oversized data is already handled by the local search, and aikar data we just hope
+ // we get it right as aikar data has no identifiers we could use to try and find its corresponding
+ // local data compound
+
+ java.nio.file.Path containingFolder = this.externalFileDir;
+ Path[] regionFiles = Files.list(containingFolder).toArray(Path[]::new);
+ boolean[] oversized = new boolean[32 * 32];
+ RegionFileVersion[] oversizedCompressionTypes = new RegionFileVersion[32 * 32];
+
+ if (regionFiles != null) {
+ int lowerXBound = ourLowerLeftPosition.x; // inclusive
+ int lowerZBound = ourLowerLeftPosition.z; // inclusive
+ int upperXBound = lowerXBound + 32 - 1; // inclusive
+ int upperZBound = lowerZBound + 32 - 1; // inclusive
+
+ // read mojang oversized data
+ for (Path regionFile : regionFiles) {
+ ChunkPos oversizedCoords = getOversizedChunkPair(regionFile);
+ if (oversizedCoords == null) {
+ continue;
+ }
+
+ if ((oversizedCoords.x < lowerXBound || oversizedCoords.x > upperXBound) || (oversizedCoords.z < lowerZBound || oversizedCoords.z > upperZBound)) {
+ continue; // not in our regionfile
+ }
+
+ // ensure oversized data is valid & is newer than data in the regionfile
+
+ int location = (oversizedCoords.x & 31) | ((oversizedCoords.z & 31) << 5);
+
+ byte[] chunkData;
+ try {
+ chunkData = Files.readAllBytes(regionFile);
+ } catch (Exception ex) {
+ LOGGER.error("Failed to read oversized chunk data in file " + regionFile.toAbsolutePath() + ", data will be lost", ex);
+ continue;
+ }
+
+ CompoundTag compound = null;
+
+ // We do not know the compression type, as it's stored in the regionfile. So we need to try all of them
+ RegionFileVersion compression = null;
+ for (RegionFileVersion compressionType : RegionFileVersion.VERSIONS.values()) {
+ try {
+ DataInputStream in = new DataInputStream(compressionType.wrap(new ByteArrayInputStream(chunkData))); // typical java
+ compound = NbtIo.read((java.io.DataInput)in);
+ compression = compressionType;
+ break; // reaches here iff readNBT does not throw
+ } catch (Exception ex) {
+ continue;
+ }
+ }
+
+ if (compound == null) {
+ LOGGER.error("Failed to read oversized chunk data in file " + regionFile.toAbsolutePath() + ", it's corrupt. Its data will be lost");
+ continue;
+ }
+
+ if (!ChunkSerializer.getChunkCoordinate(compound).equals(oversizedCoords)) {
+ LOGGER.error("Can't use oversized chunk stored in " + regionFile.toAbsolutePath() + ", got absolute chunkpos: " + ChunkSerializer.getChunkCoordinate(compound) + ", expected " + oversizedCoords);
+ continue;
+ }
+
+ if (compounds[location] == null || ChunkSerializer.getLastWorldSaveTime(compound) > ChunkSerializer.getLastWorldSaveTime(compounds[location])) {
+ oversized[location] = true;
+ oversizedCompressionTypes[location] = compression;
+ }
+ }
+ }
+
+ // now we need to calculate a new offset header
+
+ int[] calculatedOffsets = new int[32 * 32];
+ RegionBitmap newSectorAllocations = new RegionBitmap();
+ newSectorAllocations.force(0, 2); // make space for header
+
+ // allocate sectors for normal chunks
+
+ for (int chunkX = 0; chunkX < 32; ++chunkX) {
+ for (int chunkZ = 0; chunkZ < 32; ++chunkZ) {
+ int location = chunkX | (chunkZ << 5);
+
+ if (oversized[location]) {
+ continue;
+ }
+
+ int rawLength = rawLengths[location]; // bytes
+ int sectorOffset = sectorOffsets[location]; // sectors
+ int sectorLength = (int)roundToSectors(rawLength);
+
+ if (newSectorAllocations.tryAllocate(sectorOffset, sectorLength)) {
+ calculatedOffsets[location] = sectorOffset << 8 | (sectorLength > 255 ? 255 : sectorLength); // support forge style oversized
+ } else {
+ LOGGER.error("Failed to allocate space for local chunk (overlapping data??) at (" + chunkX + "," + chunkZ + ") in regionfile " + this.regionFile.toAbsolutePath() + ", chunk will be regenerated");
+ }
+ }
+ }
+
+ // allocate sectors for oversized chunks
+
+ for (int chunkX = 0; chunkX < 32; ++chunkX) {
+ for (int chunkZ = 0; chunkZ < 32; ++chunkZ) {
+ int location = chunkX | (chunkZ << 5);
+
+ if (!oversized[location]) {
+ continue;
+ }
+
+ int sectorOffset = newSectorAllocations.allocate(1);
+ int sectorLength = 1;
+
+ try {
+ this.file.write(this.createExternalStub(oversizedCompressionTypes[location]), sectorOffset * 4096);
+ // only allocate in the new offsets if the write succeeds
+ calculatedOffsets[location] = sectorOffset << 8 | (sectorLength > 255 ? 255 : sectorLength); // support forge style oversized
+ } catch (IOException ex) {
+ newSectorAllocations.free(sectorOffset, sectorLength);
+ LOGGER.error("Failed to write new oversized chunk data holder, local chunk at (" + chunkX + "," + chunkZ + ") in regionfile " + this.regionFile.toAbsolutePath() + " will be regenerated");
+ }
+ }
+ }
+
+ // rewrite aikar oversized data
+
+ this.oversizedCount = 0;
+ for (int chunkX = 0; chunkX < 32; ++chunkX) {
+ for (int chunkZ = 0; chunkZ < 32; ++chunkZ) {
+ int location = chunkX | (chunkZ << 5);
+ int isAikarOversized = hasAikarOversized[location] ? 1 : 0;
+
+ this.oversizedCount += isAikarOversized;
+ this.oversized[location] = (byte)isAikarOversized;
+ }
+ }
+
+ if (this.oversizedCount > 0) {
+ try {
+ this.writeOversizedMeta();
+ } catch (Exception ex) {
+ LOGGER.error("Failed to write aikar oversized chunk meta, all aikar style oversized chunk data will be lost for regionfile " + this.regionFile.toAbsolutePath(), ex);
+ Files.deleteIfExists(this.getOversizedMetaFile());
+ }
+ } else {
+ Files.deleteIfExists(this.getOversizedMetaFile());
+ }
+
+ this.usedSectors.copyFrom(newSectorAllocations);
+
+ // before we overwrite the old sectors, print a summary of the chunks that got changed.
+
+ LOGGER.info("Starting summary of changes for regionfile " + this.regionFile.toAbsolutePath());
+
+ for (int chunkX = 0; chunkX < 32; ++chunkX) {
+ for (int chunkZ = 0; chunkZ < 32; ++chunkZ) {
+ int location = chunkX | (chunkZ << 5);
+
+ int oldOffset = this.offsets.get(location);
+ int newOffset = calculatedOffsets[location];
+
+ if (oldOffset == newOffset) {
+ continue;
+ }
+
+ this.offsets.put(location, newOffset); // overwrite incorrect offset
+
+ if (oldOffset == 0) {
+ // found lost data
+ LOGGER.info("Found missing data for local chunk (" + chunkX + "," + chunkZ + ") in regionfile " + this.regionFile.toAbsolutePath());
+ } else if (newOffset == 0) {
+ LOGGER.warn("Data for local chunk (" + chunkX + "," + chunkZ + ") could not be recovered in regionfile " + this.regionFile.toAbsolutePath() + ", it will be regenerated");
+ } else {
+ LOGGER.info("Local chunk (" + chunkX + "," + chunkZ + ") changed to point to newer data or correct chunk in regionfile " + this.regionFile.toAbsolutePath());
+ }
+ }
+ }
+
+ LOGGER.info("End of change summary for regionfile " + this.regionFile.toAbsolutePath());
+
+ // simply destroy the timestamp header, it's not used
+
+ for (int i = 0; i < 32 * 32; ++i) {
+ this.timestamps.put(i, calculatedOffsets[i] != 0 ? (int)System.currentTimeMillis() : 0); // write a valid timestamp for valid chunks, I do not want to find out whatever dumb program actually checks this
+ }
+
+ // write new header
+ try {
+ this.flush();
+ this.file.force(true); // try to ensure it goes through...
+ LOGGER.info("Successfully wrote new header to disk for regionfile " + this.regionFile.toAbsolutePath());
+ } catch (IOException ex) {
+ LOGGER.fatal("Failed to write new header to disk for regionfile " + this.regionFile.toAbsolutePath(), ex);
+ }
+ }
+
+ return true;
+ }
+
+ final boolean canRecalcHeader; // final forces compile fail on new constructor
+ // Paper end
+
// Paper start - Cache chunk status
private final ChunkStatus[] statuses = new ChunkStatus[32 * 32];
@@ -79,8 +428,19 @@ public class RegionFile implements AutoCloseable {
public RegionFile(Path file, Path directory, boolean dsync) throws IOException {
this(file, directory, RegionFileVersion.VERSION_DEFLATE, dsync);
}
+ // Paper start - add can recalc flag
+ public RegionFile(Path file, Path directory, boolean dsync, boolean canRecalcHeader) throws IOException {
+ this(file, directory, RegionFileVersion.VERSION_DEFLATE, dsync, canRecalcHeader);
+ }
+ // Paper end - add can recalc flag
public RegionFile(Path file, Path directory, RegionFileVersion outputChunkStreamVersion, boolean dsync) throws IOException {
+ // Paper start - add can recalc flag
+ this(file, directory, outputChunkStreamVersion, dsync, false);
+ }
+ public RegionFile(Path file, Path directory, RegionFileVersion outputChunkStreamVersion, boolean dsync, boolean canRecalcHeader) throws IOException {
+ this.canRecalcHeader = canRecalcHeader;
+ // Paper end - add can recalc flag
this.header = ByteBuffer.allocateDirect(8192);
this.regionFile = file; // Paper
initOversizedState(); // Paper
@@ -109,14 +469,16 @@ public class RegionFile implements AutoCloseable {
RegionFile.LOGGER.warn("Region file {} has truncated header: {}", file, i);
}
- long j = Files.size(file);
+ final long j = Files.size(file); final long regionFileSize = j; // Paper - recalculate header on header corruption
+ boolean needsHeaderRecalc = false; // Paper - recalculate header on header corruption
+ boolean hasBackedUp = false; // Paper - recalculate header on header corruption
for (int k = 0; k < 1024; ++k) {
- int l = this.offsets.get(k);
+ final int l = this.offsets.get(k); final int headerLocation = l; // Paper - we expect this to be the header location
if (l != 0) {
- int i1 = RegionFile.getSectorNumber(l);
- int j1 = RegionFile.getNumSectors(l);
+ final int i1 = RegionFile.getSectorNumber(l); final int offset = i1; // Paper - we expect this to be offset in file in sectors
+ int j1 = RegionFile.getNumSectors(l); final int sectorLength; // Paper - diff on change, we expect this to be sector length of region - watch out for reassignments
// Spigot start
if (j1 == 255) {
// We're maxed out, so we need to read the proper length from the section
@@ -125,32 +487,102 @@ public class RegionFile implements AutoCloseable {
j1 = (realLen.getInt(0) + 4) / 4096 + 1;
}
// Spigot end
+ sectorLength = j1; // Paper - diff on change, we expect this to be sector length of region
if (i1 < 2) {
RegionFile.LOGGER.warn("Region file {} has invalid sector at index: {}; sector {} overlaps with header", file, k, i1);
- this.offsets.put(k, 0);
+ //this.offsets.put(k, 0); // Paper - we catch this, but need it in the header for the summary change
} else if (j1 == 0) {
RegionFile.LOGGER.warn("Region file {} has an invalid sector at index: {}; size has to be > 0", file, k);
- this.offsets.put(k, 0);
+ //this.offsets.put(k, 0); // Paper - we catch this, but need it in the header for the summary change
} else if ((long) i1 * 4096L > j) {
RegionFile.LOGGER.warn("Region file {} has an invalid sector at index: {}; sector {} is out of bounds", file, k, i1);
- this.offsets.put(k, 0);
+ //this.offsets.put(k, 0); // Paper - we catch this, but need it in the header for the summary change
} else {
- this.usedSectors.force(i1, j1);
+ //this.usedSectors.force(i1, j1); // Paper - move this down so we can check if it fails to allocate
+ }
+ // Paper start - recalculate header on header corruption
+ if (offset < 2 || sectorLength <= 0 || ((long)offset * 4096L) > regionFileSize) {
+ if (canRecalcHeader) {
+ LOGGER.error("Detected invalid header for regionfile " + this.regionFile.toAbsolutePath() + "! Recalculating header...");
+ needsHeaderRecalc = true;
+ break;
+ } else {
+ // location = chunkX | (chunkZ << 5);
+ LOGGER.fatal("Detected invalid header for regionfile " + this.regionFile.toAbsolutePath() +
+ "! Cannot recalculate, removing local chunk (" + (headerLocation & 31) + "," + (headerLocation >>> 5) + ") from header");
+ if (!hasBackedUp) {
+ hasBackedUp = true;
+ this.backupRegionFile();
+ }
+ this.timestamps.put(headerLocation, 0); // be consistent, delete the timestamp too
+ this.offsets.put(headerLocation, 0); // delete the entry from header
+ continue;
+ }
+ }
+ boolean failedToAllocate = !this.usedSectors.tryAllocate(offset, sectorLength);
+ if (failedToAllocate) {
+ LOGGER.error("Overlapping allocation by local chunk (" + (headerLocation & 31) + "," + (headerLocation >>> 5) + ") in regionfile " + this.regionFile.toAbsolutePath());
}
+ if (failedToAllocate & !canRecalcHeader) {
+ // location = chunkX | (chunkZ << 5);
+ LOGGER.fatal("Detected invalid header for regionfile " + this.regionFile.toAbsolutePath() +
+ "! Cannot recalculate, removing local chunk (" + (headerLocation & 31) + "," + (headerLocation >>> 5) + ") from header");
+ if (!hasBackedUp) {
+ hasBackedUp = true;
+ this.backupRegionFile();
+ }
+ this.timestamps.put(headerLocation, 0); // be consistent, delete the timestamp too
+ this.offsets.put(headerLocation, 0); // delete the entry from header
+ continue;
+ }
+ needsHeaderRecalc |= failedToAllocate;
+ // Paper end - recalculate header on header corruption
}
}
+ // Paper start - recalculate header on header corruption
+ // we move the recalc here so comparison to old header is correct when logging to console
+ if (needsHeaderRecalc) { // true if header gave us overlapping allocations or had other issues
+ LOGGER.error("Recalculating regionfile " + this.regionFile.toAbsolutePath() + ", header gave erroneous offsets & locations");
+ this.recalculateHeader();
+ }
+ // Paper end
}
}
}
private Path getExternalChunkPath(ChunkPos chunkPos) {
- String s = "c." + chunkPos.x + "." + chunkPos.z + ".mcc";
+ String s = "c." + chunkPos.x + "." + chunkPos.z + ".mcc"; // Paper - diff on change
return this.externalFileDir.resolve(s);
}
+ // Paper start
+ private static ChunkPos getOversizedChunkPair(Path file) {
+ String fileName = file.getFileName().toString();
+
+ if (!fileName.startsWith("c.") || !fileName.endsWith(".mcc")) {
+ return null;
+ }
+
+ String[] split = fileName.split("\\.");
+
+ if (split.length != 4) {
+ return null;
+ }
+
+ try {
+ int x = Integer.parseInt(split[1]);
+ int z = Integer.parseInt(split[2]);
+
+ return new ChunkPos(x, z);
+ } catch (NumberFormatException ex) {
+ return null;
+ }
+ }
+ // Paper end
+
@Nullable
public synchronized DataInputStream getChunkDataInputStream(ChunkPos pos) throws IOException {
int i = this.getOffset(pos);
@@ -174,6 +606,11 @@ public class RegionFile implements AutoCloseable {
((java.nio.Buffer) bytebuffer).flip(); // CraftBukkit - decompile error
if (bytebuffer.remaining() < 5) {
RegionFile.LOGGER.error("Chunk {} header is truncated: expected {} but read {}", pos, l, bytebuffer.remaining());
+ // Paper start - recalculate header on regionfile corruption
+ if (this.canRecalcHeader && this.recalculateHeader()) {
+ return this.getChunkDataInputStream(pos);
+ }
+ // Paper end - recalculate header on regionfile corruption
return null;
} else {
int i1 = bytebuffer.getInt();
@@ -181,6 +618,11 @@ public class RegionFile implements AutoCloseable {
if (i1 == 0) {
RegionFile.LOGGER.warn("Chunk {} is allocated, but stream is missing", pos);
+ // Paper start - recalculate header on regionfile corruption
+ if (this.canRecalcHeader && this.recalculateHeader()) {
+ return this.getChunkDataInputStream(pos);
+ }
+ // Paper end - recalculate header on regionfile corruption
return null;
} else {
int j1 = i1 - 1;
@@ -188,17 +630,44 @@ public class RegionFile implements AutoCloseable {
if (RegionFile.isExternalStreamChunk(b0)) {
if (j1 != 0) {
RegionFile.LOGGER.warn("Chunk has both internal and external streams");
+ // Paper start - recalculate header on regionfile corruption
+ if (this.canRecalcHeader && this.recalculateHeader()) {
+ return this.getChunkDataInputStream(pos);
+ }
+ // Paper end - recalculate header on regionfile corruption
}
- return this.createExternalChunkInputStream(pos, RegionFile.getExternalChunkVersion(b0));
+ // Paper start - recalculate header on regionfile corruption
+ final DataInputStream ret = this.createExternalChunkInputStream(pos, RegionFile.getExternalChunkVersion(b0));
+ if (ret == null && this.canRecalcHeader && this.recalculateHeader()) {
+ return this.getChunkDataInputStream(pos);
+ }
+ return ret;
+ // Paper end - recalculate header on regionfile corruption
} else if (j1 > bytebuffer.remaining()) {
RegionFile.LOGGER.error("Chunk {} stream is truncated: expected {} but read {}", pos, j1, bytebuffer.remaining());
+ // Paper start - recalculate header on regionfile corruption
+ if (this.canRecalcHeader && this.recalculateHeader()) {
+ return this.getChunkDataInputStream(pos);
+ }
+ // Paper end - recalculate header on regionfile corruption
return null;
} else if (j1 < 0) {
RegionFile.LOGGER.error("Declared size {} of chunk {} is negative", i1, pos);
+ // Paper start - recalculate header on regionfile corruption
+ if (this.canRecalcHeader && this.recalculateHeader()) {
+ return this.getChunkDataInputStream(pos);
+ }
+ // Paper end - recalculate header on regionfile corruption
return null;
} else {
- return this.createChunkInputStream(pos, b0, RegionFile.createStream(bytebuffer, j1));
+ // Paper start - recalculate header on regionfile corruption
+ final DataInputStream ret = this.createChunkInputStream(pos, b0, RegionFile.createStream(bytebuffer, j1));
+ if (ret == null && this.canRecalcHeader && this.recalculateHeader()) {
+ return this.getChunkDataInputStream(pos);
+ }
+ return ret;
+ // Paper end - recalculate header on regionfile corruption
}
}
}
@@ -373,10 +842,15 @@ public class RegionFile implements AutoCloseable {
}
private ByteBuffer createExternalStub() {
+ // Paper start - add version param
+ return this.createExternalStub(this.version);
+ }
+ private ByteBuffer createExternalStub(RegionFileVersion version) {
+ // Paper end - add version param
ByteBuffer bytebuffer = ByteBuffer.allocate(5);
bytebuffer.putInt(1);
- bytebuffer.put((byte) (this.version.getId() | 128));
+ bytebuffer.put((byte) (version.getId() | 128)); // Paper - replace with version param
((java.nio.Buffer) bytebuffer).flip(); // CraftBukkit - decompile error
return bytebuffer;
}
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
index 0c5f5b2960f1a0d4bbd41c3c3baf101b4c388c43..cf555bd67599441ce53ae8559c1ffd4bb681ac71 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
@@ -26,7 +26,15 @@ public class RegionFileStorage implements AutoCloseable {
private final Path folder;
private final boolean sync;
+ private final boolean isChunkData; // Paper
+
RegionFileStorage(Path directory, boolean dsync) {
+ // Paper start - add isChunkData param
+ this(directory, dsync, false);
+ }
+ RegionFileStorage(Path directory, boolean dsync, boolean isChunkData) {
+ this.isChunkData = isChunkData;
+ // Paper end - add isChunkData param
this.folder = directory;
this.sync = dsync;
}
@@ -88,9 +96,9 @@ public class RegionFileStorage implements AutoCloseable {
Files.createDirectories(this.folder);
Path path = this.folder;
int j = chunkcoordintpair.getRegionX();
- Path path1 = path.resolve("r." + j + "." + chunkcoordintpair.getRegionZ() + ".mca");
+ Path path1 = path.resolve("r." + j + "." + chunkcoordintpair.getRegionZ() + ".mca"); // Paper - diff on change
if (existingOnly && !Files.exists(path1)) return null; // CraftBukkit
- RegionFile regionfile1 = new RegionFile(path1, this.folder, this.sync);
+ RegionFile regionfile1 = new RegionFile(path1, this.folder, this.sync, this.isChunkData); // Paper - allow for chunk regionfiles to regen header
this.regionCache.putAndMoveToFirst(i, regionfile1);
// Paper start
@@ -178,6 +186,13 @@ public class RegionFileStorage implements AutoCloseable {
if (regionfile == null) {
return null;
}
+ // Paper start - Add regionfile parameter
+ return this.read(pos, regionfile);
+ }
+ public CompoundTag read(ChunkPos pos, RegionFile regionfile) throws IOException {
+ // We add the regionfile parameter to avoid the potential deadlock (on fileLock) if we went back to obtain a regionfile
+ // if we decide to re-read
+ // Paper end
// CraftBukkit end
try { // Paper
DataInputStream datainputstream = regionfile.getChunkDataInputStream(pos);
@@ -194,6 +209,20 @@ public class RegionFileStorage implements AutoCloseable {
try {
if (datainputstream != null) {
nbttagcompound = NbtIo.read((DataInput) datainputstream);
+ // Paper start - recover from corrupt regionfile header
+ if (this.isChunkData) {
+ ChunkPos chunkPos = ChunkSerializer.getChunkCoordinate(nbttagcompound);
+ if (!chunkPos.equals(pos)) {
+ net.minecraft.server.MinecraftServer.LOGGER.error("Attempting to read chunk data at " + pos + " but got chunk data for " + chunkPos + " instead! Attempting regionfile recalculation for regionfile " + regionfile.regionFile.toAbsolutePath());
+ if (regionfile.recalculateHeader()) {
+ regionfile.fileLock.lock(); // otherwise we will unlock twice and only lock once.
+ return this.read(pos, regionfile);
+ }
+ net.minecraft.server.MinecraftServer.LOGGER.fatal("Can't recalculate regionfile header, regenerating chunk " + pos + " for " + regionfile.regionFile.toAbsolutePath());
+ return null;
+ }
+ }
+ // Paper end - recover from corrupt regionfile header
break label43;
}
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileVersion.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileVersion.java
index 95070af5e5bb7013ce7126ba9f725b43e3c4c749..97d4ae5619dcc0922e0381b1bb45a135f514e3af 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileVersion.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileVersion.java
@@ -14,7 +14,7 @@ import javax.annotation.Nullable;
import net.minecraft.util.FastBufferedInputStream;
public class RegionFileVersion {
- private static final Int2ObjectMap<RegionFileVersion> VERSIONS = new Int2ObjectOpenHashMap<>();
+ public static final Int2ObjectMap<RegionFileVersion> VERSIONS = new Int2ObjectOpenHashMap<>(); // Paper - public
public static final RegionFileVersion VERSION_GZIP = register(new RegionFileVersion(1, (inputStream) -> {
return new FastBufferedInputStream(new GZIPInputStream(inputStream));
}, (outputStream) -> {