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:
parent
fdde23eecf
commit
dc58f85df2
14 changed files with 227 additions and 267 deletions
|
@ -1,40 +0,0 @@
|
|||
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 d4ea81824acdc1220616b24ea0f465db97f4a343..b0a6eb7846580489e0476e69565676e77fd224cd 100644
|
||||
--- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
|
||||
+++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java
|
||||
@@ -599,7 +599,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) -> {
|
||||
@@ -616,7 +622,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
|
|
@ -1,64 +0,0 @@
|
|||
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 59437f04911662f06596ef61b91017caa6427eec..69faebb95924946f648cf9f86ff777d3274e3f28 100644
|
||||
--- a/src/main/java/net/minecraft/Util.java
|
||||
+++ b/src/main/java/net/minecraft/Util.java
|
||||
@@ -67,6 +67,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 66dfa8c844963091b63e1f2f85d0da6dd2cd083c..f5b8ff3032e46173c0e8920efb336b9901331259 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 6381544b0038de9a09c01238638e4e127e4eddc6..f61c313195c3d16d996721b2f8cd0d9a10ce1aaf 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
|
||||
@@ -150,7 +150,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) {
|
|
@ -1,20 +0,0 @@
|
|||
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 + "!" );
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,178 +0,0 @@
|
|||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Jason Penilla <11360596+jpenilla@users.noreply.github.com>
|
||||
Date: Sun, 22 Aug 2021 23:03:33 -0700
|
||||
Subject: [PATCH] Fix and optimize legacy world conversion
|
||||
|
||||
CraftBukkit breaks legacy world conversion in three ways:
|
||||
- Writes userdata to the path of the userdata folder rather than to
|
||||
the correct file inside the aforementioned folder. This causes the
|
||||
userdata folder to fail to be created as a file already exists at
|
||||
its path.
|
||||
- Makes changes to how multiworld works, without modifying
|
||||
McRegionUpgrader to be aware of these changes.
|
||||
- Calls methods on Bukkit before the server is initialized.
|
||||
|
||||
This patch fixes all of these issues, and also threads the
|
||||
McRegionUpgrader to improve performance.
|
||||
|
||||
diff --git a/src/main/java/net/minecraft/server/players/OldUsersConverter.java b/src/main/java/net/minecraft/server/players/OldUsersConverter.java
|
||||
index 8703f97dc2f392b136c6851aa09b607cbfdfa5de..ade010fe3b62a4624b009c6d665e9909b2d314ac 100644
|
||||
--- a/src/main/java/net/minecraft/server/players/OldUsersConverter.java
|
||||
+++ b/src/main/java/net/minecraft/server/players/OldUsersConverter.java
|
||||
@@ -355,14 +355,14 @@ public class OldUsersConverter {
|
||||
}
|
||||
|
||||
private void movePlayerFile(File playerDataFolder, String fileName, String uuid) {
|
||||
- File file5 = new File(file, fileName + ".dat");
|
||||
+ File file5 = new File(file, fileName + ".dat"); // Paper - diff on change
|
||||
File file6 = new File(playerDataFolder, uuid + ".dat");
|
||||
|
||||
// CraftBukkit start - Use old file name to seed lastKnownName
|
||||
CompoundTag root = null;
|
||||
|
||||
try {
|
||||
- root = NbtIo.readCompressed(new java.io.FileInputStream(file5));
|
||||
+ root = NbtIo.readCompressed(new java.io.FileInputStream(file5)); // Paper - diff on change
|
||||
} catch (Exception exception) {
|
||||
exception.printStackTrace();
|
||||
ServerInternalException.reportInternalException(exception); // Paper
|
||||
@@ -376,7 +376,7 @@ public class OldUsersConverter {
|
||||
data.putString("lastKnownName", fileName);
|
||||
|
||||
try {
|
||||
- NbtIo.writeCompressed(root, new java.io.FileOutputStream(file2));
|
||||
+ NbtIo.writeCompressed(root, new java.io.FileOutputStream(file5)); // Paper - write to correct path (diff on change)
|
||||
} catch (Exception exception) {
|
||||
exception.printStackTrace();
|
||||
ServerInternalException.reportInternalException(exception); // Paper
|
||||
diff --git a/src/main/java/net/minecraft/world/level/storage/LevelStorageSource.java b/src/main/java/net/minecraft/world/level/storage/LevelStorageSource.java
|
||||
index af8a555c777b5abbaa2615d2ff03f03a9a93847e..b794c02ea36bdc901b1f6a160095abb3fcfe9b60 100644
|
||||
--- a/src/main/java/net/minecraft/world/level/storage/LevelStorageSource.java
|
||||
+++ b/src/main/java/net/minecraft/world/level/storage/LevelStorageSource.java
|
||||
@@ -348,6 +348,12 @@ public class LevelStorageSource {
|
||||
});
|
||||
}
|
||||
|
||||
+ // Paper start - copied from vanilla before below CB diff
|
||||
+ public File getDimensionPathForLegacyConversion(ResourceKey<Level> key) {
|
||||
+ return DimensionType.getStorageFolder(key, this.levelPath.toFile());
|
||||
+ }
|
||||
+ // Paper end
|
||||
+
|
||||
public File getDimensionPath(ResourceKey<Level> key) {
|
||||
return LevelStorageSource.getFolder(this.levelPath.toFile(), this.dimensionType); // CraftBukkit
|
||||
}
|
||||
diff --git a/src/main/java/net/minecraft/world/level/storage/McRegionUpgrader.java b/src/main/java/net/minecraft/world/level/storage/McRegionUpgrader.java
|
||||
index 87705fc8ee32016bf5ca533eb278bf32df08d3a3..b9bcbcf509ebb59d15082ce0faef8e405c16bdcc 100644
|
||||
--- a/src/main/java/net/minecraft/world/level/storage/McRegionUpgrader.java
|
||||
+++ b/src/main/java/net/minecraft/world/level/storage/McRegionUpgrader.java
|
||||
@@ -35,13 +35,29 @@ public class McRegionUpgrader {
|
||||
private static final String MCREGION_EXTENSION = ".mcr";
|
||||
|
||||
static boolean convertLevel(LevelStorageSource.LevelStorageAccess storageSession, ProgressListener progressListener) {
|
||||
+ // Paper start
|
||||
+ progressListener = new ProgressListener() {
|
||||
+ @Override
|
||||
+ public void progressStartNoAbort(net.minecraft.network.chat.Component title) {}
|
||||
+ @Override
|
||||
+ public void progressStart(net.minecraft.network.chat.Component title) {}
|
||||
+ @Override
|
||||
+ public void progressStage(net.minecraft.network.chat.Component task) {}
|
||||
+ @Override
|
||||
+ public void progressStagePercentage(int percentage) {}
|
||||
+ @Override
|
||||
+ public void stop() {}
|
||||
+ };
|
||||
+ // Paper end
|
||||
progressListener.progressStagePercentage(0);
|
||||
List<File> list = Lists.newArrayList();
|
||||
List<File> list2 = Lists.newArrayList();
|
||||
List<File> list3 = Lists.newArrayList();
|
||||
- File file = storageSession.getDimensionPath(Level.OVERWORLD);
|
||||
- File file2 = storageSession.getDimensionPath(Level.NETHER);
|
||||
- File file3 = storageSession.getDimensionPath(Level.END);
|
||||
+ // Paper start
|
||||
+ File file = storageSession.getDimensionPathForLegacyConversion(Level.OVERWORLD);
|
||||
+ File file2 = storageSession.getDimensionPathForLegacyConversion(Level.NETHER);
|
||||
+ File file3 = storageSession.getDimensionPathForLegacyConversion(Level.END);
|
||||
+ // Paper end
|
||||
LOGGER.info("Scanning folders...");
|
||||
addRegionFiles(file, list);
|
||||
if (file2.exists()) {
|
||||
@@ -88,14 +104,58 @@ public class McRegionUpgrader {
|
||||
}
|
||||
|
||||
private static void convertRegions(RegistryAccess.RegistryHolder registryManager, File directory, Iterable<File> files, BiomeSource biomeSource, int i, int j, ProgressListener progressListener) {
|
||||
- for(File file : files) {
|
||||
- convertRegion(registryManager, directory, file, biomeSource, i, j, progressListener);
|
||||
- ++i;
|
||||
- int k = (int)Math.round(100.0D * (double)i / (double)j);
|
||||
- progressListener.progressStagePercentage(k);
|
||||
+ // Paper start - thread this because it's dead simple
|
||||
+ convertRegionsThreaded(registryManager, directory, files, biomeSource, i, j, progressListener);
|
||||
+ }
|
||||
+
|
||||
+ private static void convertRegionsThreaded(RegistryAccess.RegistryHolder registryManager, File directory, Iterable<File> files, BiomeSource biomeSource, int progress, int total, ProgressListener progressListener) {
|
||||
+ if (!files.iterator().hasNext()) {
|
||||
+ return;
|
||||
}
|
||||
|
||||
+ final int threads = Runtime.getRuntime().availableProcessors() / 2;
|
||||
+ final java.util.concurrent.ExecutorService threadPool = java.util.concurrent.Executors.newFixedThreadPool(Math.max(1, threads), new java.util.concurrent.ThreadFactory() {
|
||||
+ private final java.util.concurrent.atomic.AtomicInteger threadCounter = new java.util.concurrent.atomic.AtomicInteger();
|
||||
+
|
||||
+ @Override
|
||||
+ public Thread newThread(final Runnable run) {
|
||||
+ final Thread ret = new Thread(run);
|
||||
+
|
||||
+ ret.setName("World upgrader thread for directory " + directory + " #" + this.threadCounter.getAndIncrement());
|
||||
+ ret.setUncaughtExceptionHandler((thread, throwable) -> {
|
||||
+ LOGGER.fatal("Error upgrading world", throwable);
|
||||
+ });
|
||||
+
|
||||
+ return ret;
|
||||
+ }
|
||||
+ });
|
||||
+ final java.util.concurrent.atomic.AtomicInteger converted = new java.util.concurrent.atomic.AtomicInteger(progress);
|
||||
+
|
||||
+ final long start = System.nanoTime();
|
||||
+
|
||||
+ for (final File file : files) {
|
||||
+ threadPool.execute(() -> {
|
||||
+ convertRegion(registryManager, directory, file, biomeSource, 0, total, progressListener);
|
||||
+ converted.incrementAndGet();
|
||||
+ });
|
||||
+ }
|
||||
+ threadPool.shutdown();
|
||||
+
|
||||
+ final java.text.DecimalFormat format = new java.text.DecimalFormat("#0.00");
|
||||
+ while (!threadPool.isTerminated()) {
|
||||
+ final int getConverted = converted.get();
|
||||
+ LOGGER.info("Converting... {}/{} ({}%)", getConverted, total, format.format(100.0D * (double) getConverted / (double) total));
|
||||
+ try {
|
||||
+ Thread.sleep(1000L);
|
||||
+ } catch (final InterruptedException ignored) {}
|
||||
+ }
|
||||
+
|
||||
+ final long end = System.nanoTime();
|
||||
+
|
||||
+ final double durationSeconds = Math.ceil((end - start) * 1.0e-9);
|
||||
+ LOGGER.info("Conversion for {} completed in {}s", directory, durationSeconds);
|
||||
}
|
||||
+ // Paper end
|
||||
|
||||
private static void convertRegion(RegistryAccess.RegistryHolder registryManager, File directory, File file, BiomeSource biomeSource, int i, int j, ProgressListener progressListener) {
|
||||
String string = file.getName();
|
||||
diff --git a/src/main/java/net/minecraft/world/level/storage/PrimaryLevelData.java b/src/main/java/net/minecraft/world/level/storage/PrimaryLevelData.java
|
||||
index 4d0af984490b556a9911c3b8fdca1e168e6fe932..c20e5d69b4ad8adcdaffb56e4e2a24596ae16edf 100644
|
||||
--- a/src/main/java/net/minecraft/world/level/storage/PrimaryLevelData.java
|
||||
+++ b/src/main/java/net/minecraft/world/level/storage/PrimaryLevelData.java
|
||||
@@ -212,7 +212,7 @@ public class PrimaryLevelData implements ServerLevelData, WorldData {
|
||||
levelTag.putUUID("WanderingTraderId", this.wanderingTraderId);
|
||||
}
|
||||
|
||||
- levelTag.putString("Bukkit.Version", Bukkit.getName() + "/" + Bukkit.getVersion() + "/" + Bukkit.getBukkitVersion()); // CraftBukkit
|
||||
+ if (Bukkit.getServer() != null) levelTag.putString("Bukkit.Version", Bukkit.getName() + "/" + Bukkit.getVersion() + "/" + Bukkit.getBukkitVersion()); // CraftBukkit // Paper - server may not be started yet
|
||||
}
|
||||
|
||||
@Override
|
|
@ -1,992 +0,0 @@
|
|||
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 e41b2fa1db6fb77a26cdb498904021b430e35be0..488d1e24b3e8f0fd8dc973d450215e4216720db3 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 7b1d2748328ffc1447bcacd1316f2c6fdbaf92b0..2b79ace854461b216dc4970d1cc4a3953a51dd50 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(File directory, DataFixer dataFixer, boolean dsync, LevelHeightAccessor world) {
|
||||
super(directory, 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> predicate, Predicate<BlockPos> predicate2, BlockPos blockPos, int i, PoiManager.Occupancy occupancy) {
|
||||
- return this.getInRange(predicate, blockPos, i, occupancy).map(PoiRecord::getPos).filter(predicate2).min(Comparator.comparingDouble((blockPos2) -> {
|
||||
- return blockPos2.distSqr(blockPos);
|
||||
- }));
|
||||
+ // Paper start - re-route to faster logic
|
||||
+ BlockPos ret = io.papermc.paper.util.PoiAccess.findClosestPoiDataPosition(this, predicate, predicate2, blockPos, i, i*i, occupancy, 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((poiRecord) -> {
|
||||
- return positionPredicate.test(poiRecord.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 5a86bc6f552913e2978c61233148db22e3a240f1..16f9796adcb83350e97220ba0e96bfee998f1ff4 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 90f7b06bd2c558be35c4577044fa033e1fb5cc22..e2d1149cbe75b0689c9f816b87ebb7ba0d6f56c8 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 ca3e143e641933fa6b9499bbaaa1836877d90c52..4e3987e6233e84647d019e41ba6afe7c5d09ea80 100644
|
||||
--- a/src/main/java/net/minecraft/world/level/portal/PortalForcer.java
|
||||
+++ b/src/main/java/net/minecraft/world/level/portal/PortalForcer.java
|
||||
@@ -52,16 +52,37 @@ 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).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;
|
||||
+ }
|
||||
+ 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();
|
|
@ -1,147 +0,0 @@
|
|||
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 3eff1c97dcdc6bd8a0c4b7c5bbead2bd68490a4b..032d65a489d65e9b5b5066dff80c65d2e1b28c82 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 enumprotocol, ConnectionProtocol enumprotocol1) {
|
||||
+ // Paper start - add flush parameter
|
||||
+ this.doSendPacket(packet, callback, enumprotocol, enumprotocol1, true);
|
||||
+ }
|
||||
+ private void doSendPacket(Packet<?> packet, @Nullable GenericFutureListener<? extends Future<? super Void>> callback, ConnectionProtocol enumprotocol, ConnectionProtocol enumprotocol1, boolean flush) {
|
||||
+ // Paper end - add flush parameter
|
||||
if (enumprotocol != enumprotocol1) {
|
||||
this.setProtocol(enumprotocol);
|
||||
}
|
||||
@@ -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;
|
|
@ -1,44 +0,0 @@
|
|||
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 573e5ba276d270b8f67727dc1fbe6bfd7f2a28b1..e89396ea1d06f9e4a0a58d86cb7f9d857d50dc0a 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
|
@ -1,168 +0,0 @@
|
|||
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/com/destroystokyo/paper/server/ticklist/PaperTickList.java b/src/main/java/com/destroystokyo/paper/server/ticklist/PaperTickList.java
|
||||
index 5fdaefc128956581be4bb9b34199fd6410563991..b7edc1121797bc1c57e25f540ed0124fa8b36b7a 100644
|
||||
--- a/src/main/java/com/destroystokyo/paper/server/ticklist/PaperTickList.java
|
||||
+++ b/src/main/java/com/destroystokyo/paper/server/ticklist/PaperTickList.java
|
||||
@@ -312,6 +312,7 @@ public final class PaperTickList<T> extends ServerTickList<T> { // extend to avo
|
||||
toTick.tickState = STATE_SCHEDULED;
|
||||
this.addToNotTickingReady(toTick);
|
||||
}
|
||||
+ MinecraftServer.getServer().executeMidTickTasks(); // Paper - exec chunk tasks during world tick
|
||||
} catch (final Throwable thr) {
|
||||
// start copy from TickListServer // TODO check on update
|
||||
CrashReport crashreport = CrashReport.forThrowable(thr, "Exception while ticking");
|
||||
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
|
||||
index 98eb8318413014f0650dc5c80125aa84b51cfc93..57cb2722e973cfc8edc845bc7154b8b8bbb11e12 100644
|
||||
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
|
||||
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
|
||||
@@ -330,6 +330,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
|
||||
@@ -1325,6 +1395,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/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
|
||||
index d6981bbcf480c5856b51960013d144beba2361b3..39840403da99252c5d634e99e1da19f6066dee7c 100644
|
||||
--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
|
||||
+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
|
||||
@@ -1023,6 +1023,7 @@ public class ServerChunkCache extends ChunkSource {
|
||||
Collections.shuffle(shuffled);
|
||||
iterator = shuffled.iterator();
|
||||
}
|
||||
+ int chunksTicked = 0; // Paper
|
||||
try { while (iterator.hasNext()) {
|
||||
LevelChunk chunk = iterator.next();
|
||||
ChunkHolder playerchunk = chunk.playerChunk;
|
||||
@@ -1045,6 +1046,7 @@ public class ServerChunkCache extends ChunkSource {
|
||||
this.level.tickChunk(chunk, k);
|
||||
// this.level.timings.doTickTiles.stopTiming(); // Spigot // Paper
|
||||
}
|
||||
+ if ((chunksTicked++ & 1) == 0) net.minecraft.server.MinecraftServer.getServer().executeMidTickTasks(); // Paper
|
||||
}
|
||||
} // Paper start - optimise chunk tick iteration
|
||||
} finally {
|
||||
diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
|
||||
index 5c588c39de11bbabdc2f50ef4204007c622fdc6a..e219e385df356531639cb1b4bf993dca9034aa1d 100644
|
||||
--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
|
||||
+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
|
||||
@@ -190,7 +190,9 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
||||
final Int2ObjectMap<EnderDragonPart> dragonParts;
|
||||
private final StructureFeatureManager structureFeatureManager;
|
||||
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 1dd2e968bde16da2d2da63ca3c30515e1fd5b620..3a6f79233cec7aee87be20787b6deae4b313f0ac 100644
|
||||
--- a/src/main/java/net/minecraft/world/level/Level.java
|
||||
+++ b/src/main/java/net/minecraft/world/level/Level.java
|
||||
@@ -903,6 +903,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
|
|
@ -1,21 +0,0 @@
|
|||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Spottedleaf <Spottedleaf@users.noreply.github.com>
|
||||
Date: Sun, 11 Apr 2021 02:58:48 -0700
|
||||
Subject: [PATCH] Don't read neighbour chunk data off disk when converting
|
||||
chunks
|
||||
|
||||
Lighting is purged on update anyways, so let's not add more
|
||||
into the conversion process
|
||||
|
||||
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 176610b31f66b890afe61f4de46c412382bb8d22..037bbd562e2f35e17c324cd200c55c5e6cb5d768 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
|
||||
@@ -40,6 +40,7 @@ public class ChunkStorage implements AutoCloseable {
|
||||
|
||||
// CraftBukkit start
|
||||
private boolean check(ServerChunkCache cps, int x, int z) throws IOException {
|
||||
+ if (true) return true; // Paper - this isn't even needed anymore, light is purged updating to 1.14+, why are we holding up the conversion process reading chunk data off disk - return true, we need to set light populated to true so the converter recognizes the chunk as being "full"
|
||||
ChunkPos pos = new ChunkPos(x, z);
|
||||
if (cps != null) {
|
||||
//com.google.common.base.Preconditions.checkState(org.bukkit.Bukkit.isPrimaryThread(), "primary thread"); // Paper - this function is now MT-Safe
|
|
@ -1,199 +0,0 @@
|
|||
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 807bbe54f6516f794bdcb735bb7b8d6812e3ef01..2ef4b4c2ff81d0fa33d4630593266066d8e6a6f3 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 436ea61d284120a43123709f0213ec56870147dc..d2ea11d35ea111c349df5aa375d7ee8831658262 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 aae8dca773686bec3f867b79aa11d668032b9244..2f5ab00d26dcf027ec0e152a8bf17686a280ae50 100644
|
||||
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
|
||||
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
|
||||
@@ -114,9 +114,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;
|
||||
@@ -344,7 +346,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(new File(session.getDimensionPath(world.dimension()), "region"), dataFixer, dsync);
|
||||
- this.visibleChunkMap = this.updatingChunkMap.clone();
|
||||
+ // Paper - don't copy
|
||||
this.pendingUnloads = new Long2ObjectLinkedOpenHashMap();
|
||||
this.entitiesInLevel = new LongOpenHashSet();
|
||||
this.toDrop = new LongOpenHashSet();
|
||||
@@ -676,12 +678,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) {
|
||||
@@ -833,7 +840,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;
|
||||
}
|
||||
|
||||
@@ -913,7 +920,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 {
|
||||
@@ -944,7 +951,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
||||
this.level.asyncChunkTaskManager.flush(); // Paper - flush to preserve behavior compat with pre-async behaviour
|
||||
// this.i(); // Paper - nuke IOWorker
|
||||
} else {
|
||||
- this.visibleChunkMap.values().stream().filter(ChunkHolder::wasAccessibleSinceLastSave).forEach((playerchunk) -> {
|
||||
+ this.updatingChunks.getVisibleValuesCopy().stream().filter(ChunkHolder::wasAccessibleSinceLastSave).forEach((playerchunk) -> { // Paper
|
||||
ChunkAccess ichunkaccess = (ChunkAccess) playerchunk.getChunkToSave().getNow(null); // CraftBukkit - decompile error
|
||||
|
||||
if (ichunkaccess instanceof ImposterProtoChunk || ichunkaccess instanceof LevelChunk) {
|
||||
@@ -986,7 +993,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);
|
||||
@@ -1121,7 +1128,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;
|
||||
}
|
||||
@@ -1587,7 +1599,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
|
||||
}
|
||||
|
||||
protected DistanceManager getDistanceManager() {
|
||||
@@ -1595,12 +1607,12 @@ 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").build(writer);
|
||||
- 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 4430385a758906239b8573c59b18d19470339ec5..b4cf0d44bda13625cfa8264043a49c1a0daf1054 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);
|
||||
}
|
||||
|
|
@ -1,769 +0,0 @@
|
|||
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 c81392f5b4a6dcef9c1864c1b2c268914b904561..ad4081efec9c7eaf315ddb660f813f6ef3cfbb5b 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
|
||||
@@ -70,6 +70,13 @@ public class ChunkSerializer {
|
||||
private static final String SKYLIGHT_STATE_TAG = "starlight.skylight_state";
|
||||
private static final String STARLIGHT_VERSION_TAG = "starlight.light_version";
|
||||
// Paper end - replace light engine impl
|
||||
+ // Paper start
|
||||
+ // TODO: Check on update
|
||||
+ public static long getLastWorldSaveTime(CompoundTag chunkData) {
|
||||
+ CompoundTag levelData = chunkData.getCompound("Level");
|
||||
+ return levelData.getLong("LastUpdate");
|
||||
+ }
|
||||
+ // Paper end
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger();
|
||||
public static final String TAG_UPGRADE_DATA = "UpgradeData";
|
||||
@@ -124,7 +131,7 @@ public class ChunkSerializer {
|
||||
}
|
||||
// Paper end
|
||||
BiomeSource worldchunkmanager = chunkgenerator.getBiomeSource();
|
||||
- CompoundTag nbttagcompound1 = nbt.getCompound("Level"); // Paper - diff on change, see ChunkSerializer#getChunkCoordinate
|
||||
+ CompoundTag nbttagcompound1 = nbt.getCompound("Level"); // Paper - diff on change, see ChunkSerializer#getChunkCoordinate // Paper - diff on change
|
||||
ChunkPos chunkcoordintpair1 = new ChunkPos(nbttagcompound1.getInt("xPos"), nbttagcompound1.getInt("zPos")); // Paper - diff on change, see ChunkSerializer#getChunkCoordinate
|
||||
|
||||
if (!Objects.equals(pos, chunkcoordintpair1)) {
|
||||
@@ -495,7 +502,7 @@ public class ChunkSerializer {
|
||||
nbttagcompound.put("Level", nbttagcompound1);
|
||||
nbttagcompound1.putInt("xPos", chunkcoordintpair.x);
|
||||
nbttagcompound1.putInt("zPos", chunkcoordintpair.z);
|
||||
- nbttagcompound1.putLong("LastUpdate", asyncsavedata != null ? asyncsavedata.worldTime : world.getGameTime()); // Paper - async chunk unloading
|
||||
+ nbttagcompound1.putLong("LastUpdate", asyncsavedata != null ? asyncsavedata.worldTime : world.getGameTime()); // Paper - async chunk unloading // Paper - diff on change
|
||||
nbttagcompound1.putLong("InhabitedTime", chunk.getInhabitedTime());
|
||||
nbttagcompound1.putString("Status", chunk.getStatus().getName());
|
||||
UpgradeData chunkconverter = chunk.getUpgradeData();
|
||||
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 037bbd562e2f35e17c324cd200c55c5e6cb5d768..b889dbad607b6508fb4987d21d3be691a5b37072 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
|
||||
@@ -34,7 +34,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 c22391a0d4b7db49bd3994b0887939a7d8019391..d1a4f9979f209a1afb2bf4bfa3d70c66338ae27d 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
|
||||
@@ -55,6 +55,355 @@ public class RegionFile implements AutoCloseable {
|
||||
public final java.util.concurrent.locks.ReentrantLock fileLock = new java.util.concurrent.locks.ReentrantLock(true); // Paper
|
||||
public final File 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((java.io.DataInput)new DataInputStream(new BufferedInputStream(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() {
|
||||
+ File backup = new File(this.regionFile.getParent(), this.regionFile.getName() + "." + new java.util.Random().nextLong() + ".backup");
|
||||
+ this.backupRegionFile(backup);
|
||||
+ }
|
||||
+
|
||||
+ private void backupRegionFile(File to) {
|
||||
+ try {
|
||||
+ this.file.force(true);
|
||||
+ LOGGER.warn("Backing up regionfile \"" + this.regionFile.getAbsolutePath() + "\" to " + to.getAbsolutePath());
|
||||
+ java.nio.file.Files.copy(this.regionFile.toPath(), to.toPath());
|
||||
+ LOGGER.warn("Backed up the regionfile to " + to.getAbsolutePath());
|
||||
+ } catch (IOException ex) {
|
||||
+ LOGGER.error("Failed to backup to " + to.getAbsolutePath(), 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.getAbsolutePath() + ", cannot recover header");
|
||||
+ return false;
|
||||
+ }
|
||||
+ synchronized (this) {
|
||||
+ LOGGER.warn("Corrupt regionfile header detected! Attempting to re-calculate header offsets for regionfile " + this.regionFile.getAbsolutePath(), 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.getAbsolutePath() + "'. 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?
|
||||
+ File aikarOversizedFile = this.getOversizedFile(chunkPos.x, chunkPos.z);
|
||||
+ boolean isAikarOversized = false;
|
||||
+ if (aikarOversizedFile.exists()) {
|
||||
+ 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.getAbsolutePath() + ", 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;
|
||||
+ File[] regionFiles = containingFolder.toFile().listFiles();
|
||||
+ 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 (File 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.toPath());
|
||||
+ } catch (Exception ex) {
|
||||
+ LOGGER.error("Failed to read oversized chunk data in file " + regionFile.getAbsolutePath() + ", 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(new BufferedInputStream(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.getAbsolutePath() + ", 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.getAbsolutePath() + ", 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.getAbsolutePath() + ", 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.getAbsolutePath() + " 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.getAbsolutePath(), ex);
|
||||
+ this.getOversizedMetaFile().delete();
|
||||
+ }
|
||||
+ } else {
|
||||
+ this.getOversizedMetaFile().delete();
|
||||
+ }
|
||||
+
|
||||
+ 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.getAbsolutePath());
|
||||
+
|
||||
+ 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.getAbsolutePath());
|
||||
+ } else if (newOffset == 0) {
|
||||
+ LOGGER.warn("Data for local chunk (" + chunkX + "," + chunkZ + ") could not be recovered in regionfile " + this.regionFile.getAbsolutePath() + ", it will be regenerated");
|
||||
+ } else {
|
||||
+ LOGGER.info("Local chunk (" + chunkX + "," + chunkZ + ") changed to point to newer data or correct chunk in regionfile " + this.regionFile.getAbsolutePath());
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ LOGGER.info("End of change summary for regionfile " + this.regionFile.getAbsolutePath());
|
||||
+
|
||||
+ // 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.getAbsolutePath());
|
||||
+ } catch (IOException ex) {
|
||||
+ LOGGER.fatal("Failed to write new header to disk for regionfile " + this.regionFile.getAbsolutePath(), 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];
|
||||
|
||||
@@ -82,8 +431,19 @@ public class RegionFile implements AutoCloseable {
|
||||
public RegionFile(File file, File directory, boolean dsync) throws IOException {
|
||||
this(file.toPath(), directory.toPath(), RegionFileVersion.VERSION_DEFLATE, dsync);
|
||||
}
|
||||
+ // Paper start - add can recalc flag
|
||||
+ public RegionFile(File file, File directory, boolean dsync, boolean canRecalcHeader) throws IOException {
|
||||
+ this(file.toPath(), directory.toPath(), 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.toFile(); // Paper
|
||||
initOversizedState(); // Paper
|
||||
@@ -112,14 +472,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
|
||||
@@ -128,32 +490,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.getAbsolutePath() + "! Recalculating header...");
|
||||
+ needsHeaderRecalc = true;
|
||||
+ break;
|
||||
+ } else {
|
||||
+ // location = chunkX | (chunkZ << 5);
|
||||
+ LOGGER.fatal("Detected invalid header for regionfile " + this.regionFile.getAbsolutePath() +
|
||||
+ "! 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.getAbsolutePath());
|
||||
}
|
||||
+ if (failedToAllocate & !canRecalcHeader) {
|
||||
+ // location = chunkX | (chunkZ << 5);
|
||||
+ LOGGER.fatal("Detected invalid header for regionfile " + this.regionFile.getAbsolutePath() +
|
||||
+ "! 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.getAbsolutePath() + ", 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(File file) {
|
||||
+ String fileName = file.getName();
|
||||
+
|
||||
+ 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);
|
||||
@@ -177,6 +609,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();
|
||||
@@ -184,6 +621,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;
|
||||
@@ -191,17 +633,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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -376,10 +845,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 6496108953effae82391b5c1ea6fdec8482731cd..e360ff62d3be252d6b9746b00dbdb4a2aae95405 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
|
||||
@@ -25,7 +25,15 @@ public class RegionFileStorage implements AutoCloseable {
|
||||
private final File folder;
|
||||
private final boolean sync;
|
||||
|
||||
+ private final boolean isChunkData; // Paper
|
||||
+
|
||||
RegionFileStorage(File directory, boolean dsync) {
|
||||
+ // Paper start - add isChunkData param
|
||||
+ this(directory, dsync, false);
|
||||
+ }
|
||||
+ RegionFileStorage(File directory, boolean dsync, boolean isChunkData) {
|
||||
+ this.isChunkData = isChunkData;
|
||||
+ // Paper end - add isChunkData param
|
||||
this.folder = directory;
|
||||
this.sync = dsync;
|
||||
}
|
||||
@@ -90,9 +98,9 @@ public class RegionFileStorage implements AutoCloseable {
|
||||
|
||||
File file = this.folder;
|
||||
int j = chunkcoordintpair.getRegionX();
|
||||
- File file1 = new File(file, "r." + j + "." + chunkcoordintpair.getRegionZ() + ".mca");
|
||||
+ File file1 = new File(file, "r." + j + "." + chunkcoordintpair.getRegionZ() + ".mca"); // Paper - diff on change
|
||||
if (existingOnly && !file1.exists()) return null; // CraftBukkit
|
||||
- RegionFile regionfile1 = new RegionFile(file1, this.folder, this.sync);
|
||||
+ RegionFile regionfile1 = new RegionFile(file1, this.folder, this.sync, this.isChunkData); // Paper - allow for chunk regionfiles to regen header
|
||||
|
||||
this.regionCache.putAndMoveToFirst(i, regionfile1);
|
||||
// Paper start
|
||||
@@ -180,6 +188,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);
|
||||
@@ -196,6 +211,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)) {
|
||||
+ MinecraftServer.LOGGER.error("Attempting to read chunk data at " + pos.toString() + " but got chunk data for " + chunkPos.toString() + " instead! Attempting regionfile recalculation for regionfile " + regionfile.regionFile.getAbsolutePath());
|
||||
+ if (regionfile.recalculateHeader()) {
|
||||
+ regionfile.fileLock.lock(); // otherwise we will unlock twice and only lock once.
|
||||
+ return this.read(pos, regionfile);
|
||||
+ }
|
||||
+ MinecraftServer.LOGGER.fatal("Can't recalculate regionfile header, regenerating chunk " + pos.toString() + " for " + regionfile.regionFile.getAbsolutePath());
|
||||
+ 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 b7835b9b904e7d4bff64f7189049e334f5ab4d6f..492bba91e0e61c678e5067a6f855674d42d7f4ea 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
|
||||
@@ -12,7 +12,7 @@ import java.util.zip.InflaterInputStream;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
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, GZIPInputStream::new, GZIPOutputStream::new));
|
||||
public static final RegionFileVersion VERSION_DEFLATE = register(new RegionFileVersion(2, InflaterInputStream::new, DeflaterOutputStream::new));
|
||||
public static final RegionFileVersion VERSION_NONE = register(new RegionFileVersion(3, (inputStream) -> {
|
Loading…
Add table
Add a link
Reference in a new issue