| /* |
| ** 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). |
| * |
| */ |
| |
| /* |
| * 2011 February 20 |
| * |
| * Imported by CJ Kucera into X-Ray, from a blog post by Jens Bergensten |
| * at: http://mojang.com/2011/02/16/minecraft-save-file-format-in-beta-1-3/ |
| */ |
| |
| // Interfaces with region files on the disk |
| |
| /* |
| |
| Region File Format |
| |
| 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. |
| |
| 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. |
| |
| 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. |
| |
| 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. |
| |
| A version of 1 represents a gzipped NBT file. The gzipped data is the chunk |
| length - 1. |
| |
| A version of 2 represents a deflated (zlib compressed) NBT file. The deflated |
| data is the chunk length - 1. |
| |
| */ |
| package com.apocalyptech.minecraft.xray; |
| |
| import java.io.*; |
| import java.util.ArrayList; |
| import java.util.zip.*; |
| |
| public class RegionFile { |
| |
| private static final int VERSION_GZIP = 1; |
| private static final int VERSION_DEFLATE = 2; |
| |
| private static final int SECTOR_BYTES = 4096; |
| private static final int SECTOR_INTS = SECTOR_BYTES / 4; |
| |
| static final int CHUNK_HEADER_SIZE = 5; |
| //private static final byte emptySector[] = new byte[4096]; |
| |
| private final File fileName; |
| private RandomAccessFile file; |
| private final int offsets[]; |
| private final int chunkTimestamps[]; |
| private ArrayList<Boolean> sectorFree; |
| private int sizeDelta; |
| private long lastModified = 0; |
| |
| public RegionFile(File path) { |
| offsets = new int[SECTOR_INTS]; |
| chunkTimestamps = new int[SECTOR_INTS]; |
| |
| fileName = path; |
| debugln("REGION LOAD " + fileName); |
| |
| sizeDelta = 0; |
| |
| try { |
| if (path.exists()) { |
| lastModified = path.lastModified(); |
| } |
| |
| file = new RandomAccessFile(path, "r"); |
| |
| if (file.length() < SECTOR_BYTES) { |
| /* we need to write the chunk offset table */ |
| for (int i = 0; i < SECTOR_INTS; ++i) { |
| file.writeInt(0); |
| } |
| // write another sector for the timestamp info |
| for (int i = 0; i < SECTOR_INTS; ++i) { |
| file.writeInt(0); |
| } |
| |
| sizeDelta += SECTOR_BYTES * 2; |
| } |
| |
| if ((file.length() & 0xfff) != 0) { |
| /* the file size is not a multiple of 4KB, grow it */ |
| for (int i = 0; i < (file.length() & 0xfff); ++i) { |
| file.write((byte) 0); |
| } |
| } |
| |
| /* set up the available sector map */ |
| int nSectors = (int) file.length() / SECTOR_BYTES; |
| sectorFree = new ArrayList<Boolean>(nSectors); |
| |
| for (int i = 0; i < nSectors; ++i) { |
| sectorFree.add(true); |
| } |
| |
| sectorFree.set(0, false); // chunk offset table |
| sectorFree.set(1, false); // for the last modified info |
| |
| file.seek(0); |
| for (int i = 0; i < SECTOR_INTS; ++i) { |
| int offset = file.readInt(); |
| offsets[i] = offset; |
| if (offset != 0 && (offset >> 8) + (offset & 0xFF) <= sectorFree.size()) { |
| for (int sectorNum = 0; sectorNum < (offset & 0xFF); ++sectorNum) { |
| sectorFree.set((offset >> 8) + sectorNum, false); |
| } |
| } |
| } |
| for (int i = 0; i < SECTOR_INTS; ++i) { |
| int lastModValue = file.readInt(); |
| chunkTimestamps[i] = lastModValue; |
| } |
| } catch (IOException e) { |
| e.printStackTrace(); |
| } |
| } |
| |
| /* the modification date of the region file when it was first opened */ |
| public long lastModified() { |
| return lastModified; |
| } |
| |
| // various small debug printing helpers |
| private void debug(String in) { |
| // XRay.logger.debug(in); |
| } |
| |
| private void debugln(String in) { |
| debug(in + "\n"); |
| } |
| |
| private void debug(String mode, int x, int z, String in) { |
| debug("REGION " + mode + " " + fileName.getName() + "[" + x + "," + z + "] = " + in); |
| } |
| |
| /* |
| private void debug(String mode, int x, int z, int count, String in) { |
| debug("REGION " + mode + " " + fileName.getName() + "[" + x + "," + z + "] " + count + "B = " + in); |
| } |
| */ |
| |
| private void debugln(String mode, int x, int z, String in) { |
| debug(mode, x, z, in + "\n"); |
| } |
| |
| /* |
| * gets an (uncompressed) stream representing the chunk data returns null if |
| * the chunk is not found or an error occurs |
| */ |
| public synchronized DataInputStream getChunkDataInputStream(int x, int z) { |
| if (outOfBounds(x, z)) { |
| debugln("READ", x, z, "out of bounds"); |
| return null; |
| } |
| |
| try { |
| int offset = getOffset(x, z); |
| if (offset == 0) { |
| // debugln("READ", x, z, "miss"); |
| return null; |
| } |
| |
| int sectorNumber = offset >> 8; |
| int numSectors = offset & 0xFF; |
| |
| if (sectorNumber + numSectors > sectorFree.size()) { |
| debugln("READ", x, z, "invalid sector"); |
| return null; |
| } |
| |
| file.seek(sectorNumber * SECTOR_BYTES); |
| int length = file.readInt(); |
| |
| if (length > SECTOR_BYTES * numSectors) { |
| debugln("READ", x, z, "invalid length: " + length + " > 4096 * " + numSectors); |
| return null; |
| } |
| |
| byte version = file.readByte(); |
| if (version == VERSION_GZIP) { |
| byte[] data = new byte[length - 1]; |
| file.read(data); |
| DataInputStream ret = new DataInputStream(new GZIPInputStream(new ByteArrayInputStream(data))); |
| // debug("READ", x, z, " = found"); |
| return ret; |
| } else if (version == VERSION_DEFLATE) { |
| byte[] data = new byte[length - 1]; |
| file.read(data); |
| DataInputStream ret = new DataInputStream(new InflaterInputStream(new ByteArrayInputStream(data))); |
| // debug("READ", x, z, " = found"); |
| return ret; |
| } |
| |
| debugln("READ", x, z, "unknown version " + version); |
| return null; |
| } catch (IOException e) { |
| debugln("READ", x, z, "exception"); |
| return null; |
| } |
| } |
| |
| /* Commented for X-Ray because we shouldn't be writing anything |
| public DataOutputStream getChunkDataOutputStream(int x, int z) { |
| if (outOfBounds(x, z)) return null; |
| |
| return new DataOutputStream(new DeflaterOutputStream(new ChunkBuffer(x, z))); |
| } |
| */ |
| |
| /* |
| * lets chunk writing be multithreaded by not locking the whole file as a |
| * chunk is serializing -- only writes when serialization is over |
| */ |
| /* Commented for X-Ray since we don't have any business writing things |
| class ChunkBuffer extends ByteArrayOutputStream { |
| private int x, z; |
| |
| public ChunkBuffer(int x, int z) { |
| super(8096); // initialize to 8KB |
| this.x = x; |
| this.z = z; |
| } |
| |
| public void close() { |
| RegionFile.this.write(x, z, buf, count); |
| } |
| } |
| */ |
| |
| /* write a chunk at (x,z) with length bytes of data to disk */ |
| /* Commented for X-Ray because we don't have any business writing things out |
| protected synchronized void write(int x, int z, byte[] data, int length) { |
| try { |
| 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 |
| debug("SAVE", x, z, length, "rewrite"); |
| 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) { |
| sectorFree.set(sectorNumber + i, true); |
| } |
| |
| // scan for a free space large enough to store this chunk |
| int runStart = sectorFree.indexOf(true); |
| int runLength = 0; |
| if (runStart != -1) { |
| for (int i = runStart; i < sectorFree.size(); ++i) { |
| if (runLength != 0) { |
| if (sectorFree.get(i)) runLength++; |
| else runLength = 0; |
| } else if (sectorFree.get(i)) { |
| runStart = i; |
| runLength = 1; |
| } |
| if (runLength >= sectorsNeeded) { |
| break; |
| } |
| } |
| } |
| |
| if (runLength >= sectorsNeeded) { |
| // we found a free space large enough |
| debug("SAVE", x, z, length, "reuse"); |
| sectorNumber = runStart; |
| setOffset(x, z, (sectorNumber << 8) | sectorsNeeded); |
| for (int i = 0; i < sectorsNeeded; ++i) { |
| sectorFree.set(sectorNumber + i, false); |
| } |
| write(sectorNumber, data, length); |
| } else { |
| // no free space large enough found -- we need to grow the file |
| debug("SAVE", x, z, length, "grow"); |
| file.seek(file.length()); |
| sectorNumber = sectorFree.size(); |
| for (int i = 0; i < sectorsNeeded; ++i) { |
| file.write(emptySector); |
| sectorFree.add(false); |
| } |
| sizeDelta += SECTOR_BYTES * sectorsNeeded; |
| |
| write(sectorNumber, data, length); |
| setOffset(x, z, (sectorNumber << 8) | sectorsNeeded); |
| } |
| } |
| setTimestamp(x, z, (int) (System.currentTimeMillis() / 1000L)); |
| } catch (IOException e) { |
| e.printStackTrace(); |
| } |
| } |
| */ |
| |
| /* write a chunk data to the region file at specified sector number */ |
| /* Commented for X-Ray because we don't have any business writing things |
| private void write(int sectorNumber, byte[] data, int length) throws IOException { |
| debugln(" " + sectorNumber); |
| 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 boolean outOfBounds(int x, int z) { |
| return x < 0 || x >= 32 || z < 0 || z >= 32; |
| } |
| |
| private int getOffset(int x, int z) { |
| return offsets[x + z * 32]; |
| } |
| |
| public boolean hasChunk(int x, int z) { |
| return getOffset(x, z) != 0; |
| } |
| |
| /* Commented for X-Ray |
| private void setOffset(int x, int z, int offset) throws IOException { |
| offsets[x + z * 32] = offset; |
| file.seek((x + z * 32) * 4); |
| file.writeInt(offset); |
| } |
| */ |
| |
| /* Commented for X-Ray |
| private void setTimestamp(int x, int z, int value) throws IOException { |
| chunkTimestamps[x + z * 32] = value; |
| file.seek(SECTOR_BYTES + (x + z * 32) * 4); |
| file.writeInt(value); |
| } |
| */ |
| |
| public void close() throws IOException { |
| file.close(); |
| } |
| } |