/*
* This file is part of SpoutcraftPlugin.
*
* Copyright (c) 2011 SpoutcraftDev <http://spoutcraft.org//>
* SpoutcraftPlugin is licensed under the GNU Lesser General Public License.
*
* SpoutcraftPlugin is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* SpoutcraftPlugin 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.getspout.spoutapi.chunkstore;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OptionalDataException;
import java.io.Serializable;
import java.util.HashMap;
import java.util.UUID;
import gnu.trove.iterator.TIntObjectIterator;
import org.getspout.spoutapi.Spout;
import org.getspout.spoutapi.SpoutWorld;
import org.getspout.spoutapi.chunkstore.Utils.SerializedData;
import org.getspout.spoutapi.inventory.ItemMap;
import org.getspout.spoutapi.inventory.MaterialManager;
import org.getspout.spoutapi.util.map.TByteShortByteKeyedMap;
import org.getspout.spoutapi.util.map.TByteShortByteKeyedObjectHashMap;
public class ChunkMetaData implements Serializable {
// Field serialization only
private static final long serialVersionUID = 3L;
// This data is saved. This means data can handle different map heights
// Changes may be needed to the positionToKey method
private int cx;
private int cz;
private UUID worldUid;
// Storage for objects saved to this chunk
private HashMap<String, Serializable> chunkData;
// Storage for custom block IDs
private short[] customBlockIds = null;
// Storage for custom block rotations
private byte[] customBlockData = null;
// Storage for local block data
private TByteShortByteKeyedObjectHashMap<HashMap<String, Serializable>> blockData;
private static final int CURRENT_VERSION = 4;
private static final int MAGIC_NUMBER = 0xEA5EDEBB;
transient private boolean dirty = false;
// Quais-final, need to be set in serialization
transient private int worldHeight;
transient private int worldHeightMinusOne;
transient private int xBitShifts;
transient private int zBitShifts;
transient private ItemMap worldItemMap;
transient private ItemMap serverItemMap;
transient private boolean conversionNeeded;
ChunkMetaData(UUID worldId, ItemMap worldItemMap, int cx, int cz) {
blockData = new TByteShortByteKeyedObjectHashMap<HashMap<String, Serializable>>(100);
chunkData = new HashMap<String, Serializable>();
this.cx = cx;
this.cz = cz;
this.worldUid = worldId;
SpoutWorld world = Spout.getServer().getWorld(this.worldUid);
this.worldHeight = world != null ? world.getMaxHeight() : 128;
this.xBitShifts = world != null ? world.getXBitShifts() : 11;
this.zBitShifts = world != null ? world.getZBitShifts() : 7;
worldHeightMinusOne = worldHeight - 1;
this.worldItemMap = worldItemMap;
this.serverItemMap = ItemMap.getRootMap();
conversionNeeded = false;
}
/**
* True if this chunk's data has been altered and needs to be serialized into storage
* @return dirty
*/
public boolean isDirty() {
return dirty;
}
/**
* Sets this chunk's data dirty flag
* @param dirty
*/
public void setDirty(boolean dirty) {
this.dirty = dirty;
}
public int getChunkX() {
return cx;
}
public int getChunkZ() {
return cz;
}
public UUID getWorldUID() {
return worldUid;
}
/**
* Removes the data associated with the id at this chunk
* @param id of data
* @return data removed
*/
public Serializable removeChunkData(String id) {
Serializable serial = chunkData.remove(id);
if (serial != null) {
dirty = true;
return serial;
}
return null;
}
/**
* Gets the data associated with the id at this chunk.
* <p/>
* If the data is still in a serialized form, this will deserialize it.
* @param id of data
* @return data at the given id, or null if none found
*/
public Serializable getChunkData(String id) {
Serializable serial = chunkData.get(id);
if (serial != null && serial instanceof SerializedData) {
try {
serial = Utils.deserializeRaw(((SerializedData) serial).serialData);
chunkData.put(id, serial);
} catch (ClassNotFoundException e) {
return null;
} catch (IOException e) {
return null;
}
}
return serial;
}
public Serializable putChunkData(String id, Serializable o) {
Serializable serial = chunkData.put(id, o);
dirty = true;
return serial;
}
/**
* Returns the array that is backing the block id data for this chunk.
* <p/>
* If the contents of the array are altered, setDirty(true) must be used so that the updated contents will be saved.
* Alternatively, use setCustomBlockIds(array) when you are finished manipulating the array and it will set the dirty flag for you.
* @return array of block id data for this chunk
*/
public short[] getCustomBlockIds() {
return customBlockIds;
}
/**
* Sets the array that is used for the block id data for this chunk.
* <p/>
* This array will <b>override</b> any existing data, and wipe it out, so be sure this is what you intend to do.
* @param ids to set
*/
public void setCustomBlockIds(short[] ids) {
customBlockIds = ids;
setDirty(true);
}
public byte[] getCustomBlockData() {
return customBlockData;
}
public void setCustomBlockData(byte[] rots) {
customBlockData = rots;
setDirty(true);
}
public Serializable removeBlockData(String id, int x, int y, int z) {
if (id.equals(MaterialManager.blockIdString)) {
if (customBlockIds != null) {
int key = ((x & 0xF) << xBitShifts) | ((z & 0xF) << zBitShifts) | (y & worldHeightMinusOne);
short old = customBlockIds[key];
if (old != 0) {
dirty = true;
customBlockIds[key] = 0;
}
return old;
}
} else {
HashMap<String, Serializable> localBlockData = blockData.get(x, y, z);
if (localBlockData != null) {
Serializable old = localBlockData.remove(id);
if (old != null) {
dirty = true;
if (localBlockData.size() == 0) {
blockData.remove(x, y, z);
}
}
return old;
}
}
return null;
}
public Serializable getBlockData(String id, int x, int y, int z) {
if (id.equals(MaterialManager.blockIdString)) {
if (customBlockIds != null) {
int key = ((x & 0xF) << xBitShifts) | ((z & 0xF) << zBitShifts) | (y & worldHeightMinusOne);
return customBlockIds[key];
}
} else {
HashMap<String, Serializable> localBlockData = blockData.get(x, y, z);
if (localBlockData != null) {
Serializable serial = localBlockData.get(id);
// Check if we need to deserialize it
if (serial != null && serial instanceof SerializedData) {
try {
serial = Utils.deserializeRaw(((SerializedData) serial).serialData);
localBlockData.put(id, serial);
} catch (ClassNotFoundException e) {
} catch (IOException e) {
}
}
return serial;
}
}
return null;
}
public Serializable putBlockData(String id, int x, int y, int z, Serializable o) {
if (id.equals(MaterialManager.blockIdString)) {
if (customBlockIds == null) {
customBlockIds = new short[16 * 16 * worldHeight];
}
int key = ((x & 0xF) << xBitShifts) | ((z & 0xF) << zBitShifts) | (y & worldHeightMinusOne);
customBlockIds[key] = ((Integer) o).shortValue();
dirty = true;
} else {
HashMap<String, Serializable> localBlockData = blockData.get(x, y, z);
if (localBlockData == null) {
localBlockData = new HashMap<String, Serializable>();
blockData.put(x, y, z, localBlockData);
}
localBlockData.put(id, o);
dirty = true;
}
return o;
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.writeInt(MAGIC_NUMBER);
out.writeInt(CURRENT_VERSION);
out.writeLong(worldUid.getLeastSignificantBits());
out.writeLong(worldUid.getMostSignificantBits());
out.writeInt(cx);
out.writeInt(cz);
if (customBlockIds != null) {
out.writeBoolean(true);
for (int i = 0; i < (16 * 16 * worldHeight); i++) {
Integer worldId = worldItemMap.convertFrom(this.serverItemMap, customBlockIds[i]);
if (worldId == null) {
worldId = 0;
}
out.writeShort(worldId);
}
worldItemMap.save();
serverItemMap.save();
} else {
out.writeBoolean(false);
}
out.writeInt(blockData != null ? blockData.size() : 0);
if (blockData != null) {
TIntObjectIterator<HashMap<String, Serializable>> i = blockData.iterator();
while (i.hasNext()) {
i.advance();
int key = i.key();
byte x = TByteShortByteKeyedMap.getXFromKey(key);
short y = TByteShortByteKeyedMap.getYFromKey(key);
byte z = TByteShortByteKeyedMap.getZFromKey(key);
out.writeByte(x);
out.writeShort(y);
out.writeByte(z);
writeMap(out, i.value());
}
}
if (customBlockData != null) {
out.writeBoolean(true);
out.write(customBlockData);
} else {
out.writeBoolean(false);
}
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
blockData = new TByteShortByteKeyedObjectHashMap<HashMap<String, Serializable>>(100);
chunkData = new HashMap<String, Serializable>();
int fileVersionNumber; // Can be used to determine the format of the file
long lsb = in.readLong();
if (((int) (lsb >> 32)) == MAGIC_NUMBER) {
fileVersionNumber = (int) lsb;
lsb = in.readLong();
} else {
fileVersionNumber = 0;
}
long msb = in.readLong();
worldUid = new UUID(msb, lsb);
cx = in.readInt();
cz = in.readInt();
boolean customBlockIdsExist = in.readBoolean();
// Constructor is not invoked, need to set these fields
SpoutWorld world = Spout.getServer().getWorld(this.worldUid);
this.worldHeight = world.getMaxHeight();
this.xBitShifts = world.getXBitShifts();
this.zBitShifts = world.getZBitShifts();
worldHeightMinusOne = worldHeight - 1;
if (customBlockIdsExist) {
if (fileVersionNumber >= 2) {
conversionNeeded = true;
}
customBlockIds = new short[16 * 16 * worldHeight];
int size = (16 * 16 * worldHeight);
if (fileVersionNumber < 3) {
size = 16 * 16 * 128;
}
for (int i = 0; i < size; i++) {
if (fileVersionNumber > 2) {
customBlockIds[i] = in.readShort();
} else {
int oldX = (i >> 11) & 0xF;
int oldY = i & 0x7F;
int oldZ = (i >> 7) & 0xF;
int newKey = ((oldX & 0xF) << 12) | ((oldZ & 0xF) << 8) | (oldY & 0xFF);
customBlockIds[newKey] = in.readShort();
}
}
}
int size = in.readInt();
for (int i = 0; i < size; i++) {
int x = in.readByte();
int y = in.readShort();
int z = in.readByte();
HashMap<String, Serializable> map = readMap(in);
blockData.put(x, y, z, map);
}
if (fileVersionNumber >= 4) {
boolean hasRotations = in.readBoolean();
customBlockData = new byte[16 * 16 * worldHeight];
if (hasRotations) {
in.readFully(customBlockData);
}
}
if (fileVersionNumber < CURRENT_VERSION) {
dirty = true;
}
}
public void setWorldItemMap(ItemMap worldItemMap) {
this.serverItemMap = ItemMap.getRootMap();
this.worldItemMap = worldItemMap;
if (conversionNeeded) {
convertIds(worldItemMap);
}
}
private void convertIds(ItemMap worldItemMap) {
int length = customBlockIds.length;
for (int i = 0; i < length; i++) {
Integer globalId = worldItemMap.convertTo(serverItemMap, customBlockIds[i]);
if (globalId == null) {
System.out.println("Custom ID " + customBlockIds[i] + " does not exist in custom item map, replacing with 0");
globalId = 0;
}
customBlockIds[i] = (short) (int) globalId;
}
conversionNeeded = false;
}
private void writeMap(ObjectOutputStream out, HashMap<String, Serializable> map) throws IOException {
if (map == null) {
out.writeBoolean(false);
return;
} else {
out.writeBoolean(true);
}
out.writeObject(map);
}
@SuppressWarnings("unchecked")
private HashMap<String, Serializable> readMap(ObjectInputStream in) throws IOException {
if (!in.readBoolean()) {
return null;
}
HashMap<String, Serializable> map = new HashMap<String, Serializable>();
try {
map = (HashMap<String, Serializable>) in.readObject();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (OptionalDataException ode) {
if (ode.eof) {
throw new RuntimeException("EOF reached", ode);
} else {
throw new RuntimeException("Primitive data in object stream of length " + ode.length, ode);
}
}
return map;
}
}