diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/ChunkIOManager.java b/gameserver/src/main/java/brainwine/gameserver/zone/ChunkIOManager.java deleted file mode 100644 index 9b387d4..0000000 --- a/gameserver/src/main/java/brainwine/gameserver/zone/ChunkIOManager.java +++ /dev/null @@ -1,84 +0,0 @@ -package brainwine.gameserver.zone; - -import java.io.File; -import java.io.IOException; -import java.io.RandomAccessFile; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.msgpack.packer.BufferPacker; -import org.msgpack.unpacker.BufferUnpacker; - -import brainwine.gameserver.msgpack.MessagePackHelper; -import brainwine.gameserver.util.ZipUtils; - -/** - * TODO in the event of a load/save failure, the zone should shut-down. - */ -public class ChunkIOManager { - - public static final int ALLOC_SIZE = 2048; - private static final Logger logger = LogManager.getLogger(); - private final Zone zone; - private RandomAccessFile dataFile; - - public ChunkIOManager(Zone zone) { - this.zone = zone; - File dataDir = new File("zones", zone.getDocumentId()); - dataDir.mkdirs(); - File blockDataFile = new File(dataDir, "blocks"); - - try { - if(!blockDataFile.exists()) { - blockDataFile.createNewFile(); - } - - dataFile = new RandomAccessFile(blockDataFile, "rw"); - } catch(IOException e) { - logger.error("ChunkIOManager construction for zone {} failed", zone.getDocumentId(), e); - } - } - - public void saveModifiedChunks() { - for(Chunk chunk : zone.getChunks()) { - if(chunk.isModified()) { - chunk.setModified(false); - saveChunk(chunk); - } - } - } - - public void saveChunk(Chunk chunk) { - int index = zone.getChunkIndex(chunk.getX(), chunk.getY()); - - try { - BufferPacker packer = MessagePackHelper.createBufferPacker(); - packer.write(chunk); - byte[] bytes = packer.toByteArray(); - packer.close(); - bytes = ZipUtils.deflateBytes(bytes); - dataFile.seek(index * ALLOC_SIZE); - dataFile.writeShort(bytes.length); - dataFile.write(bytes); - } catch(Exception e) { - logger.error("Could not save chunk {} of zone {}", index, zone.getDocumentId(), e); - } - } - - public Chunk loadChunk(int index) { - Chunk chunk = null; - - try { - dataFile.seek(index * ALLOC_SIZE); - byte[] bytes = new byte[dataFile.readShort()]; - dataFile.read(bytes); - BufferUnpacker unpacker = MessagePackHelper.createBufferUnpacker(ZipUtils.inflateBytes(bytes)); - chunk = unpacker.read(Chunk.class); - unpacker.close(); - } catch(Exception e) { - logger.error("Could not load chunk {} of zone {}", index, zone.getDocumentId(), e); - } - - return chunk; - } -} diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/ChunkManager.java b/gameserver/src/main/java/brainwine/gameserver/zone/ChunkManager.java new file mode 100644 index 0000000..c38211a --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/zone/ChunkManager.java @@ -0,0 +1,176 @@ +package brainwine.gameserver.zone; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.file.Files; +import java.util.Arrays; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.msgpack.unpacker.Unpacker; + +import brainwine.gameserver.msgpack.MessagePackHelper; +import brainwine.gameserver.util.ZipUtils; + +public class ChunkManager { + + private static final Logger logger = LogManager.getLogger(); + private static final String headerString = "brainwine blocks file"; + private static final int latestVersion = 1; + private static final int dataOffset = headerString.length() + 4; + private static final int allocSize = 2048; + private static boolean conversionNotified; + private final Zone zone; + private RandomAccessFile file; + + public ChunkManager(Zone zone) { + this.zone = zone; + + try { + if(file == null) { + File blocksFile = new File(zone.getDirectory(), "blocks.dat"); + File legacyBlocksFile = new File(zone.getDirectory(), "blocks"); + + if(!blocksFile.exists()) { + blocksFile.createNewFile(); + } + + file = new RandomAccessFile(blocksFile, "rw"); + + if(file.length() == 0) { + file.writeUTF(headerString); + file.writeInt(latestVersion); + + if(legacyBlocksFile.exists()) { + if(!conversionNotified) { + logger.info("One or more block data files need to be converted. This might take a while ..."); + conversionNotified = true; + } + + convertLegacyBlocksFile(legacyBlocksFile); + } + } else { + if(!file.readUTF().equals(headerString)) { + throw new IOException("Invalid header string"); + } + } + } + } catch(Exception e) { + logger.error("ChunkManager construction for zone {} failed", zone.getDocumentId(), e); + } + } + + private void convertLegacyBlocksFile(File legacyBlocksFile) throws Exception { + byte[] bytes = Files.readAllBytes(legacyBlocksFile.toPath()); + + for(int i = 0; i < bytes.length; i += 2048) { + short length = (short)(((bytes[i] & 0xFF) << 8) + (bytes[i + 1] & 0xFF)); + byte[] chunkBytes = ZipUtils.inflateBytes(Arrays.copyOfRange(bytes, i + 2, i + 2 + length)); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(baos); + Unpacker unpacker = MessagePackHelper.createBufferUnpacker(chunkBytes); + unpacker.readArrayBegin(); + int x = unpacker.readInt(); + int y = unpacker.readInt(); + int width = unpacker.readInt(); + int height = unpacker.readInt(); + dos.writeInt(x); + dos.writeInt(y); + dos.writeInt(width); + dos.writeInt(height); + unpacker.readArrayBegin(); + + for(int j = 0; j < width * height; j++) { + dos.writeInt(unpacker.readInt()); + dos.writeInt(unpacker.readInt()); + dos.writeInt(unpacker.readInt()); + } + + unpacker.close(); + byte[] updatedBytes = ZipUtils.deflateBytes(baos.toByteArray()); + dos.close(); + file.seek(dataOffset + zone.getChunkIndex(x, y) * allocSize); + file.writeShort(updatedBytes.length); + file.write(updatedBytes); + } + } + + public Chunk loadChunk(int index) { + Chunk chunk = null; + DataInputStream dis = null; + + try { + file.seek(dataOffset + index * allocSize); + byte[] bytes = new byte[file.readShort()]; + file.read(bytes); + + dis = new DataInputStream(new ByteArrayInputStream(ZipUtils.inflateBytes(bytes))); + chunk = new Chunk(dis.readInt(), dis.readInt(), dis.readInt(), dis.readInt()); + + for(int i = 0; i < zone.getChunkWidth() * zone.getChunkHeight(); i++) { + chunk.setBlock(i, new Block(dis.readInt(), dis.readInt(), dis.readInt())); + } + } catch(Exception e) { + logger.error("Could not load chunk {} of zone {}", index, zone.getDocumentId(), e); + } finally { + if(dis != null) { + try { + dis.close(); + } catch (IOException e) { + logger.warn("Resource could not be closed", e); + } + } + } + + return chunk; + } + + public void saveModifiedChunks() { + for(Chunk chunk : zone.getChunks()) { + if(chunk.isModified()) { + saveChunk(chunk); + } + } + } + + public void saveChunk(Chunk chunk) { + DataOutputStream dos = null; + int index = zone.getChunkIndex(chunk.getX(), chunk.getY()); + + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(allocSize); + dos = new DataOutputStream(baos); + dos.writeInt(chunk.getX()); + dos.writeInt(chunk.getY()); + dos.writeInt(chunk.getWidth()); + dos.writeInt(chunk.getHeight()); + + for(Block block : chunk.getBlocks()) { + dos.writeInt(block.getBase()); + dos.writeInt(block.getBack()); + dos.writeInt(block.getFront()); + } + + byte[] bytes = ZipUtils.deflateBytes(baos.toByteArray()); + file.seek(dataOffset + index * allocSize); + file.writeShort(bytes.length); + file.write(bytes); + chunk.setModified(false); + } catch(Exception e) { + logger.error("Could not save chunk %s of zone %s", index, zone.getDocumentId(), e); + } finally { + if(dos != null) { + try { + dos.close(); + } catch (IOException e) { + logger.warn("Resource could not be closed", e); + } + } + } + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java b/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java index e1cda93..8073878 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java @@ -77,7 +77,7 @@ public class Zone { private float cloudiness = 5000; private float precipitation = 0; private float acidity = 0; - private final ChunkIOManager chunkManager; + private final ChunkManager chunkManager; private final Queue digQueue = new ArrayDeque<>(); // TODO should be saved private final Set pendingSunlight = new HashSet<>(); private final Map entities = new HashMap<>(); @@ -105,12 +105,12 @@ public class Zone { this.height = height; this.chunkWidth = chunkWidth; this.chunkHeight = chunkHeight; - chunkManager = new ChunkIOManager(this); numChunksWidth = width / chunkWidth; numChunksHeight = height / chunkHeight; surface = new int[width]; sunlight = new int[width]; chunksExplored = new boolean[numChunksWidth * numChunksHeight]; + chunkManager = new ChunkManager(this); Arrays.fill(surface, height); Arrays.fill(sunlight, height); } @@ -839,6 +839,10 @@ public class Zone { return chunksExplored[chunkIndex] = true; } + public File getDirectory() { + return new File("zones", documentId); + } + /** * @return A float between 0 and 1, where 0 is completely unexplored and 1 is fully explored. */