blob: 6a062d65f97ee9093622229873275ef3a427b530 [file] [log] [blame] [raw]
/*
* 2011 January 5
*
* The author disclaims copyright to this source code. In place of
* a legal notice, here is a blessing:
*
* May you do good and not evil.
* May you find forgiveness for yourself and forgive others.
* May you share freely, never taking more than you give.
*/
/*
* 2011 February 16
*
* This source code is based on the work of Scaevolus (see notice above).
* It has been slightly modified by Mojang AB (constants instead of magic
* numbers, a chunk timestamp header, and auto-formatted according to our
* formatter template).
*
*/
/*
* Later changes made by the Glowstone project.
*/
package net.glowstone.io.anvil;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.util.BitSet;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.GZIPInputStream;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
import java.util.zip.ZipException;
import net.glowstone.GlowServer;
import net.glowstone.util.config.ServerConfig.Key;
import org.bukkit.Bukkit;
/**
* Interfaces with region files on the disk
*
* Region File Format
*
* <p>Concept: The minimum unit of storage on hard drives is 4KB. 90% of Minecraft
* chunks are smaller than 4KB. 99% are smaller than 8KB. Write a simple
* container to store chunks in single files in runs of 4KB sectors.
*
* <p>Each region file represents a 32x32 group of chunks. The conversion from
* chunk number to region number is floor(coord / 32): a chunk at (30, -3)
* would be in region (0, -1), and one at (70, -30) would be at (3, -1).
* Region files are named "r.x.z.data", where x and z are the region coordinates.
*
* <p>A region file begins with a 4KB header that describes where chunks are stored
* in the file. A 4-byte big-endian integer represents sector offsets and sector
* counts. The chunk offset for a chunk (x, z) begins at byte 4*(x+z*32) in the
* file. The bottom byte of the chunk offset indicates the number of sectors the
* chunk takes up, and the top 3 bytes represent the sector number of the chunk.
* Given a chunk offset o, the chunk data begins at byte 4096*(o/256) and takes up
* at most 4096*(o%256) bytes. A chunk cannot exceed 1MB in size. If a chunk
* offset is 0, the corresponding chunk is not stored in the region file.
*
* <p>Chunk data begins with a 4-byte big-endian integer representing the chunk data
* length in bytes, not counting the length field. The length must be smaller than
* 4096 times the number of sectors. The next byte is a version field, to allow
* backwards-compatible updates to how chunks are encoded.
*
* <p>A version of 1 represents a gzipped NBT file.
* The gzipped data is the chunk length - 1.
*
* <p>A version of 2 represents a deflated (zlib compressed) NBT file.
* The deflated data is the chunk length - 1.
*/
public class RegionFile {
private static final boolean COMPRESSION_ENABLED = ((GlowServer) Bukkit.getServer()).getConfig().getBoolean(Key.REGION_COMPRESSION);
private static final byte VERSION_GZIP = 1;
private static final byte VERSION_DEFLATE = 2;
private static final int SECTOR_BYTES = 4096;
private static final int SECTOR_INTS = SECTOR_BYTES / 4;
private static final int CHUNK_HEADER_SIZE = 5;
private static final byte[] emptySector = new byte[SECTOR_BYTES];
private final int[] offsets;
private final int[] chunkTimestamps;
private RandomAccessFile file;
private BitSet sectorsUsed;
private int totalSectors;
private int sizeDelta;
private long lastModified;
public RegionFile(File path) throws IOException {
offsets = new int[SECTOR_INTS];
chunkTimestamps = new int[SECTOR_INTS];
sizeDelta = 0;
if (path.exists()) {
lastModified = path.lastModified();
}
file = new RandomAccessFile(path, "rw");
int initialLength = (int) file.length();
// if the file size is under 8KB, grow it (4K chunk offset table, 4K timestamp table)
if (lastModified == 0 || initialLength < 4096) {
// fast path for new or region files under 4K
file.write(emptySector);
file.write(emptySector);
sizeDelta = 2 * SECTOR_BYTES;
} else {
// seek to the end to prepare for grows
file.seek(initialLength);
if (initialLength < 2 * SECTOR_BYTES) {
// if the file size is under 8KB, grow it
sizeDelta = 2 * SECTOR_BYTES - initialLength;
GlowServer.logger.warning(
"Region \"" + path + "\" under 8K: " + initialLength + " increasing by " + (
2 * SECTOR_BYTES - initialLength));
for (long i = 0; i < sizeDelta; ++i) {
file.write(0);
}
} else if ((initialLength & (SECTOR_BYTES - 1)) != 0) {
// if the file size is not a multiple of 4KB, grow it
sizeDelta = initialLength & (SECTOR_BYTES - 1);
GlowServer.logger.warning(
"Region \"" + path + "\" not aligned: " + initialLength + " increasing by " + (
SECTOR_BYTES - (initialLength & (SECTOR_BYTES - 1))));
for (long i = 0; i < sizeDelta; ++i) {
file.write(0);
}
}
}
// set up the available sector map
totalSectors = (int) file.length() / SECTOR_BYTES;
sectorsUsed = new BitSet(totalSectors);
sectorsUsed.set(0);
sectorsUsed.set(1);
// read offset table and timestamp tables
file.seek(0);
ByteBuffer header = ByteBuffer.allocate(2 * SECTOR_BYTES);
while (header.hasRemaining()) {
if (file.getChannel().read(header) == -1) {
throw new EOFException();
}
}
header.clear();
// populate the tables
IntBuffer headerAsInts = header.asIntBuffer();
for (int i = 0; i < SECTOR_INTS; ++i) {
int offset = headerAsInts.get();
offsets[i] = offset;
int startSector = offset >> 8;
int numSectors = offset & 255;
if (offset != 0 && startSector >= 0 && startSector + numSectors <= totalSectors) {
for (int sectorNum = 0; sectorNum < numSectors; ++sectorNum) {
sectorsUsed.set(startSector + sectorNum);
}
} else if (offset != 0) {
GlowServer.logger.warning(
"Region \"" + path + "\": offsets[" + i + "] = " + offset + " -> " + startSector
+ "," + numSectors + " does not fit");
}
}
// read timestamps from timestamp table
for (int i = 0; i < SECTOR_INTS; ++i) {
chunkTimestamps[i] = headerAsInts.get();
}
}
/* the modification date of the region file when it was first opened */
public long getLastModified() {
return lastModified;
}
/* gets how much the region file has grown since it was last checked */
public int getSizeDelta() {
int ret = sizeDelta;
sizeDelta = 0;
return ret;
}
/*
* gets an (uncompressed) stream representing the chunk data returns null if
* the chunk is not found or an error occurs
*/
public DataInputStream getChunkDataInputStream(int x, int z) throws IOException {
checkBounds(x, z);
int offset = getOffset(x, z);
if (offset == 0) {
// does not exist
return null;
}
int sectorNumber = offset >> 8;
int numSectors = offset & 0xFF;
if (sectorNumber + numSectors > totalSectors) {
throw new IOException(
"Invalid sector: " + sectorNumber + "+" + numSectors + " > " + totalSectors);
}
file.seek(sectorNumber * SECTOR_BYTES);
int length = file.readInt();
if (length > SECTOR_BYTES * numSectors) {
throw new IOException("Invalid length: " + length + " > " + SECTOR_BYTES * numSectors);
} else if (length <= 0) {
throw new IOException("Invalid length: " + length + " <= 0 ");
}
byte version = file.readByte();
if (version == VERSION_GZIP) {
byte[] data = new byte[length - 1];
file.read(data);
try {
return new DataInputStream(new BufferedInputStream(
new GZIPInputStream(new ByteArrayInputStream(data), 2048)));
} catch (ZipException e) {
if (e.getMessage().equals("Not in GZIP format")) {
GlowServer.logger.info("Incorrect region version, switching to zlib...");
file.seek((sectorNumber * SECTOR_BYTES) + Integer.BYTES);
file.write(VERSION_DEFLATE);
return getZlibInputStream(data);
}
}
} else if (version == VERSION_DEFLATE) {
byte[] data = new byte[length - 1];
file.read(data);
return getZlibInputStream(data);
}
throw new IOException("Unknown version: " + version);
}
private DataInputStream getZlibInputStream(byte[] data) {
return new DataInputStream(new BufferedInputStream(new InflaterInputStream(new ByteArrayInputStream(data), new Inflater(), 2048)));
}
public DataOutputStream getChunkDataOutputStream(int x, int z) {
checkBounds(x, z);
Deflater deflater = new Deflater(COMPRESSION_ENABLED ? Deflater.BEST_SPEED : Deflater.NO_COMPRESSION);
deflater.setStrategy(Deflater.HUFFMAN_ONLY);
DeflaterOutputStream dos = new DeflaterOutputStream(new ChunkBuffer(x, z), deflater, 2048);
return new DataOutputStream(new BufferedOutputStream(dos));
}
/* write a chunk at (x,z) with length bytes of data to disk */
protected void write(int x, int z, byte[] data, int length) throws IOException {
int offset = getOffset(x, z);
int sectorNumber = offset >> 8;
int sectorsAllocated = offset & 0xFF;
int sectorsNeeded = (length + CHUNK_HEADER_SIZE) / SECTOR_BYTES + 1;
// maximum chunk size is 1MB
if (sectorsNeeded >= 256) {
return;
}
if (sectorNumber != 0 && sectorsAllocated == sectorsNeeded) {
/* we can simply overwrite the old sectors */
write(sectorNumber, data, length);
} else {
/* we need to allocate new sectors */
/* mark the sectors previously used for this chunk as free */
for (int i = 0; i < sectorsAllocated; ++i) {
sectorsUsed.clear(sectorNumber + i);
}
/* scan for a free space large enough to store this chunk */
int runStart = 2;
int runLength = 0;
int currentSector = 2;
while (runLength < sectorsNeeded) {
if (sectorsUsed.length() >= currentSector) {
// We reached the end, and we will need to allocate a new sector.
break;
}
int nextSector = sectorsUsed.nextClearBit(currentSector + 1);
if (currentSector + 1 == nextSector) {
runLength++;
} else {
runStart = nextSector;
runLength = 1;
}
currentSector = nextSector;
}
if (runLength >= sectorsNeeded) {
/* we found a free space large enough */
sectorNumber = runStart;
setOffset(x, z, sectorNumber << 8 | sectorsNeeded);
for (int i = 0; i < sectorsNeeded; ++i) {
sectorsUsed.set(sectorNumber + i);
}
write(sectorNumber, data, length);
} else {
/*
* no free space large enough found -- we need to grow the
* file
*/
file.seek(file.length());
sectorNumber = totalSectors;
for (int i = 0; i < sectorsNeeded; ++i) {
file.write(emptySector);
sectorsUsed.set(totalSectors + i);
}
totalSectors += sectorsNeeded;
sizeDelta += SECTOR_BYTES * sectorsNeeded;
write(sectorNumber, data, length);
setOffset(x, z, sectorNumber << 8 | sectorsNeeded);
}
}
setTimestamp(x, z, (int) (System.currentTimeMillis() / 1000L));
//file.getChannel().force(true);
}
/* write a chunk data to the region file at specified sector number */
private void write(int sectorNumber, byte[] data, int length) throws IOException {
file.seek(sectorNumber * SECTOR_BYTES);
file.writeInt(length + 1); // chunk length
file.writeByte(VERSION_DEFLATE); // chunk version number
file.write(data, 0, length); // chunk data
}
/* is this an invalid chunk coordinate? */
private void checkBounds(int x, int z) {
if (x < 0 || x >= 32 || z < 0 || z >= 32) {
throw new IllegalArgumentException("Chunk out of bounds: (" + x + ", " + z + ")");
}
}
private int getOffset(int x, int z) {
return offsets[x + (z << 5)];
}
public boolean hasChunk(int x, int z) {
return getOffset(x, z) != 0;
}
private void setOffset(int x, int z, int offset) throws IOException {
offsets[x + (z << 5)] = offset;
file.seek((x + (z << 5)) << 2);
file.writeInt(offset);
}
private void setTimestamp(int x, int z, int value) throws IOException {
chunkTimestamps[x + (z << 5)] = value;
file.seek(SECTOR_BYTES + ((x + (z << 5)) << 2));
file.writeInt(value);
}
public void close() throws IOException {
file.getChannel().force(true);
file.close();
}
/*
* lets chunk writing be multithreaded by not locking the whole file as a
* chunk is serializing -- only writes when serialization is over
*/
class ChunkBuffer extends ByteArrayOutputStream {
private final int x, z;
public ChunkBuffer(int x, int z) {
super(SECTOR_BYTES); // initialize to 4KB
this.x = x;
this.z = z;
}
@Override
public void close() throws IOException {
RegionFile.this.write(x, z, buf, count);
}
}
}