Code/net/minecraft/world/level/chunk/storage/RegionFile.java

407 lines
15 KiB
Java
Raw Normal View History

2025-07-01 06:20:03 +00:00
package net.minecraft.world.level.chunk.storage;
import com.google.common.annotations.VisibleForTesting;
import com.mojang.logging.LogUtils;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import javax.annotation.Nullable;
import net.minecraft.Util;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.profiling.jfr.JvmProfiler;
import net.minecraft.world.level.ChunkPos;
import org.slf4j.Logger;
public class RegionFile implements AutoCloseable {
private static final Logger LOGGER = LogUtils.getLogger();
private static final int SECTOR_BYTES = 4096;
@VisibleForTesting
protected static final int SECTOR_INTS = 1024;
private static final int CHUNK_HEADER_SIZE = 5;
private static final int HEADER_OFFSET = 0;
private static final ByteBuffer PADDING_BUFFER = ByteBuffer.allocateDirect(1);
private static final String EXTERNAL_FILE_EXTENSION = ".mcc";
private static final int EXTERNAL_STREAM_FLAG = 128;
private static final int EXTERNAL_CHUNK_THRESHOLD = 256;
private static final int CHUNK_NOT_PRESENT = 0;
final RegionStorageInfo info;
private final Path path;
private final FileChannel file;
private final Path externalFileDir;
final RegionFileVersion version;
private final ByteBuffer header = ByteBuffer.allocateDirect(8192);
private final IntBuffer offsets;
private final IntBuffer timestamps;
@VisibleForTesting
protected final RegionBitmap usedSectors = new RegionBitmap();
public RegionFile(RegionStorageInfo p_333062_, Path p_63633_, Path p_63634_, boolean p_63636_) throws IOException {
this(p_333062_, p_63633_, p_63634_, RegionFileVersion.getSelected(), p_63636_);
}
public RegionFile(RegionStorageInfo p_335896_, Path p_196950_, Path p_196951_, RegionFileVersion p_331589_, boolean p_196952_) throws IOException {
this.info = p_335896_;
this.path = p_196950_;
this.version = p_331589_;
if (!Files.isDirectory(p_196951_)) {
throw new IllegalArgumentException("Expected directory, got " + p_196951_.toAbsolutePath());
} else {
this.externalFileDir = p_196951_;
this.offsets = this.header.asIntBuffer();
this.offsets.limit(1024);
this.header.position(4096);
this.timestamps = this.header.asIntBuffer();
if (p_196952_) {
this.file = FileChannel.open(
p_196950_, StandardOpenOption.CREATE, StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.DSYNC
);
} else {
this.file = FileChannel.open(p_196950_, StandardOpenOption.CREATE, StandardOpenOption.READ, StandardOpenOption.WRITE);
}
this.usedSectors.force(0, 2);
this.header.position(0);
int i = this.file.read(this.header, 0L);
if (i != -1) {
if (i != 8192) {
LOGGER.warn("Region file {} has truncated header: {}", p_196950_, i);
}
long j = Files.size(p_196950_);
for (int k = 0; k < 1024; k++) {
int l = this.offsets.get(k);
if (l != 0) {
int i1 = getSectorNumber(l);
int j1 = getNumSectors(l);
if (i1 < 2) {
LOGGER.warn("Region file {} has invalid sector at index: {}; sector {} overlaps with header", p_196950_, k, i1);
this.offsets.put(k, 0);
} else if (j1 == 0) {
LOGGER.warn("Region file {} has an invalid sector at index: {}; size has to be > 0", p_196950_, k);
this.offsets.put(k, 0);
} else if (i1 * 4096L > j) {
LOGGER.warn("Region file {} has an invalid sector at index: {}; sector {} is out of bounds", p_196950_, k, i1);
this.offsets.put(k, 0);
} else {
this.usedSectors.force(i1, j1);
}
}
}
}
}
}
public Path getPath() {
return this.path;
}
private Path getExternalChunkPath(ChunkPos p_63685_) {
String s = "c." + p_63685_.x + "." + p_63685_.z + ".mcc";
return this.externalFileDir.resolve(s);
}
@Nullable
public synchronized DataInputStream getChunkDataInputStream(ChunkPos p_63646_) throws IOException {
int i = this.getOffset(p_63646_);
if (i == 0) {
return null;
} else {
int j = getSectorNumber(i);
int k = getNumSectors(i);
int l = k * 4096;
ByteBuffer bytebuffer = ByteBuffer.allocate(l);
this.file.read(bytebuffer, j * 4096);
bytebuffer.flip();
if (bytebuffer.remaining() < 5) {
LOGGER.error("Chunk {} header is truncated: expected {} but read {}", p_63646_, l, bytebuffer.remaining());
return null;
} else {
int i1 = bytebuffer.getInt();
byte b0 = bytebuffer.get();
if (i1 == 0) {
LOGGER.warn("Chunk {} is allocated, but stream is missing", p_63646_);
return null;
} else {
int j1 = i1 - 1;
if (isExternalStreamChunk(b0)) {
if (j1 != 0) {
LOGGER.warn("Chunk has both internal and external streams");
}
return this.createExternalChunkInputStream(p_63646_, getExternalChunkVersion(b0));
} else if (j1 > bytebuffer.remaining()) {
LOGGER.error("Chunk {} stream is truncated: expected {} but read {}", p_63646_, j1, bytebuffer.remaining());
return null;
} else if (j1 < 0) {
LOGGER.error("Declared size {} of chunk {} is negative", i1, p_63646_);
return null;
} else {
JvmProfiler.INSTANCE.onRegionFileRead(this.info, p_63646_, this.version, j1);
return this.createChunkInputStream(p_63646_, b0, createStream(bytebuffer, j1));
}
}
}
}
}
private static int getTimestamp() {
return (int)(Util.getEpochMillis() / 1000L);
}
private static boolean isExternalStreamChunk(byte p_63639_) {
return (p_63639_ & 128) != 0;
}
private static byte getExternalChunkVersion(byte p_63670_) {
return (byte)(p_63670_ & -129);
}
@Nullable
private DataInputStream createChunkInputStream(ChunkPos p_63651_, byte p_63652_, InputStream p_63653_) throws IOException {
RegionFileVersion regionfileversion = RegionFileVersion.fromId(p_63652_);
if (regionfileversion == RegionFileVersion.VERSION_CUSTOM) {
String s = new DataInputStream(p_63653_).readUTF();
ResourceLocation resourcelocation = ResourceLocation.tryParse(s);
if (resourcelocation != null) {
LOGGER.error("Unrecognized custom compression {}", resourcelocation);
return null;
} else {
LOGGER.error("Invalid custom compression id {}", s);
return null;
}
} else if (regionfileversion == null) {
LOGGER.error("Chunk {} has invalid chunk stream version {}", p_63651_, p_63652_);
return null;
} else {
return new DataInputStream(regionfileversion.wrap(p_63653_));
}
}
@Nullable
private DataInputStream createExternalChunkInputStream(ChunkPos p_63648_, byte p_63649_) throws IOException {
Path path = this.getExternalChunkPath(p_63648_);
if (!Files.isRegularFile(path)) {
LOGGER.error("External chunk path {} is not file", path);
return null;
} else {
return this.createChunkInputStream(p_63648_, p_63649_, Files.newInputStream(path));
}
}
private static ByteArrayInputStream createStream(ByteBuffer p_63660_, int p_63661_) {
return new ByteArrayInputStream(p_63660_.array(), p_63660_.position(), p_63661_);
}
private int packSectorOffset(int p_63643_, int p_63644_) {
return p_63643_ << 8 | p_63644_;
}
private static int getNumSectors(int p_63641_) {
return p_63641_ & 0xFF;
}
private static int getSectorNumber(int p_63672_) {
return p_63672_ >> 8 & 16777215;
}
private static int sizeToSectors(int p_63677_) {
return (p_63677_ + 4096 - 1) / 4096;
}
public boolean doesChunkExist(ChunkPos p_63674_) {
int i = this.getOffset(p_63674_);
if (i == 0) {
return false;
} else {
int j = getSectorNumber(i);
int k = getNumSectors(i);
ByteBuffer bytebuffer = ByteBuffer.allocate(5);
try {
this.file.read(bytebuffer, j * 4096);
bytebuffer.flip();
if (bytebuffer.remaining() != 5) {
return false;
} else {
int l = bytebuffer.getInt();
byte b0 = bytebuffer.get();
if (isExternalStreamChunk(b0)) {
if (!RegionFileVersion.isValidVersion(getExternalChunkVersion(b0))) {
return false;
}
if (!Files.isRegularFile(this.getExternalChunkPath(p_63674_))) {
return false;
}
} else {
if (!RegionFileVersion.isValidVersion(b0)) {
return false;
}
if (l == 0) {
return false;
}
int i1 = l - 1;
if (i1 < 0 || i1 > 4096 * k) {
return false;
}
}
return true;
}
} catch (IOException ioexception) {
return false;
}
}
}
public DataOutputStream getChunkDataOutputStream(ChunkPos p_63679_) throws IOException {
return new DataOutputStream(this.version.wrap(new RegionFile.ChunkBuffer(p_63679_)));
}
public void flush() throws IOException {
this.file.force(true);
}
public void clear(ChunkPos p_156614_) throws IOException {
int i = getOffsetIndex(p_156614_);
int j = this.offsets.get(i);
if (j != 0) {
this.offsets.put(i, 0);
this.timestamps.put(i, getTimestamp());
this.writeHeader();
Files.deleteIfExists(this.getExternalChunkPath(p_156614_));
this.usedSectors.free(getSectorNumber(j), getNumSectors(j));
}
}
protected synchronized void write(ChunkPos p_63655_, ByteBuffer p_63656_) throws IOException {
int i = getOffsetIndex(p_63655_);
int j = this.offsets.get(i);
int k = getSectorNumber(j);
int l = getNumSectors(j);
int i1 = p_63656_.remaining();
int j1 = sizeToSectors(i1);
int k1;
RegionFile.CommitOp regionfile$commitop;
if (j1 >= 256) {
Path path = this.getExternalChunkPath(p_63655_);
LOGGER.warn("Saving oversized chunk {} ({} bytes} to external file {}", p_63655_, i1, path);
j1 = 1;
k1 = this.usedSectors.allocate(j1);
regionfile$commitop = this.writeToExternalFile(path, p_63656_);
ByteBuffer bytebuffer = this.createExternalStub();
this.file.write(bytebuffer, k1 * 4096);
} else {
k1 = this.usedSectors.allocate(j1);
regionfile$commitop = () -> Files.deleteIfExists(this.getExternalChunkPath(p_63655_));
this.file.write(p_63656_, k1 * 4096);
}
this.offsets.put(i, this.packSectorOffset(k1, j1));
this.timestamps.put(i, getTimestamp());
this.writeHeader();
regionfile$commitop.run();
if (k != 0) {
this.usedSectors.free(k, l);
}
}
private ByteBuffer createExternalStub() {
ByteBuffer bytebuffer = ByteBuffer.allocate(5);
bytebuffer.putInt(1);
bytebuffer.put((byte)(this.version.getId() | 128));
bytebuffer.flip();
return bytebuffer;
}
private RegionFile.CommitOp writeToExternalFile(Path p_63663_, ByteBuffer p_63664_) throws IOException {
Path path = Files.createTempFile(this.externalFileDir, "tmp", null);
try (FileChannel filechannel = FileChannel.open(path, StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
p_63664_.position(5);
filechannel.write(p_63664_);
}
return () -> Files.move(path, p_63663_, StandardCopyOption.REPLACE_EXISTING);
}
private void writeHeader() throws IOException {
this.header.position(0);
this.file.write(this.header, 0L);
}
private int getOffset(ChunkPos p_63687_) {
return this.offsets.get(getOffsetIndex(p_63687_));
}
public boolean hasChunk(ChunkPos p_63683_) {
return this.getOffset(p_63683_) != 0;
}
private static int getOffsetIndex(ChunkPos p_63689_) {
return p_63689_.getRegionLocalX() + p_63689_.getRegionLocalZ() * 32;
}
@Override
public void close() throws IOException {
try {
this.padToFullSector();
} finally {
try {
this.file.force(true);
} finally {
this.file.close();
}
}
}
private void padToFullSector() throws IOException {
int i = (int)this.file.size();
int j = sizeToSectors(i) * 4096;
if (i != j) {
ByteBuffer bytebuffer = PADDING_BUFFER.duplicate();
bytebuffer.position(0);
this.file.write(bytebuffer, j - 1);
}
}
class ChunkBuffer extends ByteArrayOutputStream {
private final ChunkPos pos;
public ChunkBuffer(final ChunkPos p_63696_) {
super(8096);
super.write(0);
super.write(0);
super.write(0);
super.write(0);
super.write(RegionFile.this.version.getId());
this.pos = p_63696_;
}
@Override
public void close() throws IOException {
ByteBuffer bytebuffer = ByteBuffer.wrap(this.buf, 0, this.count);
int i = this.count - 5 + 1;
JvmProfiler.INSTANCE.onRegionFileWrite(RegionFile.this.info, this.pos, RegionFile.this.version, i);
bytebuffer.putInt(0, i);
RegionFile.this.write(this.pos, bytebuffer);
}
}
interface CommitOp {
void run() throws IOException;
}
}