mirror of
https://github.com/PaperMC/Paper.git
synced 2024-10-30 23:39:58 +01:00
760 lines
41 KiB
Diff
760 lines
41 KiB
Diff
|
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..4881e6ef4393a3d4fc1bd88e2574dcb6d7028e40 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,341 @@ 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);
|
||
|
+ }
|
||
|
+ }
|
||
|
+
|
||
|
+ // note: only call for CHUNK regionfiles
|
||
|
+ void recalculateHeader() throws IOException {
|
||
|
+ if (!this.canRecalcHeader) {
|
||
|
+ return;
|
||
|
+ }
|
||
|
+ 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);
|
||
|
+ 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) {
|
||
|
+ ChunkPos ourLowerLeftPosition = RegionFileStorage.getRegionFileCoordinates(this.regionFile);
|
||
|
+
|
||
|
+ if (ourLowerLeftPosition == null) {
|
||
|
+ LOGGER.fatal("Unable to get chunk location of regionfile " + this.regionFile.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) {
|
||
|
+ 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 (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);
|
||
|
+ }
|
||
|
+ }
|
||
|
+ }
|
||
|
+
|
||
|
+ 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 +417,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 +458,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 +476,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 +595,12 @@ 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 +608,12 @@ 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 +621,49 @@ 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 +838,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..4e0006b73fbd2634aa42334ae9dde79b4eccaf38 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,17 @@ 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());
|
||
|
+ regionfile.recalculateHeader();
|
||
|
+ regionfile.fileLock.lock(); // otherwise we will unlock twice and only lock once.
|
||
|
+ return this.read(pos, regionfile);
|
||
|
+ }
|
||
|
+ }
|
||
|
+ // 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) -> {
|