342 lines
13 KiB
Java
342 lines
13 KiB
Java
package net.minecraft.world.level.chunk.storage;
|
|
|
|
import java.io.ByteArrayOutputStream;
|
|
import org.apache.logging.log4j.LogManager;
|
|
import java.nio.file.StandardCopyOption;
|
|
import java.nio.file.CopyOption;
|
|
import java.nio.file.attribute.FileAttribute;
|
|
import net.minecraft.Util;
|
|
import java.io.BufferedOutputStream;
|
|
import java.io.OutputStream;
|
|
import java.io.DataOutputStream;
|
|
import java.io.ByteArrayInputStream;
|
|
import java.io.BufferedInputStream;
|
|
import javax.annotation.Nullable;
|
|
import java.io.InputStream;
|
|
import java.io.DataInputStream;
|
|
import net.minecraft.world.level.ChunkPos;
|
|
import java.nio.file.StandardOpenOption;
|
|
import java.nio.file.OpenOption;
|
|
import java.nio.file.Files;
|
|
import java.nio.file.LinkOption;
|
|
import java.io.IOException;
|
|
import java.io.File;
|
|
import java.nio.IntBuffer;
|
|
import java.nio.file.Path;
|
|
import java.nio.channels.FileChannel;
|
|
import java.nio.ByteBuffer;
|
|
import org.apache.logging.log4j.Logger;
|
|
|
|
public class RegionFile implements AutoCloseable {
|
|
private static final Logger LOGGER;
|
|
private static final ByteBuffer PADDING_BUFFER;
|
|
private final FileChannel file;
|
|
private final Path externalFileDir;
|
|
private final RegionFileVersion version;
|
|
private final ByteBuffer header;
|
|
private final IntBuffer offsets;
|
|
private final IntBuffer timestamps;
|
|
private final RegionBitmap usedSectors;
|
|
|
|
public RegionFile(final File file1, final File file2) throws IOException {
|
|
this(file1.toPath(), file2.toPath(), RegionFileVersion.VERSION_DEFLATE);
|
|
}
|
|
|
|
public RegionFile(final Path path1, final Path path2, final RegionFileVersion cbb) throws IOException {
|
|
this.header = ByteBuffer.allocateDirect(8192);
|
|
this.usedSectors = new RegionBitmap();
|
|
this.version = cbb;
|
|
if (!Files.isDirectory(path2)) {
|
|
throw new IllegalArgumentException("Expected directory, got " + path2.toAbsolutePath());
|
|
}
|
|
this.externalFileDir = path2;
|
|
(this.offsets = this.header.asIntBuffer()).limit(1024);
|
|
this.header.position(4096);
|
|
this.timestamps = this.header.asIntBuffer();
|
|
this.file = FileChannel.open(path1, StandardOpenOption.CREATE, StandardOpenOption.READ, StandardOpenOption.WRITE);
|
|
this.usedSectors.force(0, 2);
|
|
this.header.position(0);
|
|
final int integer5 = this.file.read(this.header, 0L);
|
|
if (integer5 != -1) {
|
|
if (integer5 != 8192) {
|
|
RegionFile.LOGGER.warn("Region file {} has truncated header: {}", path1, integer5);
|
|
}
|
|
for (int integer6 = 0; integer6 < 1024; ++integer6) {
|
|
final int integer7 = this.offsets.get(integer6);
|
|
if (integer7 != 0) {
|
|
final int integer8 = getSectorNumber(integer7);
|
|
final int integer9 = getNumSectors(integer7);
|
|
this.usedSectors.force(integer8, integer9);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private Path getExternalChunkPath(final ChunkPos bje) {
|
|
final String string3 = "c." + bje.x + "." + bje.z + ".mcc";
|
|
return this.externalFileDir.resolve(string3);
|
|
}
|
|
|
|
@Nullable
|
|
public synchronized DataInputStream getChunkDataInputStream(final ChunkPos bje) throws IOException {
|
|
final int integer3 = this.getOffset(bje);
|
|
if (integer3 == 0) {
|
|
return null;
|
|
}
|
|
final int integer4 = getSectorNumber(integer3);
|
|
final int integer5 = getNumSectors(integer3);
|
|
final int integer6 = integer5 * 4096;
|
|
final ByteBuffer byteBuffer7 = ByteBuffer.allocate(integer6);
|
|
this.file.read(byteBuffer7, integer4 * 4096);
|
|
byteBuffer7.flip();
|
|
if (byteBuffer7.remaining() < 5) {
|
|
RegionFile.LOGGER.error("Chunk {} header is truncated: expected {} but read {}", bje, integer6, byteBuffer7.remaining());
|
|
return null;
|
|
}
|
|
final int integer7 = byteBuffer7.getInt();
|
|
final byte byte9 = byteBuffer7.get();
|
|
if (integer7 == 0) {
|
|
RegionFile.LOGGER.warn("Chunk {} is allocated, but stream is missing", bje);
|
|
return null;
|
|
}
|
|
final int integer8 = integer7 - 1;
|
|
if (isExternalStreamChunk(byte9)) {
|
|
if (integer8 != 0) {
|
|
RegionFile.LOGGER.warn("Chunk has both internal and external streams");
|
|
}
|
|
return this.createExternalChunkInputStream(bje, getExternalChunkVersion(byte9));
|
|
}
|
|
if (integer8 > byteBuffer7.remaining()) {
|
|
RegionFile.LOGGER.error("Chunk {} stream is truncated: expected {} but read {}", bje, integer8, byteBuffer7.remaining());
|
|
return null;
|
|
}
|
|
if (integer8 < 0) {
|
|
RegionFile.LOGGER.error("Declared size {} of chunk {} is negative", integer7, bje);
|
|
return null;
|
|
}
|
|
return this.createChunkInputStream(bje, byte9, createStream(byteBuffer7, integer8));
|
|
}
|
|
|
|
private static boolean isExternalStreamChunk(final byte byte1) {
|
|
return (byte1 & 0x80) != 0x0;
|
|
}
|
|
|
|
private static byte getExternalChunkVersion(final byte byte1) {
|
|
return (byte)(byte1 & 0xFFFFFF7F);
|
|
}
|
|
|
|
@Nullable
|
|
private DataInputStream createChunkInputStream(final ChunkPos bje, final byte byte2, final InputStream inputStream) throws IOException {
|
|
final RegionFileVersion cbb5 = RegionFileVersion.fromId(byte2);
|
|
if (cbb5 == null) {
|
|
RegionFile.LOGGER.error("Chunk {} has invalid chunk stream version {}", bje, byte2);
|
|
return null;
|
|
}
|
|
return new DataInputStream(new BufferedInputStream(cbb5.wrap(inputStream)));
|
|
}
|
|
|
|
@Nullable
|
|
private DataInputStream createExternalChunkInputStream(final ChunkPos bje, final byte byte2) throws IOException {
|
|
final Path path4 = this.getExternalChunkPath(bje);
|
|
if (!Files.isRegularFile(path4)) {
|
|
RegionFile.LOGGER.error("External chunk path {} is not file", path4);
|
|
return null;
|
|
}
|
|
return this.createChunkInputStream(bje, byte2, Files.newInputStream(path4));
|
|
}
|
|
|
|
private static ByteArrayInputStream createStream(final ByteBuffer byteBuffer, final int integer) {
|
|
return new ByteArrayInputStream(byteBuffer.array(), byteBuffer.position(), integer);
|
|
}
|
|
|
|
private int packSectorOffset(final int integer1, final int integer2) {
|
|
return integer1 << 8 | integer2;
|
|
}
|
|
|
|
private static int getNumSectors(final int integer) {
|
|
return integer & 0xFF;
|
|
}
|
|
|
|
private static int getSectorNumber(final int integer) {
|
|
return integer >> 8;
|
|
}
|
|
|
|
private static int sizeToSectors(final int integer) {
|
|
return (integer + 4096 - 1) / 4096;
|
|
}
|
|
|
|
public boolean doesChunkExist(final ChunkPos bje) {
|
|
final int integer3 = this.getOffset(bje);
|
|
if (integer3 == 0) {
|
|
return false;
|
|
}
|
|
final int integer4 = getSectorNumber(integer3);
|
|
final int integer5 = getNumSectors(integer3);
|
|
final ByteBuffer byteBuffer6 = ByteBuffer.allocate(5);
|
|
try {
|
|
this.file.read(byteBuffer6, integer4 * 4096);
|
|
byteBuffer6.flip();
|
|
if (byteBuffer6.remaining() != 5) {
|
|
return false;
|
|
}
|
|
final int integer6 = byteBuffer6.getInt();
|
|
final byte byte8 = byteBuffer6.get();
|
|
if (isExternalStreamChunk(byte8)) {
|
|
if (!RegionFileVersion.isValidVersion(getExternalChunkVersion(byte8))) {
|
|
return false;
|
|
}
|
|
if (!Files.isRegularFile(this.getExternalChunkPath(bje))) {
|
|
return false;
|
|
}
|
|
}
|
|
else {
|
|
if (!RegionFileVersion.isValidVersion(byte8)) {
|
|
return false;
|
|
}
|
|
if (integer6 == 0) {
|
|
return false;
|
|
}
|
|
final int integer7 = integer6 - 1;
|
|
if (integer7 < 0 || integer7 > 4096 * integer5) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
catch (IOException iOException7) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public DataOutputStream getChunkDataOutputStream(final ChunkPos bje) throws IOException {
|
|
return new DataOutputStream(new BufferedOutputStream(this.version.wrap(new ChunkBuffer(bje))));
|
|
}
|
|
|
|
protected synchronized void write(final ChunkPos bje, final ByteBuffer byteBuffer) throws IOException {
|
|
final int integer4 = getOffsetIndex(bje);
|
|
final int integer5 = this.offsets.get(integer4);
|
|
final int integer6 = getSectorNumber(integer5);
|
|
final int integer7 = getNumSectors(integer5);
|
|
final int integer8 = byteBuffer.remaining();
|
|
int integer9 = sizeToSectors(integer8);
|
|
int integer10;
|
|
CommitOp b11;
|
|
if (integer9 >= 256) {
|
|
final Path path12 = this.getExternalChunkPath(bje);
|
|
RegionFile.LOGGER.warn("Saving oversized chunk {} ({} bytes} to external file {}", bje, integer8, path12);
|
|
integer9 = 1;
|
|
integer10 = this.usedSectors.allocate(integer9);
|
|
b11 = this.writeToExternalFile(path12, byteBuffer);
|
|
final ByteBuffer byteBuffer2 = this.createExternalStub();
|
|
this.file.write(byteBuffer2, integer10 * 4096);
|
|
}
|
|
else {
|
|
integer10 = this.usedSectors.allocate(integer9);
|
|
b11 = (() -> Files.deleteIfExists(this.getExternalChunkPath(bje)));
|
|
this.file.write(byteBuffer, integer10 * 4096);
|
|
}
|
|
final int integer11 = (int)(Util.getEpochMillis() / 1000L);
|
|
this.offsets.put(integer4, this.packSectorOffset(integer10, integer9));
|
|
this.timestamps.put(integer4, integer11);
|
|
this.writeHeader();
|
|
b11.run();
|
|
if (integer6 != 0) {
|
|
this.usedSectors.free(integer6, integer7);
|
|
}
|
|
}
|
|
|
|
private ByteBuffer createExternalStub() {
|
|
final ByteBuffer byteBuffer2 = ByteBuffer.allocate(5);
|
|
byteBuffer2.putInt(1);
|
|
byteBuffer2.put((byte)(this.version.getId() | 0x80));
|
|
byteBuffer2.flip();
|
|
return byteBuffer2;
|
|
}
|
|
|
|
private CommitOp writeToExternalFile(final Path path, final ByteBuffer byteBuffer) throws IOException {
|
|
final Path path2 = Files.createTempFile(this.externalFileDir, "tmp", null, new FileAttribute[0]);
|
|
try (final FileChannel fileChannel5 = FileChannel.open(path2, StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
|
|
byteBuffer.position(5);
|
|
fileChannel5.write(byteBuffer);
|
|
}
|
|
return () -> Files.move(path2, path, StandardCopyOption.REPLACE_EXISTING);
|
|
}
|
|
|
|
private void writeHeader() throws IOException {
|
|
this.header.position(0);
|
|
this.file.write(this.header, 0L);
|
|
}
|
|
|
|
private int getOffset(final ChunkPos bje) {
|
|
return this.offsets.get(getOffsetIndex(bje));
|
|
}
|
|
|
|
public boolean hasChunk(final ChunkPos bje) {
|
|
return this.getOffset(bje) != 0;
|
|
}
|
|
|
|
private static int getOffsetIndex(final ChunkPos bje) {
|
|
return bje.getRegionLocalX() + bje.getRegionLocalZ() * 32;
|
|
}
|
|
|
|
@Override
|
|
public void close() throws IOException {
|
|
try {
|
|
this.padToFullSector();
|
|
}
|
|
finally {
|
|
try {
|
|
this.writeHeader();
|
|
}
|
|
finally {
|
|
try {
|
|
this.file.force(true);
|
|
}
|
|
finally {
|
|
this.file.close();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void padToFullSector() throws IOException {
|
|
final int integer2 = (int)this.file.size();
|
|
final int integer3 = sizeToSectors(integer2) * 4096;
|
|
if (integer2 != integer3) {
|
|
final ByteBuffer byteBuffer4 = RegionFile.PADDING_BUFFER.duplicate();
|
|
byteBuffer4.position(0);
|
|
this.file.write(byteBuffer4, integer3 - 1);
|
|
}
|
|
}
|
|
|
|
static {
|
|
LOGGER = LogManager.getLogger();
|
|
PADDING_BUFFER = ByteBuffer.allocateDirect(1);
|
|
}
|
|
|
|
class ChunkBuffer extends ByteArrayOutputStream {
|
|
private final ChunkPos pos;
|
|
|
|
public ChunkBuffer(final ChunkPos bje) {
|
|
super(8096);
|
|
super.write(0);
|
|
super.write(0);
|
|
super.write(0);
|
|
super.write(0);
|
|
super.write(RegionFile.this.version.getId());
|
|
this.pos = bje;
|
|
}
|
|
|
|
@Override
|
|
public void close() throws IOException {
|
|
final ByteBuffer byteBuffer2 = ByteBuffer.wrap(this.buf, 0, this.count);
|
|
byteBuffer2.putInt(0, this.count - 5 + 1);
|
|
RegionFile.this.write(this.pos, byteBuffer2);
|
|
}
|
|
}
|
|
|
|
interface CommitOp {
|
|
void run() throws IOException;
|
|
}
|
|
}
|