/*
* This file is part of Cubic Chunks Mod, licensed under the MIT License (MIT).
*
* Copyright (c) 2015 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.
*/
package cubicchunks.server.chunkio;
import net.minecraft.nbt.CompressedStreamTools;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.math.ChunkPos;
import net.minecraft.world.WorldProvider;
import net.minecraft.world.storage.IThreadedFileIO;
import net.minecraft.world.storage.ThreadedFileIOBase;
import org.apache.logging.log4j.Logger;
import org.mapdb.DB;
import org.mapdb.DBMaker;
import org.mapdb.Serializer;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import cubicchunks.CubicChunks;
import cubicchunks.util.AddressTools;
import cubicchunks.util.CubePos;
import cubicchunks.world.ICubicWorldServer;
import cubicchunks.world.column.Column;
import cubicchunks.world.cube.Cube;
import static cubicchunks.util.AddressTools.getX;
import static cubicchunks.util.AddressTools.getY;
import static cubicchunks.util.AddressTools.getZ;
public class CubeIO implements IThreadedFileIO {
private static final long kB = 1024;
private static final long MB = kB*1024;
private static final Logger LOGGER = CubicChunks.LOGGER;
private static class SaveEntry {
private long address;
private NBTTagCompound nbt;
public SaveEntry(long address, NBTTagCompound nbt) {
this.address = address;
this.nbt = nbt;
}
}
private static DB initializeDBConnection(final File saveFile, final WorldProvider dimension) {
// init database connection
LOGGER.info("Initializing db connection...");
File file = new File(saveFile, String.format("cubes.dim%d.db", dimension.getDimension()));
LOGGER.info("Connected to db at {}", file);
file.getParentFile().mkdirs();
DB db = DBMaker.
fileDB(file).
fileMmapEnable().
allocateStartSize(5*MB).
allocateIncrement(1*MB).
make();
return db;
// NOTE: could set different cache settings
// the default is a hash map cache with 32768 entries
// see: http://www.mapdb.org/features.html
}
private ICubicWorldServer world;
private final DB db;
private ConcurrentMap<Long, byte[]> columns;
private ConcurrentMap<Long, byte[]> cubes;
private ConcurrentMap<ChunkPos, SaveEntry> columnsToSave;
private ConcurrentMap<CubePos, SaveEntry> cubesToSave;
private final Thread theShutdownHook;
public CubeIO(ICubicWorldServer world) {
this.world = world;
this.db = initializeDBConnection(this.world.getSaveHandler().getWorldDirectory(), this.world.getProvider());
//we can't use closeOnJvmShutdown() because Minecraft saves all unsaved things on shutdown
//so the DB will be closed while we are still saving.
//also we need to save the thread into field because in client environment we need to remove the shutdown hook
Runtime.getRuntime().addShutdownHook(theShutdownHook = new Thread() {
public void run() {
try {
ThreadedFileIOBase.getThreadedIOInstance().waitForFinish();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (!CubeIO.this.db.isClosed()) {
CubeIO.this.db.close();
}
}
}
});
this.columns = this.db.hashMap("columns", Serializer.LONG_PACKED, Serializer.BYTE_ARRAY).createOrOpen();
this.cubes = this.db.hashMap("chunks", Serializer.LONG, Serializer.BYTE_ARRAY).createOrOpen();
// init chunk save queue
this.columnsToSave = new ConcurrentHashMap<>();
this.cubesToSave = new ConcurrentHashMap<>();
}
public void flush() {
if (!Runtime.getRuntime().removeShutdownHook(theShutdownHook)) {
err("WARNING!!!");
err("Shutdown hook removing failed!");
err("This may cause memory leak and/or crash");
}
if (columnsToSave.size() != 0 || cubesToSave.size() != 0) {
err("Attempt to flush() CubeIO when there are remaining cubes to save! Saving remaining cubes to avoid corruption");
while (this.writeNextIO()) ;
}
if (!this.db.isClosed()) {
this.db.close();
} else {
err("DB already closed!");
}
}
public Column loadColumn(int chunkX, int chunkZ) throws IOException {
NBTTagCompound nbt;
SaveEntry saveEntry;
if ((saveEntry = columnsToSave.get(new ChunkPos(chunkX, chunkZ))) != null) {
nbt = saveEntry.nbt;
} else {
// does the database have the column?
long address = AddressTools.getAddress(chunkX, chunkZ);
byte[] data = this.columns.get(address);
if (data == null) {
// returning null tells the world to generate a new column
return null;
}
// read the NBT
nbt = CompressedStreamTools.readCompressed(new ByteArrayInputStream(data));
}
// restore the column
return IONbtReader.readColumn(world, chunkX, chunkZ, nbt);
}
public PartialCubeData loadCubeAsyncPart(Column column, int cubeY) throws IOException {
// TODO address is due for refactor
long address = AddressTools.getAddress(column.getX(), cubeY, column.getZ());
NBTTagCompound nbt;
SaveEntry saveEntry;
if ((saveEntry = this.cubesToSave.get(new CubePos(address))) != null) {
nbt = saveEntry.nbt;
} else {
// does the database have the cube?
byte[] data = this.cubes.get(address);
if (data == null) {
return null;
}
nbt = CompressedStreamTools.readCompressed(new ByteArrayInputStream(data));
}
// restore the cube - async part
Cube cube = IONbtReader.readCubeAsyncPart(column, column.getX(), cubeY, column.getZ(), nbt);
return new PartialCubeData(cube, nbt);
}
public void loadCubeSyncPart(PartialCubeData info) {
IONbtReader.readCubeSyncPart(info.cube, world, info.nbt);
}
public void saveColumn(Column column) {
// NOTE: this function blocks the world thread
// make it as fast as possible by offloading processing to the IO thread
// except we have to write the NBT in this thread to avoid problems
// with concurrent access to world data structures
// add the column to the save queue
this.columnsToSave.put(column.getChunkCoordIntPair(), new SaveEntry(AddressTools.getAddress(column.getX(), column.getZ()), IONbtWriter.write(column)));
column.markSaved();
// signal the IO thread to process the save queue
ThreadedFileIOBase.getThreadedIOInstance().queueIO(this);
}
public void saveCube(Cube cube) {
// NOTE: this function blocks the world thread, so make it fast
this.cubesToSave.put(cube.getCoords(), new SaveEntry(cube.getAddress(), IONbtWriter.write(cube)));
cube.markSaved();
// signal the IO thread to process the save queue
ThreadedFileIOBase.getThreadedIOInstance().queueIO(this);
}
@Override
public boolean writeNextIO() {
try {
// NOTE: return true to redo this call (used for batching)
final int ColumnsBatchSize = 25;
final int CubesBatchSize = 250;
int numColumnsSaved = 0;
int numColumnsRemaining = 0;
int numColumnBytesSaved = 0;
int numCubesSaved = 0;
int numCubesRemaining = 0;
int numCubeBytesSaved = 0;
long start = System.currentTimeMillis();
// save a batch of columns
Iterator<SaveEntry> it = columnsToSave.values().iterator();
for (SaveEntry entry; it.hasNext() && numColumnsSaved < ColumnsBatchSize; numColumnsSaved++) {
entry = it.next();
try {
// save the column
byte[] data = IONbtWriter.writeNbtBytes(entry.nbt);
this.columns.put(entry.address, data);
//column can be removed from toSave queue only after writing to disk
//to avoid race conditions
it.remove();
numColumnBytesSaved += data.length;
} catch (Throwable t) {
err(String.format("Unable to write column (%d, %d)", getX(entry.address), getZ(entry.address)), t);
}
}
boolean hasMoreColumns = it.hasNext();
it = cubesToSave.values().iterator();
// save a batch of cubes
for (SaveEntry entry; it.hasNext() && numCubesSaved < CubesBatchSize; numCubesSaved++) {
entry = it.next();
try {
// save the cube
byte[] data = IONbtWriter.writeNbtBytes(entry.nbt);
try {
this.cubes.put(entry.address, data);
} finally {
//cube can be removed from toSave queue only after writing to disk
//to avoid race conditions
it.remove();
}
numCubeBytesSaved += data.length;
} catch (Throwable t) {
err(String.format("Unable to write cube %d, %d, %d", getX(entry.address), getY(entry.address), getZ(entry.address)), t);
}
}
boolean hasMoreCubes = it.hasNext();
numColumnsRemaining = this.columnsToSave.size();
numCubesRemaining = this.cubesToSave.size();
// flush changes to disk
this.db.commit();
long diff = System.currentTimeMillis() - start;
LOGGER.debug("Wrote {} columns ({} remaining) ({}k) and {} cubes ({} remaining) ({}k) in {} ms",
numColumnsSaved, numColumnsRemaining, numColumnBytesSaved/1024,
numCubesSaved, numCubesRemaining, numCubeBytesSaved/1024, diff
);
return hasMoreColumns || hasMoreCubes;
} catch (Throwable t) {
err("Exception occurred when saving cubes", t);
return cubesToSave.size() != 0 || columnsToSave.size() != 0;
}
}
/**
* Method that prints error message even when shutting down (ie. LOGGER is disabled)
*/
private static void err(String message) {
if (!LOGGER.isErrorEnabled()) {
System.err.println(message);
} else {
LOGGER.error(message);
}
}
/**
* Method that prints error message even when shutting down (ie. LOGGER is disabled)
*/
private static void err(String message, Throwable t) {
if (!LOGGER.isErrorEnabled()) {
System.err.println(message);
t.printStackTrace();
} else {
LOGGER.error(message, t);
}
}
/**
* Stores partially read cube, before sync read but after async read
*/
public static class PartialCubeData {
final NBTTagCompound nbt;
final Cube cube;
PartialCubeData(Cube cube, NBTTagCompound nbt) {
this.cube = cube;
this.nbt = nbt;
}
public Cube getCube() {
return cube;
}
}
}