diff --git a/sources/src/main/java/net/minecraft/server/RegionFile.java b/sources/src/main/java/net/minecraft/server/RegionFile.java new file mode 100644 index 000000000..37b46167e --- /dev/null +++ b/sources/src/main/java/net/minecraft/server/RegionFile.java @@ -0,0 +1,506 @@ +package net.minecraft.server; + +import com.destroystokyo.paper.exception.ServerInternalException; +import com.google.common.collect.Lists; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.Buffer; +import java.nio.IntBuffer; +import java.util.List; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.GZIPInputStream; +import java.util.zip.InflaterInputStream; +import javax.annotation.Nullable; + +public class RegionFile { + + // Spigot start + // Minecraft is limited to 256 sections per chunk. So 1MB. This can easily be overriden. + // So we extend this to use the REAL size when the count is maxed by seeking to that section and reading the length. + private static final boolean ENABLE_EXTENDED_SAVE = Boolean.parseBoolean(System.getProperty("net.minecraft.server.RegionFile.enableExtendedSave", "true")); + // Spigot end + private static final byte[] a = new byte[4096]; + private final File b;private File getFile() { return b; } // Paper - OBFHELPER + private RandomAccessFile c;private RandomAccessFile getDataFile() { return c; } // Paper - OBFHELPER + private final int[] d = new int[1024];private int[] offsets = d; // Paper - OBFHELPER + private final int[] e = new int[1024];private int[] timestamps = e; // Paper - OBFHELPER + private List f; + private int g; + private long h; + + public RegionFile(File file) { + this.b = file; + this.g = 0; + + try { + if (file.exists()) { + this.h = file.lastModified(); + } + + + this.c = new RandomAccessFile(file, "rw"); + if (this.c.length() < 8192L) { // Paper - headers should be 8192 + this.c.write(a); + this.c.write(a); + this.g += 8192; + } + + int i; + + if ((this.c.length() & 4095L) != 0L) { + for (i = 0; (long) i < (this.c.length() & 4095L); ++i) { + this.c.write(0); + } + } + + i = (int) this.c.length() / 4096; + this.f = Lists.newArrayListWithCapacity(i); + + int j; + + for (j = 0; j < i; ++j) { + this.f.add(Boolean.valueOf(true)); + } + + this.f.set(0, Boolean.valueOf(false)); + this.f.set(1, Boolean.valueOf(false)); + this.c.seek(0L); + + int k; + + // Paper Start + ByteBuffer header = ByteBuffer.allocate(8192); + while (header.hasRemaining()) { + if (this.c.getChannel().read(header) == -1) throw new EOFException(); + } + ((Buffer) header).clear(); + IntBuffer headerAsInts = header.asIntBuffer(); + initOversizedState(); + // Paper End + for (j = 0; j < 1024; ++j) { + k = headerAsInts.get(); // Paper + this.d[j] = k; + // Spigot start + int length = k & 255; + if (length == 255) { + if ((k >> 8) <= this.f.size()) { + // We're maxed out, so we need to read the proper length from the section + this.c.seek((k >> 8) * 4096); + length = (this.c.readInt() + 4) / 4096 + 1; + this.c.seek(j * 4 + 4); // Go back to where we were + } + } + if (k > 0 && (k >> 8) > 1 && (k >> 8) + (length) <= this.f.size()) { // Paper >= 1 as 0/1 are the headers, and negative isnt valid + for (int l = 0; l < (length); ++l) { + // Spigot end + this.f.set((k >> 8) + l, Boolean.valueOf(false)); + } + } + // Spigot start + else if (k != 0) { // Paper + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.WARNING, "Invalid chunk: ({0}, {1}) Offset: {2} Length: {3} runs off end file. {4}", new Object[]{j % 32, (int) (j / 32), k >> 8, length, file}); + deleteChunk(j); // Paper + } + // Spigot end + } + + for (j = 0; j < 1024; ++j) { + k = headerAsInts.get(); // Paper + if (offsets[j] != 0) this.timestamps[j] = k; // Paper - don't set timestamp if it got 0'd above due to corruption + } + } catch (IOException ioexception) { + ioexception.printStackTrace(); + ServerInternalException.reportInternalException(ioexception); // Paper + } + + } + + @Nullable + public synchronized DataInputStream getReadStream(int i, int j) { return a(i, j); } @Nullable public synchronized DataInputStream a(int i, int j) { // Paper - OBFHELPER + if (this.d(i, j)) { + return null; + } else { + try { + int k = this.getOffset(i, j); + + if (k == 0) { + return null; + } else { + int l = k >> 8; + int i1 = k & 255; + // Spigot start + if (i1 == 255) { + this.c.seek(l * 4096); + i1 = (this.c.readInt() + 4) / 4096 + 1; + } + // Spigot end + + if (l + i1 > this.f.size()) { + return null; + } else { + this.c.seek((long) (l * 4096)); + int j1 = this.c.readInt(); + + if (j1 > 4096 * i1) { + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.WARNING, "Invalid chunk: ({0}, {1}) Offset: {2} Invalid Size: {3}>{4} {5}", new Object[]{i, j, l, j1, i1 * 4096, this.b}); // Spigot + return null; + } else if (j1 <= 0) { + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.WARNING, "Invalid chunk: ({0}, {1}) Offset: {2} Invalid Size: {3} {4}", new Object[]{i, j, l, j1, this.b}); // Spigot + return null; + } else { + byte b0 = this.c.readByte(); + byte[] abyte; + + if (b0 == 1) { + abyte = new byte[j1 - 1]; + this.c.read(abyte); + return new DataInputStream(new BufferedInputStream(new GZIPInputStream(new ByteArrayInputStream(abyte)))); + } else if (b0 == 2) { + abyte = new byte[j1 - 1]; + this.c.read(abyte); + return new DataInputStream(new BufferedInputStream(new InflaterInputStream(new ByteArrayInputStream(abyte)))); + } else { + return null; + } + } + } + } + } catch (IOException ioexception) { + return null; + } + } + } + + @Nullable + public DataOutputStream getWriteStream(int i, int j) { return b(i, j); } @Nullable public DataOutputStream b(int i, int j) { // Paper - OBFHELPER + return this.d(i, j) ? null : new DataOutputStream(new RegionFile.ChunkBuffer(i, j)); // Paper - remove middleware, move deflate to .close() for dynamic levels + } + + protected synchronized void a(int i, int j, byte[] abyte, int k) { + try { + int l = this.getOffset(i, j); + int i1 = l >> 8; + int j1 = l & 255; + // Spigot start + if (j1 == 255) { + this.c.seek(i1 * 4096); + j1 = (this.c.readInt() + 4) / 4096 + 1; + } + // Spigot end + int k1 = (k + 5) / 4096 + 1; + + if (k1 >= 256) { + // Spigot start + if (!USE_SPIGOT_OVERSIZED_METHOD && !RegionFileCache.isOverzealous()) throw new ChunkTooLargeException(i, j, k1); // Paper - throw error instead + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.WARNING,"Large Chunk Detected: ({0}, {1}) Size: {2} {3}", new Object[]{i, j, k1, this.b}); + if (!ENABLE_EXTENDED_SAVE) return; + // Spigot end + } + + if (i1 != 0 && j1 == k1) { + this.a(i1, abyte, k); + } else { + int l1; + + for (l1 = 0; l1 < j1; ++l1) { + this.f.set(i1 + l1, Boolean.valueOf(true)); + } + + l1 = this.f.indexOf(Boolean.valueOf(true)); + int i2 = 0; + int j2; + + if (l1 != -1) { + for (j2 = l1; j2 < this.f.size(); ++j2) { + if (i2 != 0) { + if (((Boolean) this.f.get(j2)).booleanValue()) { + ++i2; + } else { + i2 = 0; + } + } else if (((Boolean) this.f.get(j2)).booleanValue()) { + l1 = j2; + i2 = 1; + } + + if (i2 >= k1) { + break; + } + } + } + + if (i2 >= k1) { + i1 = l1; + this.a(i, j, l1 << 8 | (k1 > 255 ? 255 : k1)); // Spigot + + for (j2 = 0; j2 < k1; ++j2) { + this.f.set(i1 + j2, Boolean.valueOf(false)); + } + + this.a(i1, abyte, k); + } else { + this.c.seek(this.c.length()); + i1 = this.f.size(); + + for (j2 = 0; j2 < k1; ++j2) { + this.c.write(RegionFile.a); + this.f.add(Boolean.valueOf(false)); + } + + this.g += 4096 * k1; + this.a(i1, abyte, k); + this.a(i, j, i1 << 8 | (k1 > 255 ? 255 : k1)); // Spigot + } + } + + this.b(i, j, (int) (MinecraftServer.aw() / 1000L)); + } catch (IOException ioexception) { + org.spigotmc.SneakyThrow.sneaky(ioexception); // Paper - we want the upper try/catch to retry this + } + + } + + private void a(int i, byte[] abyte, int j) throws IOException { + this.c.seek((long) (i * 4096)); + this.c.writeInt(j + 1); + this.c.writeByte(2); + this.c.write(abyte, 0, j); + } + + private boolean d(int i, int j) { + return i < 0 || i >= 32 || j < 0 || j >= 32; + } + + private synchronized int getOffset(int i, int j) { + return this.d[i + j * 32]; + } + + public boolean c(int i, int j) { + return this.getOffset(i, j) != 0; + } + + private void a(int i, int j, int k) throws IOException { + this.d[i + j * 32] = k; + this.c.seek((long) ((i + j * 32) * 4)); + this.c.writeInt(k); + } + + private void b(int i, int j, int k) throws IOException { + this.e[i + j * 32] = k; + this.c.seek((long) (4096 + (i + j * 32) * 4)); + this.c.writeInt(k); + } + + public void c() throws IOException { + if (this.c != null) { + this.c.close(); + } + + } + + // Paper start + public synchronized void deleteChunk(int j1) { + backup(); + int k = offsets[j1]; + int x = j1 & 1024; + int z = j1 >> 2; + int offset = (k >> 8); + int len = (k & 255); + org.apache.logging.log4j.Logger logger = org.apache.logging.log4j.LogManager.getLogger(); + String debug = "idx:" + + j1 + " - " + x + "," + z + " - offset: " + offset + " - len: " + len; + try { + RandomAccessFile file = getDataFile(); + file.seek(j1 * 4); + file.writeInt(0); + // clear the timestamp + file.seek(4096 + j1 * 4); + file.writeInt(0); + timestamps[j1] = 0; + offsets[j1] = 0; + logger.error("Deleted corrupt chunk (" + debug + ") " + getFile().getAbsolutePath(), e); + } catch (IOException e) { + + logger.error("Error deleting corrupt chunk (" + debug + ") " + getFile().getAbsolutePath(), e); + } + } + private boolean backedUp = false; + private synchronized void backup() { + if (backedUp) { + return; + } + backedUp = true; + File file = this.getFile(); + java.text.DateFormat formatter = new java.text.SimpleDateFormat("yyyy-MM-dd"); + java.util.Date today = new java.util.Date(); + File corrupt = new File(file.getParentFile(), file.getName() + "." + formatter.format(today) + ".corrupt"); + if (corrupt.exists()) { + return; + } + org.apache.logging.log4j.Logger logger = org.apache.logging.log4j.LogManager.getLogger(); + logger.error("Region file " + file.getAbsolutePath() + " was corrupt. Backing up to " + corrupt.getAbsolutePath() + " and repairing"); + try { + java.nio.file.Files.copy(file.toPath(), corrupt.toPath()); + + } catch (IOException e) { + logger.error("Error backing up corrupt file" + file.getAbsolutePath(), e); + } + } + + private final byte[] oversized = new byte[1024]; + private int oversizedCount = 0; + + private synchronized void initOversizedState() throws IOException { + File metaFile = getOversizedMetaFile(); + if (metaFile.exists()) { + final byte[] read = java.nio.file.Files.readAllBytes(metaFile.toPath()); + 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) { + File oversizedFile = getOversizedFile(x, z); + if (oversizedFile.exists()) { + oversizedFile.delete(); + } + } + if (oversizedCount > 0) { + if (previous != oversized) { + writeOversizedMeta(); + } + } else if (previous) { + File oversizedMetaFile = getOversizedMetaFile(); + if (oversizedMetaFile.exists()) { + oversizedMetaFile.delete(); + } + } + } + + private void writeOversizedMeta() throws IOException { + java.nio.file.Files.write(getOversizedMetaFile().toPath(), oversized); + } + + private File getOversizedMetaFile() { + return new File(getFile().getParentFile(), getFile().getName().replaceAll("\\.mca$", "") + ".oversized.nbt"); + } + + private File getOversizedFile(int x, int z) { + return new File(this.getFile().getParentFile(), this.getFile().getName().replaceAll("\\.mca$", "") + "_oversized_" + x + "_" + z + ".nbt"); + } + + void writeOversizedData(int x, int z, NBTTagCompound oversizedData) throws IOException { + File file = getOversizedFile(x, z); + try (DataOutputStream out = new DataOutputStream(new BufferedOutputStream(new DeflaterOutputStream(new java.io.FileOutputStream(file), new java.util.zip.Deflater(java.util.zip.Deflater.BEST_COMPRESSION), 32 * 1024), 32 * 1024))) { + NBTCompressedStreamTools.writeNBT(oversizedData, out); + } + this.setOversized(x, z, true); + + } + + synchronized NBTTagCompound getOversizedData(int x, int z) throws IOException { + File file = getOversizedFile(x, z); + try (DataInputStream out = new DataInputStream(new BufferedInputStream(new InflaterInputStream(new java.io.FileInputStream(file))))) { + return NBTCompressedStreamTools.readNBT(out); + } + + } + + private static final boolean USE_SPIGOT_OVERSIZED_METHOD = Boolean.getBoolean("Paper.useSpigotExtendedSaveMethod"); // Paper + static { + if (USE_SPIGOT_OVERSIZED_METHOD) { + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.SEVERE, "===================================="); + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.SEVERE, "Using Spigot Oversized Chunk save method. Warning this will result in extremely fragmented chunks, as well as making the entire region file unable to be to used in any other software but Forge or Spigot (not usable in Vanilla or CraftBukkit). Paper's method is highly recommended."); + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.SEVERE, "===================================="); + } + } + + public class ChunkTooLargeException extends RuntimeException { + public ChunkTooLargeException(int x, int z, int sectors) { + super("Chunk " + x + "," + z + " of " + getFile().toString() + " is too large (" + sectors + "/256)"); + } + } + private static class DirectByteArrayOutputStream extends ByteArrayOutputStream { + public DirectByteArrayOutputStream() { + super(); + } + + public DirectByteArrayOutputStream(int size) { + super(size); + } + + public byte[] getBuffer() { + return this.buf; + } + } + // Paper end + + class ChunkBuffer extends ByteArrayOutputStream { + + private final int b; + private final int c; + + public ChunkBuffer(int i, int j) { + super(8096); + this.b = i; + this.c = j; + } + + public void close() throws IOException { + // Paper start - apply dynamic compression + int origLength = this.count; + byte[] buf = this.buf; + DirectByteArrayOutputStream out = compressData(buf, origLength); + byte[] bytes = out.getBuffer(); + int length = out.size(); + + RegionFile.this.a(this.b, this.c, bytes, length); // Paper - change to bytes/length + } + } + + private static final byte[] compressionBuffer = new byte[1024 * 64]; // 64k fits most standard chunks input size even, ideally 1 pass through zlib + private static final java.util.zip.Deflater deflater = new java.util.zip.Deflater(); + // since file IO is single threaded, no benefit to using per-region file buffers/synchronization, we can change that later if it becomes viable. + private static DirectByteArrayOutputStream compressData(byte[] buf, int length) throws IOException { + synchronized (deflater) { + deflater.setInput(buf, 0, length); + deflater.finish(); + + + DirectByteArrayOutputStream out = new DirectByteArrayOutputStream(length); + while (!deflater.finished()) { + out.write(compressionBuffer, 0, deflater.deflate(compressionBuffer)); + } + out.close(); + deflater.reset(); + return out; + } + } + // Paper end + +}