/*
* This file is part of LanternServer, licensed under the MIT License (MIT).
*
* Copyright (c) LanternPowered <https://www.lanternpowered.org>
* Copyright (c) SpongePowered <https://www.spongepowered.org>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the Software), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/*
* 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 to limit the maximum cache
* size (relevant to extremely big worlds on Linux systems with limited
* number of file handles). The region files are postfixed with ".mcr"
* (Minecraft region file) instead of ".data" to differentiate from the
* original McRegion files.
*/
/*
* Copyright (c) 2011-2014 Glowstone - Tad Hardesty
* Copyright (c) 2010-2011 Lightstone - Graham Edgecombe
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.lanternpowered.server.data.io.anvil;
import org.lanternpowered.server.game.Lantern;
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;
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.IOException;
import java.io.RandomAccessFile;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.BitSet;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.GZIPInputStream;
import java.util.zip.InflaterInputStream;
import javax.annotation.Nullable;
public class RegionFile {
private static final Marker REGION_FILE_MARKER = MarkerFactory.getMarker("REGION_FILE");
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;
private static final int CHUNK_HEADER_SIZE = 5;
private static final byte[] EMPTY_SECTOR = new byte[SECTOR_BYTES];
private RandomAccessFile file;
private final int[] offsets;
private final BitSet freeSectors;
private final int regionX;
private final int regionZ;
RegionFile(Path path, int regionX, int regionZ) throws IOException {
this.regionX = regionX;
this.regionZ = regionZ;
this.offsets = new int[SECTOR_INTS];
long lastModified = 0;
if (Files.isRegularFile(path)) {
lastModified = Files.getLastModifiedTime(path).toMillis();
}
this.file = new RandomAccessFile(path.toFile(), "rw");
// seek to the end to prepare size checking
this.file.seek(this.file.length());
// if the file size is under 8KB, grow it (4K chunk offset table, 4K timestamp table)
if (this.file.length() < 2 * SECTOR_BYTES) {
if (lastModified != 0) {
// Only give a warning if the region file existed beforehand
Lantern.getLogger().warn(REGION_FILE_MARKER, "Region \"{}\" under 8K: {} increasing by {}",
path, this.file.length(), 2 * SECTOR_BYTES - this.file.length());
}
for (long i = this.file.length(); i < 2 * SECTOR_BYTES; ++i) {
this.file.write(0);
}
}
// if the file size is not a multiple of 4KB, grow it
if ((this.file.length() & 0xfff) != 0) {
Lantern.getLogger().warn(REGION_FILE_MARKER, "Region \"{}\" not aligned: {} increasing by {}",
path, this.file.length(), SECTOR_BYTES - (this.file.length() & 0xfff));
for (long i = this.file.length() & 0xfff; i < SECTOR_BYTES; ++i) {
this.file.write(0);
}
}
// set up the available sector map
final int nSectors = (int) (this.file.length() / SECTOR_BYTES);
this.freeSectors = new BitSet(nSectors);
this.freeSectors.set(2, nSectors);
// don't set the following sectors
// 0 - chunk offset table
// 1 - for the last modified
// read offsets from offset table
this.file.seek(0);
for (int i = 0; i < SECTOR_INTS; ++i) {
final int offset = this.file.readInt();
this.offsets[i] = offset;
final int startSector = (offset >> 8);
final int numSectors = (offset & 0xff);
if (offset != 0 && startSector >= 0 && startSector + numSectors <= nSectors) {
for (int sectorNum = 0; sectorNum < numSectors; ++sectorNum) {
this.freeSectors.set(startSector + sectorNum, false);
}
} else if (offset != 0) {
Lantern.getLogger().warn(REGION_FILE_MARKER, "Region \"{}\": offsets[{}] = {} -> {},{} does not fit",
path, i, offset, startSector, numSectors);
}
}
// read timestamps from timestamp table -- the timestamps aren't used, so there is no need to read them
// for (int i = 0; i < SECTOR_INTS; ++i) {
// this.file.readInt();
// }
}
/**
* Gets whether there chunk data exists for the chunk at the
* coordinates (relative to the region coordinates).
*
* @param x the x coordinate
* @param z the z coordinate
* @return whether the chunk data exists
*/
public synchronized boolean hasChunk(int x, int z) {
checkBounds(x, z);
try {
final int offset = getOffset(x, z);
if (offset == 0) {
// Does not exist
return false;
}
final int sectorNumber = offset >> 8;
final int numSectors = offset & 0xff;
if (sectorNumber + numSectors > this.freeSectors.size()) {
logWarning();
return false;
}
this.file.seek(sectorNumber * SECTOR_BYTES);
final int length = this.file.readInt();
if (length > SECTOR_BYTES * numSectors) {
logWarning();
return false;
}
final byte version = this.file.readByte();
if (version == VERSION_GZIP || version == VERSION_DEFLATE) {
return true;
}
} catch (IOException ignored) {
}
logWarning();
return false;
}
/*
* gets an (uncompressed) stream representing the chunk data returns null if
* the chunk is not found or an error occurs
*/
@Nullable
public synchronized DataInputStream getChunkDataInputStream(int x, int z) {
checkBounds(x, z);
try {
final int offset = getOffset(x, z);
if (offset == 0) {
// Does not exist
return null;
}
final int sectorNumber = offset >> 8;
final int numSectors = offset & 0xff;
if (sectorNumber + numSectors > this.freeSectors.size()) {
logWarning();
return null;
}
this.file.seek(sectorNumber * SECTOR_BYTES);
final int length = this.file.readInt();
if (length > SECTOR_BYTES * numSectors) {
logWarning();
return null;
}
final byte version = this.file.readByte();
if (version == VERSION_GZIP) {
final byte[] data = new byte[length - 1];
this.file.read(data);
return new DataInputStream(new BufferedInputStream(new GZIPInputStream(new ByteArrayInputStream(data))));
} else if (version == VERSION_DEFLATE) {
final byte[] data = new byte[length - 1];
this.file.read(data);
return new DataInputStream(new BufferedInputStream(new InflaterInputStream(new ByteArrayInputStream(data))));
}
} catch (IOException ignored) {
}
logWarning();
return null;
}
private void logWarning() {
// Use the debug level, there is no need to spam the console with
// corrupt file errors that cannot be fixed
// But might be useful for debugging purposes
Lantern.getLogger().debug(REGION_FILE_MARKER, "An error occurred loading the region file ({};{}), is the file corrupt?",
this.regionX, this.regionZ);
}
public DataOutputStream getChunkDataOutputStream(int x, int z) {
checkBounds(x, z);
return new DataOutputStream(new BufferedOutputStream(new DeflaterOutputStream(
new ChunkBuffer(x, z), new Deflater(Deflater.BEST_SPEED))));
}
/*
* lets chunk writing be multithreaded by not locking the whole file as a
* chunk is serializing -- only writes when serialization is over
*/
private class ChunkBuffer extends ByteArrayOutputStream {
private final int x;
private final int z;
ChunkBuffer(int x, int z) {
super(8096); // initialize to 8KB
this.x = x;
this.z = z;
}
@Override
public void close() throws IOException {
try {
RegionFile.this.write(this.x, this.z, this.buf, this.count);
} finally {
super.close();
}
}
}
// 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) throws IOException {
final int offset = getOffset(x, z);
int sectorNumber = offset >> 8;
final int sectorsAllocated = offset & 0xff;
final 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) {
this.freeSectors.set(sectorNumber + i, true);
}
// scan for a free space large enough to store this chunk
int runStart = this.freeSectors.nextSetBit(0);
int runLength = 0;
if (runStart != -1) {
for (int i = runStart; i < this.freeSectors.size(); ++i) {
if (runLength != 0) {
if (this.freeSectors.get(i)) {
runLength++;
} else {
runLength = 0;
}
} else if (this.freeSectors.get(i)) {
runStart = i;
runLength = 1;
}
if (runLength >= sectorsNeeded) {
break;
}
}
}
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) {
this.freeSectors.set(sectorNumber + i, false);
}
write(sectorNumber, data, length);
} else {
// no free space large enough found -- we need to grow the file
this.file.seek(this.file.length());
sectorNumber = this.freeSectors.size();
int size = this.freeSectors.size();
for (int i = 0; i < sectorsNeeded; ++i) {
this.file.write(EMPTY_SECTOR);
this.freeSectors.set(size++, false);
}
write(sectorNumber, data, length);
setOffset(x, z, (sectorNumber << 8) | sectorsNeeded);
}
}
setTimestamp(x, z, (int) (System.currentTimeMillis() / 1000L));
}
// write a chunk data to the region file at specified sector number
private void write(int sectorNumber, byte[] data, int length) throws IOException {
this.file.seek(sectorNumber * SECTOR_BYTES);
this.file.writeInt(length + 1); // chunk length
this.file.writeByte(VERSION_DEFLATE); // chunk version number
this.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 this.offsets[x + z * 32];
}
private void setOffset(int x, int z, int offset) throws IOException {
this.offsets[x + z * 32] = offset;
this.file.seek((x + z * 32) * 4);
this.file.writeInt(offset);
}
private void setTimestamp(int x, int z, int value) throws IOException {
this.file.seek(SECTOR_BYTES + (x + z * 32) * 4);
this.file.writeInt(value);
}
public void close() throws IOException {
this.file.getChannel().force(true);
this.file.close();
}
}