Plex-FAWE/src/main/java/com/sk89q/worldedit/data/McRegionReader.java
2011-11-23 04:48:40 +01:00

198 lines
6.4 KiB
Java

// $Id$
/*
* WorldEdit
* Copyright (C) 2010 sk89q <http://www.sk89q.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*
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.sk89q.worldedit.data;
import java.io.*;
import java.util.zip.*;
import com.sk89q.worldedit.Vector2D;
/**
* Reader for a MCRegion file. This reader works on input streams, meaning
* that it can be used to read files from non-file based sources.
*
* @author sk89q
*/
public class McRegionReader {
protected static final int VERSION_GZIP = 1;
protected static final int VERSION_DEFLATE = 2;
protected static final int SECTOR_BYTES = 4096;
protected static final int SECTOR_INTS = SECTOR_BYTES / 4;
public static final int CHUNK_HEADER_SIZE = 5;
protected ForwardSeekableInputStream stream;
protected DataInputStream dataStream;
protected int offsets[];
/**
* Construct the reader.
*
* @param stream
* @throws DataException
* @throws IOException
*/
public McRegionReader(InputStream stream) throws DataException, IOException {
this.stream = new ForwardSeekableInputStream(stream);
this.dataStream = new DataInputStream(this.stream);
readHeader();
}
/**
* Read the header.
*
* @throws DataException
* @throws IOException
*/
private void readHeader() throws DataException, IOException {
offsets = new int[SECTOR_INTS];
for (int i = 0; i < SECTOR_INTS; ++i) {
int offset = dataStream.readInt();
offsets[i] = offset;
}
}
/**
* Gets the uncompressed data input stream for a chunk.
*
* @param pos
* @return
* @throws IOException
* @throws DataException
*/
public synchronized InputStream getChunkInputStream(Vector2D pos)
throws IOException, DataException {
int x = pos.getBlockX() & 31;
int z = pos.getBlockZ() & 31;
if (x < 0 || x >= 32 || z < 0 || z >= 32) {
throw new DataException("MCRegion file does not contain " + x + "," + z);
}
int offset = getOffset(x, z);
// The chunk hasn't been generated
if (offset == 0) {
return null;
}
int sectorNumber = offset >> 8;
int numSectors = offset & 0xFF;
stream.seek(sectorNumber * SECTOR_BYTES);
int length = dataStream.readInt();
if (length > SECTOR_BYTES * numSectors) {
throw new DataException("MCRegion chunk at "
+ x + "," + z + " has an invalid length of " + length);
}
byte version = dataStream.readByte();
if (version == VERSION_GZIP) {
byte[] data = new byte[length - 1];
if (dataStream.read(data) < length - 1) {
throw new DataException("MCRegion file does not contain "
+ x + "," + z + " in full");
}
return new GZIPInputStream(new ByteArrayInputStream(data));
} else if (version == VERSION_DEFLATE) {
byte[] data = new byte[length - 1];
if (dataStream.read(data) < length - 1) {
throw new DataException("MCRegion file does not contain "
+ x + "," + z + " in full");
}
return new InflaterInputStream(new ByteArrayInputStream(data));
} else {
throw new DataException("MCRegion chunk at "
+ x + "," + z + " has an unsupported version of " + version);
}
}
/**
* Get the offset for a chunk. May return 0 if it doesn't exist.
*
* @param x
* @param z
* @return
*/
private int getOffset(int x, int z) {
return offsets[x + z * 32];
}
/**
* Returns whether the file contains a chunk.
*
* @param x
* @param z
* @return
*/
public boolean hasChunk(int x, int z) {
return getOffset(x, z) != 0;
}
/**
* Close the stream.
*
* @throws IOException
*/
public void close() throws IOException {
stream.close();
}
}