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/server/ChunkRegionLoader.java b/src/main/java/net/minecraft/server/ChunkRegionLoader.java
index f51bf71c8d6eef3c054ac64765709794fcfad5ee..17a5b5b5bab4412242a18224b366674c25cdf286 100644
--- a/src/main/java/net/minecraft/server/ChunkRegionLoader.java
+++ b/src/main/java/net/minecraft/server/ChunkRegionLoader.java
@@ -24,6 +24,14 @@ public class ChunkRegionLoader {
 
     private static final Logger LOGGER = LogManager.getLogger();
 
+    // Tuinity start
+    // TODO: Check on update
+    public static long getLastWorldSaveTime(NBTTagCompound chunkData) {
+        NBTTagCompound levelData = chunkData.getCompound("Level");
+        return levelData.getLong("LastUpdate");
+    }
+    // Tuinity end
+
     // Paper start - guard against serializing mismatching coordinates
     // TODO Note: This needs to be re-checked each update
     public static ChunkCoordIntPair getChunkCoordinate(NBTTagCompound chunkData) {
@@ -401,10 +409,10 @@ public class ChunkRegionLoader {
         NBTTagCompound nbttagcompound1 = new NBTTagCompound();
 
         nbttagcompound.setInt("DataVersion", SharedConstants.getGameVersion().getWorldVersion());
-        nbttagcompound.set("Level", nbttagcompound1);
+        nbttagcompound.set("Level", nbttagcompound1); // Tuinity - diff on change
         nbttagcompound1.setInt("xPos", chunkcoordintpair.x);
         nbttagcompound1.setInt("zPos", chunkcoordintpair.z);
-        nbttagcompound1.setLong("LastUpdate", asyncsavedata != null ? asyncsavedata.worldTime : worldserver.getTime()); // Paper - async chunk unloading
+        nbttagcompound1.setLong("LastUpdate", asyncsavedata != null ? asyncsavedata.worldTime : worldserver.getTime()); // Paper - async chunk unloading // Tuinity - diff on change
         nbttagcompound1.setLong("InhabitedTime", ichunkaccess.getInhabitedTime());
         nbttagcompound1.setString("Status", ichunkaccess.getChunkStatus().d());
         ChunkConverter chunkconverter = ichunkaccess.p();
diff --git a/src/main/java/net/minecraft/server/IChunkLoader.java b/src/main/java/net/minecraft/server/IChunkLoader.java
index 582a5695bac7d078e3022b8ee70c512c0680d992..5601088cd5024a40e8296bab979f43de924c2b62 100644
--- a/src/main/java/net/minecraft/server/IChunkLoader.java
+++ b/src/main/java/net/minecraft/server/IChunkLoader.java
@@ -21,7 +21,7 @@ public class IChunkLoader implements AutoCloseable {
     protected final RegionFileCache regionFileCache;
 
     public IChunkLoader(File file, DataFixer datafixer, boolean flag) {
-        this.regionFileCache = new RegionFileCache(file, flag); // Paper - nuke IOWorker
+        this.regionFileCache = new RegionFileCache(file, flag, true); // Paper - nuke IOWorker // Tuinity
         this.b = datafixer;
         // Paper - nuke IOWorker
     }
diff --git a/src/main/java/net/minecraft/server/RegionFile.java b/src/main/java/net/minecraft/server/RegionFile.java
index 1751fb6934d9242e475c1a44b2a4a1ade6987766..1ffa213a819f9d39488ca3599f77e771de8081a5 100644
--- a/src/main/java/net/minecraft/server/RegionFile.java
+++ b/src/main/java/net/minecraft/server/RegionFile.java
@@ -5,6 +5,7 @@ import java.io.BufferedInputStream;
 import java.io.BufferedOutputStream;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
+import java.io.DataInput;
 import java.io.DataInputStream;
 import java.io.DataOutputStream;
 import java.io.File;
@@ -29,15 +30,350 @@ public class RegionFile implements AutoCloseable {
     private static final Logger LOGGER = LogManager.getLogger();
     private static final ByteBuffer c = ByteBuffer.allocateDirect(1);
     private final FileChannel dataFile;
-    private final java.nio.file.Path e;
-    private final RegionFileCompression f;
+    private final java.nio.file.Path e; private final java.nio.file.Path getContainingDataFolder() { return this.e; } // Tuinity - OBFHELPER
+    private final RegionFileCompression f; private final RegionFileCompression getRegionFileCompression() { return this.f; } // Tuinity - OBFHELPER
     private final ByteBuffer g;
-    private final IntBuffer h;
-    private final IntBuffer i;
+    private final IntBuffer h; private final IntBuffer getOffsets() { return this.h; } // Tuinity - OBFHELPER
+    private final IntBuffer i; private final IntBuffer getTimestamps() { return this.i; } // Tuinity - OBFHELPER
     @VisibleForTesting
     protected final RegionFileBitSet freeSectors;
     public final File file; // Paper
 
+    // Tuinity 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 NBTTagCompound OVERSIZED_COMPOUND = new NBTTagCompound();
+
+    private NBTTagCompound 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.dataFile.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;
+            }
+
+            RegionFileCompression compression = RegionFileCompression.getByType(compressionType);
+            if (compression == null) {
+                return null;
+            }
+
+            InputStream input = compression.wrap(new ByteArrayInputStream(chunkData.array(), chunkData.position(), chunkDataLength - chunkData.position()));
+
+            return NBTCompressedStreamTools.readNBT((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.dataFile.read(length, sector * 4096L)) {
+            return -1;
+        }
+
+        return length.getInt(0);
+    }
+
+    private void backupRegionFile() {
+        File backup = new File(this.file.getParent(), this.file.getName() + "." + new java.util.Random().nextLong() + ".backup");
+        this.backupRegionFile(backup);
+    }
+
+    private void backupRegionFile(File to) {
+        try {
+            this.dataFile.force(true);
+            MinecraftServer.LOGGER.warn("Backing up regionfile \"" + this.file.getAbsolutePath() + "\" to " + to.getAbsolutePath());
+            java.nio.file.Files.copy(this.file.toPath(), to.toPath());
+            MinecraftServer.LOGGER.warn("Backed up the regionfile to " + to.getAbsolutePath());
+        } catch (IOException ex) {
+            MinecraftServer.LOGGER.error("Failed to backup to " + to.getAbsolutePath(), ex);
+        }
+    }
+
+    // note: only call for CHUNK regionfiles
+    void recalculateHeader() throws IOException {
+        if (!this.canRecalcHeader) {
+            return;
+        }
+        synchronized (this) {
+            MinecraftServer.LOGGER.warn("Corrupt regionfile header detected! Attempting to re-calculate header offsets for regionfile " + this.file.getAbsolutePath(), new Throwable());
+
+            // try to backup file so maybe it could be sent to us for further investigation
+
+            this.backupRegionFile();
+            NBTTagCompound[] compounds = new NBTTagCompound[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.dataFile.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);
+                NBTTagCompound compound = this.attemptRead(i, chunkDataLength, fileLength);
+                if (compound == null || compound == OVERSIZED_COMPOUND) {
+                    continue;
+                }
+
+                ChunkCoordIntPair chunkPos = ChunkRegionLoader.getChunkCoordinate(compound);
+                int location = (chunkPos.x & 31) | ((chunkPos.z & 31) << 5);
+
+                NBTTagCompound otherCompound = compounds[location];
+
+                if (otherCompound != null && ChunkRegionLoader.getLastWorldSaveTime(otherCompound) > ChunkRegionLoader.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 {
+                        NBTTagCompound aikarOversizedCompound = this.getOversizedData(chunkPos.x, chunkPos.z);
+                        if (ChunkRegionLoader.getLastWorldSaveTime(compound) == ChunkRegionLoader.getLastWorldSaveTime(aikarOversizedCompound)) {
+                            // best we got for an id. hope it's good enough
+                            isAikarOversized = true;
+                        }
+                    } catch (Exception ex) {
+                        MinecraftServer.LOGGER.error("Failed to read aikar oversized data for absolute chunk (" + chunkPos.x + "," + chunkPos.z + ") in regionfile " + this.file.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.getContainingDataFolder();
+            File[] regionFiles = containingFolder.toFile().listFiles();
+            boolean[] oversized = new boolean[32 * 32];
+            RegionFileCompression[] oversizedCompressionTypes = new RegionFileCompression[32 * 32];
+
+            if (regionFiles != null) {
+                ChunkCoordIntPair ourLowerLeftPosition = RegionFileCache.getRegionFileCoordinates(this.file);
+
+                if (ourLowerLeftPosition == null) {
+                    MinecraftServer.LOGGER.fatal("Unable to get chunk location of regionfile " + this.file.getAbsolutePath() + ", cannot recover oversized chunks");
+                } else {
+                    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) {
+                        ChunkCoordIntPair 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) {
+                            MinecraftServer.LOGGER.error("Failed to read oversized chunk data in file " + regionFile.getAbsolutePath() + ", data will be lost", ex);
+                            continue;
+                        }
+
+                        NBTTagCompound compound = null;
+
+                        // We do not know the compression type, as it's stored in the regionfile. So we need to try all of them
+                        RegionFileCompression compression = null;
+                        for (RegionFileCompression compressionType : RegionFileCompression.getCompressionTypes().values()) {
+                            try {
+                                DataInputStream in = new DataInputStream(new BufferedInputStream(compressionType.wrap(new ByteArrayInputStream(chunkData)))); // typical java
+                                compound = NBTCompressedStreamTools.readNBT((DataInput)in);
+                                compression = compressionType;
+                                break; // reaches here iff readNBT does not throw
+                            } catch (Exception ex) {
+                                continue;
+                            }
+                        }
+
+                        if (compound == null) {
+                            MinecraftServer.LOGGER.error("Failed to read oversized chunk data in file " + regionFile.getAbsolutePath() + ", it's corrupt. Its data will be lost");
+                            continue;
+                        }
+
+                        if (compounds[location] == null || ChunkRegionLoader.getLastWorldSaveTime(compound) > ChunkRegionLoader.getLastWorldSaveTime(compounds[location])) {
+                            oversized[location] = true;
+                            oversizedCompressionTypes[location] = compression;
+                        }
+                    }
+                }
+            }
+
+            // now we need to calculate a new offset header
+
+            int[] calculatedOffsets = new int[32 * 32];
+            RegionFileBitSet newSectorAllocations = new RegionFileBitSet();
+            newSectorAllocations.allocate(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 {
+                        MinecraftServer.LOGGER.error("Failed to allocate space for local chunk (overlapping data??) at (" + chunkX + "," + chunkZ + ") in regionfile " + this.file.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.allocateNewSpace(1);
+                    int sectorLength = 1;
+
+                    try {
+                        this.dataFile.write(this.getOversizedChunkHolderData(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);
+                        MinecraftServer.LOGGER.error("Failed to write new oversized chunk data holder, local chunk at (" + chunkX + "," + chunkZ + ") in regionfile " + this.file.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) {
+                    MinecraftServer.LOGGER.error("Failed to write aikar oversized chunk meta, all aikar style oversized chunk data will be lost for regionfile " + this.file.getAbsolutePath(), ex);
+                    this.getOversizedMetaFile().delete();
+                }
+            } else {
+                this.getOversizedMetaFile().delete();
+            }
+
+            this.freeSectors.copyFrom(newSectorAllocations);
+
+            // before we overwrite the old sectors, print a summary of the chunks that got changed.
+
+            MinecraftServer.LOGGER.info("Starting summary of changes for regionfile " + this.file.getAbsolutePath());
+
+            for (int chunkX = 0; chunkX < 32; ++chunkX) {
+                for (int chunkZ = 0; chunkZ < 32; ++chunkZ) {
+                    int location = chunkX | (chunkZ << 5);
+
+                    int oldOffset = this.getOffsets().get(location);
+                    int newOffset = calculatedOffsets[location];
+
+                    if (oldOffset == newOffset) {
+                        continue;
+                    }
+
+                    this.getOffsets().put(location, newOffset); // overwrite incorrect offset
+
+                    if (oldOffset == 0) {
+                        // found lost data
+                        MinecraftServer.LOGGER.info("Found missing data for local chunk (" + chunkX + "," + chunkZ + ") in regionfile " + this.file.getAbsolutePath());
+                    } else if (newOffset == 0) {
+                        MinecraftServer.LOGGER.warn("Data for local chunk (" + chunkX + "," + chunkZ + ") could not be recovered in regionfile " + this.file.getAbsolutePath() + ", it will be regenerated");
+                    } else {
+                        MinecraftServer.LOGGER.info("Local chunk (" + chunkX + "," + chunkZ + ") changed to point to newer data or correct chunk in regionfile " + this.file.getAbsolutePath());
+                    }
+                }
+            }
+
+            MinecraftServer.LOGGER.info("End of change summary for regionfile " + this.file.getAbsolutePath());
+
+            // simply destroy the timestamp header, it's not used
+
+            for (int i = 0; i < 32 * 32; ++i) {
+                this.getTimestamps().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.flushHeader();
+                this.dataFile.force(true); // try to ensure it goes through...
+                MinecraftServer.LOGGER.info("Successfully wrote new header to disk for regionfile " + this.file.getAbsolutePath());
+            } catch (IOException ex) {
+                MinecraftServer.LOGGER.fatal("Failed to write new header to disk for regionfile " + this.file.getAbsolutePath(), ex);
+            }
+        }
+    }
+
+    final boolean canRecalcHeader; // final forces compile fail on new constructor
+    // Tuinity end
+
     public final java.util.concurrent.locks.ReentrantLock fileLock = new java.util.concurrent.locks.ReentrantLock(true); // Paper
 
     // Paper start - Cache chunk status
@@ -65,11 +401,22 @@ public class RegionFile implements AutoCloseable {
     // Paper end
 
     public RegionFile(File file, File file1, boolean flag) throws IOException {
+        // Tuinity start - add can recalc flag
         this(file.toPath(), file1.toPath(), RegionFileCompression.b, flag);
     }
+    public RegionFile(File file, File file1, boolean flag, boolean canRecalcHeader) throws IOException {
+        this(file.toPath(), file1.toPath(), RegionFileCompression.b, flag, canRecalcHeader);
+        // Tuinity end - add can recalc flag
+    }
 
     public RegionFile(java.nio.file.Path java_nio_file_path, java.nio.file.Path java_nio_file_path1, RegionFileCompression regionfilecompression, boolean flag) throws IOException {
+        // Tuinity start - add can recalc flag
+        this(java_nio_file_path, java_nio_file_path1, regionfilecompression, flag, false);
+    }
+    public RegionFile(java.nio.file.Path java_nio_file_path, java.nio.file.Path java_nio_file_path1, RegionFileCompression regionfilecompression, boolean flag, boolean canRecalcHeader) throws IOException {
         this.g = ByteBuffer.allocateDirect(8192);
+        this.canRecalcHeader = canRecalcHeader;
+        // Tuinity end - add can recalc flag
         this.file = java_nio_file_path.toFile(); // Paper
         initOversizedState(); // Paper
         this.freeSectors = new RegionFileBitSet();
@@ -97,14 +444,16 @@ public class RegionFile implements AutoCloseable {
                     RegionFile.LOGGER.warn("Region file {} has truncated header: {}", java_nio_file_path, i);
                 }
 
-                long j = Files.size(java_nio_file_path);
+                final long j = Files.size(java_nio_file_path); final long regionFileSize = j;
 
+                boolean needsHeaderRecalc = false; // Tuinity - recalculate header on header corruption
+                boolean hasBackedUp = false; // Tuinity - recalculate header on header corruption
                 for (int k = 0; k < 1024; ++k) {
-                    int l = this.h.get(k);
+                    int l = this.h.get(k); final int headerLocation = k; // Tuinity - we expect this to be the header location
 
                     if (l != 0) {
-                        int i1 = b(l);
-                        int j1 = a(l);
+                        final int i1 = b(l); final int offset = i1; // Tuinity - we expect this to be offset in file in sectors
+                        int j1 = a(l); final int sectorLength; // Tuinity - 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
@@ -112,33 +461,105 @@ public class RegionFile implements AutoCloseable {
                             this.dataFile.read(realLen, i1 * 4096);
                             j1 = (realLen.getInt(0) + 4) / 4096 + 1;
                         }
+                        sectorLength = j1; // Tuinity - diff on change, we expect this to be sector length of region
                         // Spigot end
 
                         if (i1 < 2) {
                             RegionFile.LOGGER.warn("Region file {} has invalid sector at index: {}; sector {} overlaps with header", java_nio_file_path, k, i1);
-                            this.h.put(k, 0);
-                        } else if (j1 == 0) {
+                            //this.h.put(k, 0); // Tuinity - we catch this, but need it in the header for the summary change
+                        } else if (j1 <= 0) { // Tuinity - <= 0, not ==
                             RegionFile.LOGGER.warn("Region file {} has an invalid sector at index: {}; size has to be > 0", java_nio_file_path, k);
-                            this.h.put(k, 0);
+                            //this.h.put(k, 0); // Tuinity - 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", java_nio_file_path, k, i1);
-                            this.h.put(k, 0);
+                            //this.h.put(k, 0); // Tuinity - we catch this, but need it in the header for the summary change
                         } else {
-                            this.freeSectors.a(i1, j1);
+                            //this.freeSectors.a(i1, j1); // Tuinity - move this down so we can check if it fails to allocate
+                        }
+                        // Tuinity start - recalculate header on header corruption
+                        if (offset < 2 || sectorLength <= 0 || ((long)offset * 4096L) > regionFileSize) {
+                            if (canRecalcHeader) {
+                                MinecraftServer.LOGGER.error("Detected invalid header for regionfile " + this.file.getAbsolutePath() + "! Recalculating header...");
+                                needsHeaderRecalc = true;
+                                break;
+                            } else {
+                                // location = chunkX | (chunkZ << 5);
+                                MinecraftServer.LOGGER.fatal("Detected invalid header for regionfile " + this.file.getAbsolutePath() +
+                                        "! Cannot recalculate, removing local chunk (" + (headerLocation & 31) + "," + (headerLocation >>> 5) + ") from header");
+                                if (!hasBackedUp) {
+                                    hasBackedUp = true;
+                                    this.backupRegionFile();
+                                }
+                                this.getTimestamps().put(headerLocation, 0); // be consistent, delete the timestamp too
+                                this.getOffsets().put(headerLocation, 0); // delete the entry from header
+                                continue;
+                            }
+                        }
+                        boolean failedToAllocate = !this.freeSectors.tryAllocate(offset, sectorLength);
+                        if (failedToAllocate) {
+                            MinecraftServer.LOGGER.error("Overlapping allocation by local chunk (" + (headerLocation & 31) + "," + (headerLocation >>> 5) + ") in regionfile " + this.file.getAbsolutePath());
                         }
+                        if (failedToAllocate & !canRecalcHeader) {
+                            // location = chunkX | (chunkZ << 5);
+                            MinecraftServer.LOGGER.fatal("Detected invalid header for regionfile " + this.file.getAbsolutePath() +
+                                    "! Cannot recalculate, removing local chunk (" + (headerLocation & 31) + "," + (headerLocation >>> 5) + ") from header");
+                            if (!hasBackedUp) {
+                                hasBackedUp = true;
+                                this.backupRegionFile();
+                            }
+                            this.getTimestamps().put(headerLocation, 0); // be consistent, delete the timestamp too
+                            this.getOffsets().put(headerLocation, 0); // delete the entry from header
+                            continue;
+                        }
+                        needsHeaderRecalc |= failedToAllocate;
+                        // Tuinity end - recalculate header on header corruption
                     }
                 }
+
+                // Tuinity 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
+                    MinecraftServer.LOGGER.error("Recalculating regionfile " + this.file.getAbsolutePath() + ", header gave erroneous offsets & locations");
+                    this.recalculateHeader();
+                }
+                // Tuinity end
             }
 
         }
     }
 
+    private final java.nio.file.Path getOversizedChunkPath(ChunkCoordIntPair chunkcoordintpair) { return this.e(chunkcoordintpair); } // Tuinity - OBFHELPER
     private java.nio.file.Path e(ChunkCoordIntPair chunkcoordintpair) {
-        String s = "c." + chunkcoordintpair.x + "." + chunkcoordintpair.z + ".mcc";
+        String s = "c." + chunkcoordintpair.x + "." + chunkcoordintpair.z + ".mcc"; // Tuinity - diff on change
 
         return this.e.resolve(s);
     }
 
+    // Tuinity start
+    private static ChunkCoordIntPair 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 ChunkCoordIntPair(x, z);
+        } catch (NumberFormatException ex) {
+            return null;
+        }
+    }
+    // Tuinity end
+
     @Nullable public synchronized DataInputStream getReadStream(ChunkCoordIntPair chunkCoordIntPair) throws IOException { return a(chunkCoordIntPair);} // Paper - OBFHELPER
     @Nullable
     public synchronized DataInputStream a(ChunkCoordIntPair chunkcoordintpair) throws IOException {
@@ -163,6 +584,12 @@ public class RegionFile implements AutoCloseable {
             ((java.nio.Buffer) bytebuffer).flip();
             if (bytebuffer.remaining() < 5) {
                 RegionFile.LOGGER.error("Chunk {} header is truncated: expected {} but read {}", chunkcoordintpair, l, bytebuffer.remaining());
+                // Tuinity start - recalculate header on regionfile corruption
+                if (this.canRecalcHeader) {
+                    this.recalculateHeader();
+                    return this.getReadStream(chunkcoordintpair);
+                }
+                // Tuinity end
                 return null;
             } else {
                 int i1 = bytebuffer.getInt();
@@ -170,6 +597,12 @@ public class RegionFile implements AutoCloseable {
 
                 if (i1 == 0) {
                     RegionFile.LOGGER.warn("Chunk {} is allocated, but stream is missing", chunkcoordintpair);
+                    // Tuinity start - recalculate header on regionfile corruption
+                    if (this.canRecalcHeader) {
+                        this.recalculateHeader();
+                        return this.getReadStream(chunkcoordintpair);
+                    }
+                    // Tuinity end - recalculate header on regionfile corruption
                     return null;
                 } else {
                     int j1 = i1 - 1;
@@ -177,17 +610,49 @@ public class RegionFile implements AutoCloseable {
                     if (a(b0)) {
                         if (j1 != 0) {
                             RegionFile.LOGGER.warn("Chunk has both internal and external streams");
+                            // Tuinity start - recalculate header on regionfile corruption
+                            if (this.canRecalcHeader) {
+                                this.recalculateHeader();
+                                return this.getReadStream(chunkcoordintpair);
+                            }
+                            // Tuinity end - recalculate header on regionfile corruption
                         }
 
-                        return this.a(chunkcoordintpair, b(b0));
+                        // Tuinity start - recalculate header on regionfile corruption
+                        DataInputStream ret = this.a(chunkcoordintpair, b(b0));
+                        if (ret == null && this.canRecalcHeader) {
+                            this.recalculateHeader();
+                            return this.getReadStream(chunkcoordintpair);
+                        }
+                        return ret;
+                        // Tuinity end - recalculate header on regionfile corruption
                     } else if (j1 > bytebuffer.remaining()) {
                         RegionFile.LOGGER.error("Chunk {} stream is truncated: expected {} but read {}", chunkcoordintpair, j1, bytebuffer.remaining());
+                        // Tuinity start - recalculate header on regionfile corruption
+                        if (this.canRecalcHeader) {
+                            this.recalculateHeader();
+                            return this.getReadStream(chunkcoordintpair);
+                        }
+                        // Tuinity end
                         return null;
                     } else if (j1 < 0) {
                         RegionFile.LOGGER.error("Declared size {} of chunk {} is negative", i1, chunkcoordintpair);
+                        // Tuinity start - recalculate header on regionfile corruption
+                        if (this.canRecalcHeader) {
+                            this.recalculateHeader();
+                            return this.getReadStream(chunkcoordintpair);
+                        }
+                        // Tuinity end - recalculate header on regionfile corruption
                         return null;
                     } else {
-                        return this.a(chunkcoordintpair, b0, a(bytebuffer, j1));
+                        // Tuinity start - recalculate header on regionfile corruption
+                        DataInputStream ret = this.a(chunkcoordintpair, b0, a(bytebuffer, j1));
+                        if (ret == null && this.canRecalcHeader) {
+                            this.recalculateHeader();
+                            return this.getReadStream(chunkcoordintpair);
+                        }
+                        return ret;
+                        // Tuinity end - recalculate header on regionfile corruption
                     }
                 }
             }
@@ -347,10 +812,15 @@ public class RegionFile implements AutoCloseable {
     }
 
     private ByteBuffer b() {
+        // Tuinity start - add compressionType param
+        return this.getOversizedChunkHolderData(this.getRegionFileCompression());
+    }
+    private ByteBuffer getOversizedChunkHolderData(RegionFileCompression compressionType) {
+        // Tuinity end
         ByteBuffer bytebuffer = ByteBuffer.allocate(5);
 
         bytebuffer.putInt(1);
-        bytebuffer.put((byte) (this.f.a() | 128));
+        bytebuffer.put((byte) (compressionType.compressionTypeId() | 128)); // Tuinity - replace with compressionType
         ((java.nio.Buffer) bytebuffer).flip();
         return bytebuffer;
     }
@@ -387,6 +857,7 @@ public class RegionFile implements AutoCloseable {
         };
     }
 
+    private final void flushHeader() throws IOException { this.b(); } // Tuinity - OBFHELPER
     private void c() throws IOException {
         ((java.nio.Buffer) this.g).position(0);
         this.dataFile.write(this.g, 0L);
diff --git a/src/main/java/net/minecraft/server/RegionFileBitSet.java b/src/main/java/net/minecraft/server/RegionFileBitSet.java
index 1ebdf73cc927405bc536dc74a5118d2a086db0e5..cfa3ecb031b59ec677f016ecdea92d16436fb511 100644
--- a/src/main/java/net/minecraft/server/RegionFileBitSet.java
+++ b/src/main/java/net/minecraft/server/RegionFileBitSet.java
@@ -4,18 +4,42 @@ import java.util.BitSet;
 
 public class RegionFileBitSet {
 
-    private final BitSet a = new BitSet();
+    private final BitSet a = new BitSet(); private final BitSet getBitset() { return this.a; } // Tuinity - OBFHELPER
 
     public RegionFileBitSet() {}
 
+    public final void allocate(int from, int length) { this.a(from, length); } // Tuinity - OBFHELPER
     public void a(int i, int j) {
         this.a.set(i, i + j);
     }
 
+    public final void free(int from, int length) { this.b(from, length); } // Tuinity - OBFHELPER
     public void b(int i, int j) {
         this.a.clear(i, i + j);
     }
 
+    // Tuinity start
+    public final void copyFrom(RegionFileBitSet other) {
+        BitSet thisBitset = this.getBitset();
+        BitSet otherBitset = other.getBitset();
+
+        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.getBitset();
+        int firstSet = bitset.nextSetBit(from);
+        if (firstSet > 0 && firstSet < (from + length)) {
+            return false;
+        }
+        bitset.set(from, from + length);
+        return true;
+    }
+    // Tuinity end
+
+    public final int allocateNewSpace(final int requiredLength) { return this.a(requiredLength); } // Tuinity - OBFHELPER
     public int a(int i) {
         int j = 0;
 
diff --git a/src/main/java/net/minecraft/server/RegionFileCache.java b/src/main/java/net/minecraft/server/RegionFileCache.java
index d64f7ad925e5f40740a58ceee0845ac2db5419f2..8b341c14e7082fc96a464f2386a3dedea31ec59c 100644
--- a/src/main/java/net/minecraft/server/RegionFileCache.java
+++ b/src/main/java/net/minecraft/server/RegionFileCache.java
@@ -15,12 +15,43 @@ public class RegionFileCache implements AutoCloseable { // Paper - no final
     public final Long2ObjectLinkedOpenHashMap<RegionFile> cache = new Long2ObjectLinkedOpenHashMap();
     private final File b;
     private final boolean c;
+    private final boolean isChunkData; // Tuinity
 
     RegionFileCache(File file, boolean flag) {
+        // Tuinity start - add isChunkData param
+        this(file, flag, false);
+    }
+    RegionFileCache(File file, boolean flag, boolean isChunkData) {
+        this.isChunkData = isChunkData;
+        // Tuinity end - add isChunkData param
         this.b = file;
         this.c = flag;
     }
 
+    // Tuinity start
+    public static ChunkCoordIntPair getRegionFileCoordinates(File file) {
+        String fileName = file.getName();
+        if (!fileName.startsWith("r.") || !fileName.endsWith(".mca")) {
+            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 ChunkCoordIntPair(x << 5, z << 5);
+        } catch (NumberFormatException ex) {
+            return null;
+        }
+    }
+    // Tuinity end
+
 
     // Paper start
     public synchronized RegionFile getRegionFileIfLoaded(ChunkCoordIntPair chunkcoordintpair) { // Paper - synchronize for async io
@@ -54,9 +85,9 @@ public class RegionFileCache implements AutoCloseable { // Paper - no final
                 this.b.mkdirs();
             }
 
-            File file = new File(this.b, "r." + chunkcoordintpair.getRegionX() + "." + chunkcoordintpair.getRegionZ() + ".mca");
+            File file = new File(this.b, "r." + chunkcoordintpair.getRegionX() + "." + chunkcoordintpair.getRegionZ() + ".mca"); // Tuinity - diff on change
             if (existingOnly && !file.exists()) return null; // CraftBukkit
-            RegionFile regionfile1 = new RegionFile(file, this.b, this.c);
+            RegionFile regionfile1 = new RegionFile(file, this.b, this.c, this.isChunkData); // Tuinity - allow for chunk regionfiles to regen header
 
             this.cache.putAndMoveToFirst(i, regionfile1);
             // Paper start
@@ -145,6 +176,13 @@ public class RegionFileCache implements AutoCloseable { // Paper - no final
             return null;
         }
         // CraftBukkit end
+        // Tuinity start - Add regionfile parameter
+        return this.readFromRegionFile(regionfile, chunkcoordintpair);
+    }
+    private NBTTagCompound readFromRegionFile(RegionFile regionfile, ChunkCoordIntPair chunkcoordintpair) 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
+        // Tuinity end
         try { // Paper
         DataInputStream datainputstream = regionfile.a(chunkcoordintpair);
         // Paper start
@@ -160,6 +198,17 @@ public class RegionFileCache implements AutoCloseable { // Paper - no final
         try {
             if (datainputstream != null) {
                 nbttagcompound = NBTCompressedStreamTools.a((DataInput) datainputstream);
+                // Tuinity start - recover from corrupt regionfile header
+                if (this.isChunkData) {
+                    ChunkCoordIntPair chunkPos = ChunkRegionLoader.getChunkCoordinate(nbttagcompound);
+                    if (!chunkPos.equals(chunkcoordintpair)) {
+                        MinecraftServer.LOGGER.error("Attempting to read chunk data at " + chunkcoordintpair.toString() + " but got chunk data for " + chunkPos.toString() + " instead! Attempting regionfile recalculation for regionfile " + regionfile.file.getAbsolutePath());
+                        regionfile.recalculateHeader();
+                        regionfile.fileLock.lock(); // otherwise we will unlock twice and only lock once.
+                        return this.readFromRegionFile(regionfile, chunkcoordintpair);
+                    }
+                }
+                // Tuinity end - recover from corrupt regionfile header
                 return nbttagcompound;
             }
 
diff --git a/src/main/java/net/minecraft/server/RegionFileCompression.java b/src/main/java/net/minecraft/server/RegionFileCompression.java
index 3382d678e68e559b8d3cb9dced4fce24206cd38f..3b7894256dc8daa81be35f845cb5f8de02d7cb00 100644
--- a/src/main/java/net/minecraft/server/RegionFileCompression.java
+++ b/src/main/java/net/minecraft/server/RegionFileCompression.java
@@ -13,7 +13,7 @@ import javax.annotation.Nullable;
 
 public class RegionFileCompression {
 
-    private static final Int2ObjectMap<RegionFileCompression> d = new Int2ObjectOpenHashMap();
+    private static final Int2ObjectMap<RegionFileCompression> d = new Int2ObjectOpenHashMap(); static final Int2ObjectMap<RegionFileCompression> getCompressionTypes() { return RegionFileCompression.d; } // Tuinity - OBFHELPER
     public static final RegionFileCompression a = a(new RegionFileCompression(1, GZIPInputStream::new, GZIPOutputStream::new));
     public static final RegionFileCompression b = a(new RegionFileCompression(2, InflaterInputStream::new, DeflaterOutputStream::new));
     public static final RegionFileCompression c = a(new RegionFileCompression(3, (inputstream) -> {
@@ -36,8 +36,8 @@ public class RegionFileCompression {
         return regionfilecompression;
     }
 
-    @Nullable
-    public static RegionFileCompression a(int i) {
+    @Nullable public static RegionFileCompression getByType(int type) { return RegionFileCompression.a(type); } // Tuinity - OBFHELPER
+    @Nullable public static RegionFileCompression a(int i) { // Tuinity - OBFHELPER
         return (RegionFileCompression) RegionFileCompression.d.get(i);
     }
 
@@ -45,6 +45,7 @@ public class RegionFileCompression {
         return RegionFileCompression.d.containsKey(i);
     }
 
+    public final int compressionTypeId() { return this.a(); } // Tuinity - OBFHELPER
     public int a() {
         return this.e;
     }
@@ -53,6 +54,7 @@ public class RegionFileCompression {
         return (OutputStream) this.g.wrap(outputstream);
     }
 
+    public final InputStream wrap(InputStream inputstream) throws IOException { return this.a(inputstream); } // Tuinity - OBFHELPER
     public InputStream a(InputStream inputstream) throws IOException {
         return (InputStream) this.f.wrap(inputstream);
     }
diff --git a/src/main/java/net/minecraft/server/WorldUpgrader.java b/src/main/java/net/minecraft/server/WorldUpgrader.java
index 5ccdc0b87b922724c3dd3085860c55d4959ca0b4..888dae2d5ee8a71e83dd24e5f3c6bc8513016f9d 100644
--- a/src/main/java/net/minecraft/server/WorldUpgrader.java
+++ b/src/main/java/net/minecraft/server/WorldUpgrader.java
@@ -218,7 +218,7 @@ public class WorldUpgrader {
                     int l = Integer.parseInt(matcher.group(2)) << 5;
 
                     try {
-                        RegionFile regionfile = new RegionFile(file2, file1, true);
+                        RegionFile regionfile = new RegionFile(file2, file1, true, true); // Tuinity - allow for chunk regionfiles to regen header
                         Throwable throwable = null;
 
                         try {
