package game.world; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.RandomAccessFile; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.zip.DeflaterOutputStream; import java.util.zip.InflaterInputStream; import game.collect.Lists; import game.collect.Maps; import game.block.Block; import game.entity.Entity; import game.init.BlockRegistry; import game.init.Config; import game.init.EntityRegistry; import game.init.UniverseRegistry; import game.log.Log; import game.nbt.NBTLoader; import game.nbt.NBTTagCompound; import game.nbt.NBTTagList; import game.tileentity.TileEntity; import game.world.Converter.SaveVersion; public class Region { public static class FolderInfo { public final long time; public final long lastPlayed; public final SaveVersion legacy; public final String version; public FolderInfo(long time, long lastPlayed, SaveVersion legacy, String version) { this.time = time; this.lastPlayed = lastPlayed; this.legacy = legacy; this.version = version; } } private static class ChunkBuffer extends ByteArrayOutputStream { public ChunkBuffer() { super(8096); } public byte[] getData() { return this.buf; } } static { Thread thread = new Thread(new Runnable() { public void run() { while(!killed) { processQueue(); } } }, "File IO Thread"); thread.setPriority(1); thread.start(); } private static final Map CACHE = Maps.newHashMap(); private static final List QUEUE = Collections.synchronizedList(Lists.newArrayList()); // public static long lastPlayed; // public static int version; // public static String owner; private static volatile long queued; private static volatile long saved; private static volatile boolean waiting; private static volatile boolean killed; private final int[] timestamps = new int[64]; private final int[] positions = new int[64]; private final int[] sizes = new int[64]; private final File regFile; private final File folder; private final int xPos; private final int zPos; private RandomAccessFile file; private long lastModified; private int offset; private boolean modified; public Region(File dir, int x, int z) { File sdir = new File(dir, getRegionFolder(x << 3, z << 3)); if(!sdir.exists()) sdir.mkdirs(); this.regFile = new File(sdir, getRegionName(x << 3, z << 3)); this.folder = dir; this.xPos = x; this.zPos = z; try { if(this.regFile.exists()) this.lastModified = this.regFile.lastModified(); this.file = new RandomAccessFile(this.regFile, "rw"); this.file.seek(0L); if(this.file.length() < 512L) { for(int i = 0; i < 64; i++) { this.file.writeLong(0L); } } else { for(int i = 0; i < 64; i++) { int end = this.positions[i] = this.file.readUnsignedShort(); end += ((this.sizes[i] = this.file.readUnsignedShort()) + 255) >> 8; this.timestamps[i] = this.file.readInt(); if(end > this.offset) this.offset = end; } } } catch(IOException e) { e.printStackTrace(); } } private void reorganize(boolean reopen) throws IOException { byte[] buffer = new byte[8192]; File tmp = new File(this.folder, this.regFile.getName() + ".tmp"); RandomAccessFile file = new RandomAccessFile(tmp, "rw"); int offset = 0; for(int i = 0; i < 64; i++) { file.writeShort(this.sizes[i] == 0 ? 0 : offset); file.writeShort(this.sizes[i]); file.writeInt(this.timestamps[i]); offset += (this.sizes[i] + 255) >> 8; } this.offset = offset; offset = 0; for(int i = 0; i < 64; i++) { int size = this.sizes[i]; if(size == 0) { this.positions[i] = 0; continue; } this.file.seek(512L + (long)(this.positions[i] << 8)); this.file.read(buffer, 0, size); file.write(buffer, 0, size); this.positions[i] = offset; offset += (size + 255) >> 8; file.seek(512L + (long)(offset << 8)); } file.close(); this.file.close(); tmp.renameTo(this.regFile); this.file = reopen ? new RandomAccessFile(this.regFile, "rw") : null; this.modified = false; } private synchronized byte[] read(int x, int z) { if(this.timestamps[x + z * 8] == 0) return null; FileInputStream in = null; try { // this.file.seek((long)(512 + (x + z * 8) * 8192)); int size = this.sizes[x + z * 8]; // this.file.readShort(); if(size > 8192 /* - 2 */ || size < 0) { Log.JNI.warn("Chunk-Region-Datei " + this.regFile + " hat eine ungültige Größe bei " + x + ", " + z + ", überspringe"); return null; } byte[] data; if(size == 0) { File expand = getExpansionFile(this.folder, this.xPos * 8 + x, this.zPos * 8 + z); if(!expand.exists()) { Log.JNI.warn("Chunk-Erweiterungs-Datei " + expand + " ist nicht vorhanden oder nicht lesbar, überspringe"); return null; } in = new FileInputStream(expand); data = new byte[(int)expand.length()]; int remain = data.length; while(remain > 0) { int n = Math.min(remain, 8192); in.read(data, (data.length - remain), n); remain -= n; } try { if(in != null) { in.close(); } } catch(IOException e) { } in = null; } else { int pos = this.positions[x + z * 8] << 8; if(pos + size > this.file.length() - 512L || pos < 0) { Log.JNI.warn("Chunk-Region-Datei " + this.regFile + " hat eine ungültige Position bei " + x + ", " + z + ", überspringe"); return null; } this.file.seek(512L + (long)pos); data = new byte[size]; // - 2]; this.file.read(data); } return data; } catch(IOException e) { try { if(in != null) { in.close(); } } catch(IOException e1) { } Log.JNI.error(e, "Fehler beim lesen von Chunk-Region-Datei " + this.regFile + " bei " + x + ", " + z + ", überspringe"); return null; } } private synchronized void write(int x, int z, byte[] data, int size) { try { if(((int)this.file.length() - 512) >= 1048576) this.reorganize(true); this.modified = true; // if(size >= 8192) // Log.DATA.info("REG " + x + ", " + z + ": " + size); int pos = 0; if(size /* + 2 */ > 8192) { FileOutputStream out = null; try { out = new FileOutputStream(getExpansionFile(this.folder, this.xPos * 8 + x, this.zPos * 8 + z)); int remain = size; while(remain > 0) { int n = Math.min(remain, 8192); out.write(data, (size - remain), n); remain -= n; } } catch(IOException e) { e.printStackTrace(); } try { if(out != null) { out.close(); } } catch(IOException e) { } data = new byte[0]; size = 0; } else { pos = this.offset; this.offset += (size + 255) >> 8; this.file.seek(512L + (long)(pos << 8)); // (512 + (x + z * 8) * 8192)); // this.file.writeShort(size); this.file.write(data, 0, size); } int time = (int)(System.currentTimeMillis() / 10000L); this.timestamps[x + z * 8] = time; this.positions[x + z * 8] = pos; this.sizes[x + z * 8] = size; this.file.seek((long)((x + z * 8) * 8)); this.file.writeShort(pos); this.file.writeShort(size); this.file.writeInt(time); } catch(IOException e) { e.printStackTrace(); } } public synchronized void close(boolean reorg) throws IOException { // FIX ?? if(this.file != null) { if(reorg && this.modified) this.reorganize(false); else this.file.close(); this.file = null; } } public long getTimestamp(int x, int z) { return (long)this.timestamps[x + z * 8] * 10000L; } public void writeTag(int x, int z, NBTTagCompound tag) throws IOException { ChunkBuffer buf = new ChunkBuffer(); DataOutputStream out = new DataOutputStream(new DeflaterOutputStream(buf)); NBTLoader.write(tag, out); out.close(); this.write(x, z, buf.getData(), buf.size()); } // public NBTTagCompound readTag(int x, int z) throws IOException { // byte[] data = this.read(x, z); // if(data == null) // return null; // return CompressedStreamTools.read(new DataInputStream(new BufferedInputStream(new InflaterInputStream(new ByteArrayInputStream(data))))); // } public File getFile() { return this.regFile; } private static synchronized Region getRegionFile(File dir, int x, int z) { Region reg = CACHE.get(dir + "." + x + "." + z); if(reg != null) return reg; if(CACHE.size() >= 256) clearCache(false); Region nreg = new Region(dir, x, z); CACHE.put(dir + "." + x + "." + z, nreg); return nreg; } private static File getExpansionFile(File dir, int x, int z) { File sdir = new File(dir, getRegionFolder(x, z)); if(!sdir.exists()) sdir.mkdirs(); return new File(sdir, String.format("c.%c%X%c%X.chk", x < 0 ? 'n' : 'p', (x < 0) ? -x : x, z < 0 ? 'n' : 'p', (z < 0) ? -z : z)); } private static synchronized void clearCache(boolean all) { if(all) { for(Region reg : CACHE.values()) { try { reg.close(true); } catch(IOException e) { e.printStackTrace(); } } CACHE.clear(); } else { Iterator iter = CACHE.values().iterator(); for(int z = 0; z < 8 && iter.hasNext(); z++) { try { iter.next().close(true); } catch(IOException e) { e.printStackTrace(); } iter.remove(); } } } public static void finishWrite() { waiting = true; try { while(queued != saved) { Thread.sleep(10L); } } catch(InterruptedException e) { e.printStackTrace(); } waiting = false; clearCache(true); } public static /* synchronized */ NBTTagCompound readChunk(File dir, int x, int z) throws IOException { // return getRegionFile(dir, x >> 3, z >> 3).readTag(x & 7, z & 7); byte[] data = getRegionFile(dir, x >> 3, z >> 3).read(x & 7, z & 7); if(data == null) return null; return NBTLoader.read(new DataInputStream(new BufferedInputStream(new InflaterInputStream(new ByteArrayInputStream(data))))); } public static /* synchronized */ void writeChunk(File dir, int x, int z, NBTTagCompound tag) throws IOException { ChunkBuffer buf = new ChunkBuffer(); DataOutputStream out = new DataOutputStream(new DeflaterOutputStream(buf)); NBTLoader.write(tag, out); out.close(); getRegionFile(dir, x >> 3, z >> 3).write(x & 7, z & 7, buf.getData(), buf.size()); // getRegionFile(dir, x >> 3, z >> 3).writeTag(x & 7, z & 7, tag); } public static String getRegionFolder(int x, int z) { return String.format("%c%03X%c%03X", x < 0 ? 'n' : 'p', ((x < 0) ? -x : x) >> 9, z < 0 ? 'n' : 'p', ((z < 0) ? -z : z) >> 9); } public static String getRegionName(int x, int z) { return String.format("r.%c%X%c%X.rgn", x < 0 ? 'n' : 'p', ((x < 0) ? -x : x) >> 3, z < 0 ? 'n' : 'p', ((z < 0) ? -z : z) >> 3); } public static Chunk readNbt(WorldServer world, int x, int z, NBTTagCompound tag) { // if(!tag.hasKey("Level", 10)) { // Log.error("Chunk-Datei bei " + x + "," + z + " hat keine Level-Daten, überspringe"); // return null; // } // tag = tag.getCompoundTag("Level"); if(!tag.hasKey("Sections", 9)) { Log.JNI.warn("Chunk-Datei bei " + x + "," + z + " hat keine Block-Daten, überspringe"); return null; } Chunk chunk = new Chunk(world, x, z); chunk.setHeights(tag.getIntArray("HeightMap")); chunk.setTerrainPopulated(tag.getBoolean("TerrainPopulated")); chunk.setLightPopulated(tag.getBoolean("LightPopulated")); chunk.setInhabited(tag.getLong("InhabitedTime")); NBTTagList sects = tag.getTagList("Sections", 10); int stor = 32; BlockArray[] sections = new BlockArray[stor]; boolean light = !world.dimension.hasNoLight(); for(int n = 0; n < sects.tagCount(); ++n) { NBTTagCompound sect = sects.getCompoundTagAt(n); int y = sect.getByte("Y"); BlockArray storage = new BlockArray(y << 4, light); byte[] blocks = sect.getByteArray("Blocks"); NibbleArray data = new NibbleArray(sect.getByteArray("Data")); NibbleArray adddata = sect.hasKey("Add", 7) ? new NibbleArray(sect.getByteArray("Add")) : null; char[] seg = new char[blocks.length]; for(int c = 0; c < seg.length; ++c) { int cx = c & 15; int cy = c >> 8 & 15; int cz = c >> 4 & 15; int ca = adddata != null ? adddata.get(cx, cy, cz) : 0; seg[c] = (char)(ca << 12 | (blocks[c] & 255) << 4 | data.get(cx, cy, cz)); } storage.setData(seg); storage.setBlocklight(new NibbleArray(sect.getByteArray("BlockLight"))); if(light) { storage.setSkylight(new NibbleArray(sect.getByteArray("SkyLight"))); } storage.update(); sections[y] = storage; } chunk.setStorage(sections); if(tag.hasKey("Biomes", 7)) { chunk.setBiomes(tag.getByteArray("Biomes")); } NBTTagList entities = tag.getTagList("Entities", 10); if(entities != null) { for(int n = 0; n < entities.tagCount(); ++n) { NBTTagCompound ent = entities.getCompoundTagAt(n); Entity entity = EntityRegistry.createEntityFromNBT(ent, world); chunk.setHasEntities(true); if(entity != null) { chunk.addEntity(entity); Entity rider = entity; for(NBTTagCompound ride = ent; ride.hasKey("Riding", 10); ride = ride.getCompoundTag("Riding")) { Entity pass = EntityRegistry.createEntityFromNBT(ride.getCompoundTag("Riding"), world); if(pass != null) { chunk.addEntity(pass); rider.mountEntity(pass); } rider = pass; } } } } NBTTagList tiles = tag.getTagList("TileEntities", 10); if(tiles != null) { for(int n = 0; n < tiles.tagCount(); ++n) { NBTTagCompound tile = tiles.getCompoundTagAt(n); TileEntity tileentity = TileEntity.createAndLoadEntity(tile); if(tileentity != null) { chunk.addTileEntity(tileentity); } } } if(tag.hasKey("TileTicks", 9)) { NBTTagList ticks = tag.getTagList("TileTicks", 10); if(ticks != null) { int invalid = 0; for(int n = 0; n < ticks.tagCount(); ++n) { NBTTagCompound tick = ticks.getCompoundTagAt(n); Block block; if(tick.hasKey("i", 8)) { block = BlockRegistry.getByIdFallback(tick.getString("i")); } else { block = BlockRegistry.getBlockById(tick.getInteger("i")); } if(block != null) { // FIX world.scheduleBlockUpdate(new BlockPos(tick.getInteger("x"), tick.getInteger("y"), tick.getInteger("z")), block, tick.getInteger("t"), tick.getInteger("p")); } else if(invalid++ < 10) { Log.JNI.warn("Unbekannter Block-Tick in Chunk " + x + "," + z + ": " + (tick.hasKey("i", 8) ? ("'" + tick.getString("i") + "'") : ("#" + tick.getInteger("i")))); } } if(invalid > 10) { Log.JNI.warn((invalid - 10) + " weitere ..."); } } } return chunk; } public static NBTTagCompound writeNbt(WorldServer world, Chunk chunk) { NBTTagCompound tag = new NBTTagCompound(); // tag.setShort("V", (short)Config.PROTOCOL); tag.setLong("LastUpdate", world.getTime()); tag.setIntArray("HeightMap", chunk.getHeights()); tag.setBoolean("TerrainPopulated", chunk.isTerrainPopulated()); tag.setBoolean("LightPopulated", chunk.isLightPopulated()); tag.setLong("InhabitedTime", chunk.getInhabited()); BlockArray[] sections = chunk.getStorage(); NBTTagList sects = new NBTTagList(); boolean light = !world.dimension.hasNoLight(); for(BlockArray storage : sections) { if(storage != null) { NBTTagCompound sect = new NBTTagCompound(); sect.setByte("Y", (byte)(storage.getY() >> 4 & 511)); byte[] blocks = new byte[storage.getData().length]; NibbleArray data = new NibbleArray(); NibbleArray adddata = null; for(int c = 0; c < storage.getData().length; ++c) { char cd = storage.getData()[c]; int cx = c & 15; int cy = c >> 8 & 15; int cz = c >> 4 & 15; if(cd >> 12 != 0) { if(adddata == null) { adddata = new NibbleArray(); } adddata.set(cx, cy, cz, cd >> 12); } blocks[c] = (byte)(cd >> 4 & 255); data.set(cx, cy, cz, cd & 15); } sect.setByteArray("Blocks", blocks); sect.setByteArray("Data", data.getData()); if(adddata != null) { sect.setByteArray("Add", adddata.getData()); } sect.setByteArray("BlockLight", storage.getBlocklight().getData()); if(light) { sect.setByteArray("SkyLight", storage.getSkylight().getData()); } else { sect.setByteArray("SkyLight", new byte[storage.getBlocklight().getData().length]); } sects.appendTag(sect); } } tag.setTag("Sections", sects); tag.setByteArray("Biomes", chunk.getBiomes()); chunk.setHasEntities(false); NBTTagList entities = new NBTTagList(); for(int n = 0; n < chunk.getEntities().length; ++n) { for(Entity entity : chunk.getEntities()[n]) { NBTTagCompound ent = new NBTTagCompound(); if(entity.writeToNBTOptional(ent)) { chunk.setHasEntities(true); entities.appendTag(ent); } } } tag.setTag("Entities", entities); NBTTagList tiles = new NBTTagList(); for(TileEntity tileentity : chunk.getTiles().values()) { NBTTagCompound tile = new NBTTagCompound(); tileentity.writeToNBT(tile); tiles.appendTag(tile); } tag.setTag("TileEntities", tiles); List tics = world.getPendingBlockUpdates(chunk); if(tics != null) { long time = world.getTime(); NBTTagList ticks = new NBTTagList(); for(NextTickListEntry tic : tics) { NBTTagCompound tick = new NBTTagCompound(); String res = BlockRegistry.REGISTRY.getNameForObject(tic.getBlock()); tick.setString("i", res == null ? "" : res.toString()); tick.setInteger("x", tic.position.getX()); tick.setInteger("y", tic.position.getY()); tick.setInteger("z", tic.position.getZ()); tick.setInteger("t", (int)(tic.scheduledTime - time)); tick.setInteger("p", tic.priority); ticks.appendTag(tick); } tag.setTag("TileTicks", ticks); } return tag; } private static void processQueue() { for(int i = 0; i < QUEUE.size(); ++i) { WorldServer loader = QUEUE.get(i); boolean flag = loader.writeNextIO(); if(!flag) { QUEUE.remove(i--); ++saved; } try { Thread.sleep(waiting ? 0L : 10L); } catch(InterruptedException e) { e.printStackTrace(); } } if(QUEUE.isEmpty()) { try { Thread.sleep(25L); } catch(InterruptedException e) { e.printStackTrace(); } } } public static void queueIO(WorldServer loader) { if(!QUEUE.contains(loader)) { ++queued; QUEUE.add(loader); } } public static void killIO() { killed = true; } public static void saveWorldInfo(File worldDir, long time) { NBTTagCompound data = new NBTTagCompound(); data.setLong("Time", time); data.setLong("LastAccess", System.currentTimeMillis()); data.setString("Version", Config.VERSION); NBTTagCompound cfg = new NBTTagCompound(); for(String cvar : Config.VARS.keySet()) { cfg.setString(cvar, Config.VARS.get(cvar).getValue()); // Config.Value value = Config.VARS.get(cvar); // switch(value.getType()) { // case BOOLEAN: // cfg.setString(cvar, "" + value.getBoolean()); // break; // case INTEGER: // cfg.setString(cvar, "" + value.getInt()); // break; // case FLOAT: // cfg.setString(cvar, "" + value.getFloat()); // break; // case STRING: // cfg.setString(cvar, value.getString()); // break; // } } data.setTag("Config", cfg); data.setTag("Universe", UniverseRegistry.saveNbt()); if(worldDir != null) worldDir.mkdirs(); File nfile = new File(worldDir, "level.nbt.tmp"); File lfile = new File(worldDir, "level.nbt"); try { // File ofile = new File(worldDir, "level.nbt_old"); NBTLoader.writeGZip(data, nfile); // if(ofile.exists()) // ofile.delete(); // lfile.renameTo(ofile); if(lfile.exists()) lfile.delete(); nfile.renameTo(lfile); // if(nfile.exists()) // nfile.delete(); } catch(Exception e) { Log.JNI.error(e, "Fehler beim Schreiben von " + nfile); } } public static FolderInfo loadWorldInfo(File worldDir) { Config.clear(); UniverseRegistry.clear(); File file = new File(worldDir, "level.nbt"); if(!file.exists()) file = new File(worldDir, "level.nbt.tmp"); if(file.exists()) { try { NBTTagCompound tag = NBTLoader.readGZip(file); NBTTagCompound cfg = tag.getCompoundTag("Config"); for(String key : cfg.getKeySet()) { Config.set(key, cfg.getString(key), null); } UniverseRegistry.loadNbt(tag.getCompoundTag("Universe")); // tag.getInteger("Version"); long lastPlayed = tag.getLong("LastAccess"); String version = tag.hasKey("Version", 8) ? tag.getString("Version") : null; version = version != null && version.isEmpty() ? null : version; long time = tag.hasKey("Time", 4) ? tag.getLong("Time") : World.START_TIME; return new FolderInfo(time, lastPlayed, null, version); } catch(Exception e) { Log.JNI.error(e, "Fehler beim Lesen von " + file); } } return null; } // public static void reloadWorldInfo(File worldDir) { // File file = new File(worldDir, "level.nbt"); // if(file.exists()) { // Config.clear(); // try { // Config.readFromNbt(NBTLoader.readGZip(file).getCompoundTag("Config"), true); // } // catch(Exception e) { // Log.error("Fehler beim Lesen von " + file, e); // return; // } // } // } }