mirror of
https://github.com/PaperMC/Paper.git
synced 2024-11-05 10:20:53 +01:00
252 lines
11 KiB
Diff
252 lines
11 KiB
Diff
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
|
From: Aikar <aikar@aikar.co>
|
|
Date: Fri, 15 Feb 2019 01:08:19 -0500
|
|
Subject: [PATCH] Allow Saving of Oversized Chunks
|
|
|
|
Note 1.17 update: With 1.17, Entities are no longer stored in chunk slices, so this needs updating!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
|
|
The Minecraft World Region File format has a hard cap of 1MB per chunk.
|
|
This is due to the fact that the header of the file format only allocates
|
|
a single byte for sector count, meaning a maximum of 256 sectors, at 4k per sector.
|
|
|
|
This limit can be reached fairly easily with books, resulting in the chunk being unable
|
|
to save to the world. Worse off, is that nothing printed when this occured, and silently
|
|
performed a chunk rollback on next load.
|
|
|
|
This leads to security risk with duplication and is being actively exploited.
|
|
|
|
This patch catches the too large scenario, falls back and moves any large Entity
|
|
or Tile Entity into a new compound, and this compound is saved into a different file.
|
|
|
|
On Chunk Load, we check for oversized status, and if so, we load the extra file and
|
|
merge the Entities and Tile Entities from the oversized chunk back into the level to
|
|
then be loaded as normal.
|
|
|
|
Once a chunk is returned back to normal size, the oversized flag will clear, and no
|
|
extra data file will exist.
|
|
|
|
This fix maintains compatability with all existing Anvil Region Format tools as it
|
|
does not alter the save format. They will just not know about the extra entities.
|
|
|
|
This fix also maintains compatability if someone switches server jars to one without
|
|
this fix, as the data will remain in the oversized file. Once the server returns
|
|
to a jar with this fix, the data will be restored.
|
|
|
|
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 ddcc212ba83d9365adb842b3d3ced64e3d7dd155..584985272a02eb5b61a22cf2404fbd97a55a3358 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
|
|
@@ -18,8 +18,12 @@ import java.nio.file.LinkOption;
|
|
import java.nio.file.Path;
|
|
import java.nio.file.StandardCopyOption;
|
|
import java.nio.file.StandardOpenOption;
|
|
+import java.util.zip.InflaterInputStream; // Paper
|
|
+
|
|
import javax.annotation.Nullable;
|
|
import net.minecraft.Util;
|
|
+import net.minecraft.nbt.CompoundTag;
|
|
+import net.minecraft.nbt.NbtIo;
|
|
import net.minecraft.world.level.ChunkPos;
|
|
import org.slf4j.Logger;
|
|
|
|
@@ -45,6 +49,7 @@ public class RegionFile implements AutoCloseable {
|
|
@VisibleForTesting
|
|
protected final RegionBitmap usedSectors;
|
|
public final java.util.concurrent.locks.ReentrantLock fileLock = new java.util.concurrent.locks.ReentrantLock(true); // Paper
|
|
+ public final Path regionFile; // Paper
|
|
|
|
public RegionFile(Path file, Path directory, boolean dsync) throws IOException {
|
|
this(file, directory, RegionFileVersion.VERSION_DEFLATE, dsync);
|
|
@@ -52,6 +57,8 @@ public class RegionFile implements AutoCloseable {
|
|
|
|
public RegionFile(Path file, Path directory, RegionFileVersion outputChunkStreamVersion, boolean dsync) throws IOException {
|
|
this.header = ByteBuffer.allocateDirect(8192);
|
|
+ this.regionFile = file; // Paper
|
|
+ initOversizedState(); // Paper
|
|
this.usedSectors = new RegionBitmap();
|
|
this.version = outputChunkStreamVersion;
|
|
if (!Files.isDirectory(directory, new LinkOption[0])) {
|
|
@@ -430,6 +437,74 @@ public class RegionFile implements AutoCloseable {
|
|
|
|
}
|
|
|
|
+ // Paper start
|
|
+ private final byte[] oversized = new byte[1024];
|
|
+ private int oversizedCount = 0;
|
|
+
|
|
+ private synchronized void initOversizedState() throws IOException {
|
|
+ Path metaFile = getOversizedMetaFile();
|
|
+ if (Files.exists(metaFile)) {
|
|
+ final byte[] read = java.nio.file.Files.readAllBytes(metaFile);
|
|
+ System.arraycopy(read, 0, oversized, 0, oversized.length);
|
|
+ for (byte temp : oversized) {
|
|
+ oversizedCount += temp;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private static int getChunkIndex(int x, int z) {
|
|
+ return (x & 31) + (z & 31) * 32;
|
|
+ }
|
|
+ synchronized boolean isOversized(int x, int z) {
|
|
+ return this.oversized[getChunkIndex(x, z)] == 1;
|
|
+ }
|
|
+ synchronized void setOversized(int x, int z, boolean oversized) throws IOException {
|
|
+ final int offset = getChunkIndex(x, z);
|
|
+ boolean previous = this.oversized[offset] == 1;
|
|
+ this.oversized[offset] = (byte) (oversized ? 1 : 0);
|
|
+ if (!previous && oversized) {
|
|
+ oversizedCount++;
|
|
+ } else if (!oversized && previous) {
|
|
+ oversizedCount--;
|
|
+ }
|
|
+ if (previous && !oversized) {
|
|
+ Path oversizedFile = getOversizedFile(x, z);
|
|
+ if (Files.exists(oversizedFile)) {
|
|
+ Files.delete(oversizedFile);
|
|
+ }
|
|
+ }
|
|
+ if (oversizedCount > 0) {
|
|
+ if (previous != oversized) {
|
|
+ writeOversizedMeta();
|
|
+ }
|
|
+ } else if (previous) {
|
|
+ Path oversizedMetaFile = getOversizedMetaFile();
|
|
+ if (Files.exists(oversizedMetaFile)) {
|
|
+ Files.delete(oversizedMetaFile);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private void writeOversizedMeta() throws IOException {
|
|
+ java.nio.file.Files.write(getOversizedMetaFile(), oversized);
|
|
+ }
|
|
+
|
|
+ private Path getOversizedMetaFile() {
|
|
+ return this.regionFile.getParent().resolve(this.regionFile.getFileName().toString().replaceAll("\\.mca$", "") + ".oversized.nbt");
|
|
+ }
|
|
+
|
|
+ private Path getOversizedFile(int x, int z) {
|
|
+ return this.regionFile.getParent().resolve(this.regionFile.getFileName().toString().replaceAll("\\.mca$", "") + "_oversized_" + x + "_" + z + ".nbt");
|
|
+ }
|
|
+
|
|
+ synchronized CompoundTag getOversizedData(int x, int z) throws IOException {
|
|
+ Path file = getOversizedFile(x, z);
|
|
+ try (DataInputStream out = new DataInputStream(new java.io.BufferedInputStream(new InflaterInputStream(Files.newInputStream(file))))) {
|
|
+ return NbtIo.read((java.io.DataInput) out);
|
|
+ }
|
|
+
|
|
+ }
|
|
+ // Paper end
|
|
private class ChunkBuffer extends ByteArrayOutputStream {
|
|
|
|
private final ChunkPos pos;
|
|
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 4c1b1ac36ef09a9cc8da6e8ee71f82d4a67478df..96f129cb13642dc9667464b58c025fa0ed700cfd 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
|
|
@@ -11,8 +11,10 @@ import java.nio.file.Path;
|
|
import javax.annotation.Nullable;
|
|
import net.minecraft.FileUtil;
|
|
import net.minecraft.nbt.CompoundTag;
|
|
+import net.minecraft.nbt.ListTag;
|
|
import net.minecraft.nbt.NbtIo;
|
|
import net.minecraft.nbt.StreamTagVisitor;
|
|
+import net.minecraft.nbt.Tag;
|
|
import net.minecraft.util.ExceptionCollector;
|
|
import net.minecraft.world.level.ChunkPos;
|
|
|
|
@@ -120,6 +122,71 @@ public class RegionFileStorage implements AutoCloseable {
|
|
}
|
|
}
|
|
|
|
+ // Paper start
|
|
+ private static void printOversizedLog(String msg, Path file, int x, int z) {
|
|
+ org.apache.logging.log4j.LogManager.getLogger().fatal(msg + " (" + file.toString().replaceAll(".+[\\\\/]", "") + " - " + x + "," + z + ") Go clean it up to remove this message. /minecraft:tp " + (x<<4)+" 128 "+(z<<4) + " - DO NOT REPORT THIS TO PAPER - You may ask for help on Discord, but do not file an issue. These error messages can not be removed.");
|
|
+ }
|
|
+
|
|
+ private static final int DEFAULT_SIZE_THRESHOLD = 1024 * 8;
|
|
+ private static final int OVERZEALOUS_TOTAL_THRESHOLD = 1024 * 64;
|
|
+ private static final int OVERZEALOUS_THRESHOLD = 1024;
|
|
+ private static int SIZE_THRESHOLD = DEFAULT_SIZE_THRESHOLD;
|
|
+ private static void resetFilterThresholds() {
|
|
+ SIZE_THRESHOLD = Math.max(1024 * 4, Integer.getInteger("Paper.FilterThreshhold", DEFAULT_SIZE_THRESHOLD));
|
|
+ }
|
|
+ static {
|
|
+ resetFilterThresholds();
|
|
+ }
|
|
+
|
|
+ static boolean isOverzealous() {
|
|
+ return SIZE_THRESHOLD == OVERZEALOUS_THRESHOLD;
|
|
+ }
|
|
+
|
|
+
|
|
+ private static CompoundTag readOversizedChunk(RegionFile regionfile, ChunkPos chunkCoordinate) throws IOException {
|
|
+ synchronized (regionfile) {
|
|
+ try (DataInputStream datainputstream = regionfile.getChunkDataInputStream(chunkCoordinate)) {
|
|
+ CompoundTag oversizedData = regionfile.getOversizedData(chunkCoordinate.x, chunkCoordinate.z);
|
|
+ CompoundTag chunk = NbtIo.read((DataInput) datainputstream);
|
|
+ if (oversizedData == null) {
|
|
+ return chunk;
|
|
+ }
|
|
+ CompoundTag oversizedLevel = oversizedData.getCompound("Level");
|
|
+
|
|
+ mergeChunkList(chunk.getCompound("Level"), oversizedLevel, "Entities", "Entities");
|
|
+ mergeChunkList(chunk.getCompound("Level"), oversizedLevel, "TileEntities", "TileEntities");
|
|
+
|
|
+ return chunk;
|
|
+ } catch (Throwable throwable) {
|
|
+ throwable.printStackTrace();
|
|
+ throw throwable;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private static void mergeChunkList(CompoundTag level, CompoundTag oversizedLevel, String key, String oversizedKey) {
|
|
+ ListTag levelList = level.getList(key, 10);
|
|
+ ListTag oversizedList = oversizedLevel.getList(oversizedKey, 10);
|
|
+
|
|
+ if (!oversizedList.isEmpty()) {
|
|
+ levelList.addAll(oversizedList);
|
|
+ level.put(key, levelList);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private static int getNBTSize(Tag nbtBase) {
|
|
+ DataOutputStream test = new DataOutputStream(new org.apache.commons.io.output.NullOutputStream());
|
|
+ try {
|
|
+ nbtBase.write(test);
|
|
+ return test.size();
|
|
+ } catch (IOException e) {
|
|
+ e.printStackTrace();
|
|
+ return 0;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // Paper End
|
|
+
|
|
@Nullable
|
|
public CompoundTag read(ChunkPos pos) throws IOException {
|
|
// CraftBukkit start - SPIGOT-5680: There's no good reason to preemptively create files on read, save that for writing
|
|
@@ -131,6 +198,12 @@ public class RegionFileStorage implements AutoCloseable {
|
|
try { // Paper
|
|
DataInputStream datainputstream = regionfile.getChunkDataInputStream(pos);
|
|
|
|
+ // Paper start
|
|
+ if (regionfile.isOversized(pos.x, pos.z)) {
|
|
+ printOversizedLog("Loading Oversized Chunk!", regionfile.regionFile, pos.x, pos.z);
|
|
+ return readOversizedChunk(regionfile, pos);
|
|
+ }
|
|
+ // Paper end
|
|
CompoundTag nbttagcompound;
|
|
label43:
|
|
{
|
|
@@ -217,6 +290,7 @@ public class RegionFileStorage implements AutoCloseable {
|
|
|
|
try {
|
|
NbtIo.write(nbt, (DataOutput) dataoutputstream);
|
|
+ regionfile.setOversized(pos.x, pos.z, false); // Paper - We don't do this anymore, mojang stores differently, but clear old meta flag if it exists to get rid of our own meta file once last oversized is gone
|
|
} catch (Throwable throwable) {
|
|
if (dataoutputstream != null) {
|
|
try {
|