Created
March 21, 2011 22:05
-
-
Save cgaebel/880317 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| ** 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. | |
| **/ | |
| // 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. | |
| */ | |
| import java.io.*; | |
| import java.io.IOException; | |
| import java.lang.*; | |
| import java.util.ArrayList; | |
| import java.util.zip.*; | |
| public class RegionFile { | |
| 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 ArrayList<Boolean> sectorFree; | |
| private int sizeDelta; | |
| private long lastModified = 0; | |
| public RegionFile(File path) { | |
| offsets = new int[1024]; | |
| fileName = path; | |
| debugln("REGION LOAD " + fileName); | |
| sizeDelta = 0; | |
| try { | |
| if (path.exists()) | |
| lastModified = path.lastModified(); | |
| file = new RandomAccessFile(path, "rw"); | |
| if (file.length() < 4096) { | |
| /* we need to write the chunk offset table */ | |
| for (int i = 0; i < 1024; ++i) | |
| file.writeInt(0); | |
| sizeDelta += 4096; | |
| } | |
| 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() / 4096; | |
| sectorFree = new ArrayList<Boolean>(nSectors); | |
| for (int i = 0; i < nSectors; ++i) { | |
| sectorFree.add(true); | |
| } | |
| sectorFree.set(0, false); // chunk offset table | |
| file.seek(0); | |
| for (int i = 0; i < 1024; ++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); | |
| } | |
| } | |
| } | |
| } catch (IOException e) { | |
| e.printStackTrace(); | |
| } | |
| } | |
| /* the modification date of the region file when it was first opened */ | |
| public long lastModified() { | |
| return lastModified; | |
| } | |
| /* gets how much the region file has grown since it was last checked */ | |
| public synchronized int getSizeDelta() { | |
| int ret = sizeDelta; | |
| sizeDelta = 0; | |
| return ret; | |
| } | |
| // various small debug printing helpers | |
| private void debug(String in) { | |
| //System.out.print(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 * 4096); | |
| int length = file.readInt(); | |
| if (length > 4096 * numSectors) { | |
| debugln("READ", x, z, "invalid length: " + length + " > 4096 * " + numSectors); | |
| return null; | |
| } | |
| byte version = file.readByte(); | |
| if (version == 1) { | |
| 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 == 2) { | |
| 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; | |
| } | |
| } | |
| 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 */ | |
| 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 */ | |
| 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) / 4096 + 1; | |
| if (sectorsNeeded >= 256) // maximum chunk size is 1MB | |
| 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 += 4096 * sectorsNeeded; | |
| write(sectorNumber, data, length); | |
| setOffset(x, z, (sectorNumber << 8) | sectorsNeeded); | |
| } | |
| } | |
| } catch (IOException e) { | |
| e.printStackTrace(); | |
| } | |
| } | |
| /* write a chunk data to the region file at specified sector number */ | |
| private void write(int sectorNumber, byte[] data, int length) throws IOException { | |
| debugln(" " + sectorNumber); | |
| file.seek(sectorNumber * 4096); | |
| file.writeInt(length + 1); // chunk length | |
| file.writeByte(2); // 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) throws IOException { | |
| return offsets[x + z * 32]; | |
| } | |
| 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); | |
| } | |
| public void close() throws IOException { | |
| file.close(); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment