/* * To change this template, choose Tools | Templates * and open the template in the editor. */ package org.pepsoft.worldpainter.exporting; import org.jnbt.CompoundTag; import org.jnbt.NBTInputStream; import org.jnbt.NBTOutputStream; import org.jnbt.Tag; import org.pepsoft.minecraft.*; import org.pepsoft.util.jobqueue.HashList; import org.pepsoft.worldpainter.layers.ReadOnly; import javax.vecmath.Point3i; import java.awt.*; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.*; import java.util.List; import static org.pepsoft.minecraft.Block.BLOCK_TYPE_NAMES; import static org.pepsoft.minecraft.Constants.BLK_AIR; import static org.pepsoft.minecraft.Constants.SUPPORTED_VERSION_1; import static org.pepsoft.worldpainter.Constants.*; import org.pepsoft.worldpainter.Dimension; /** * * @author pepijn */ public class MinecraftWorldImpl implements MinecraftWorld { public MinecraftWorldImpl(File worldDir, int dimension, int maxHeight, int version, boolean readOnly, int cacheSize) { this.version = version; this.readOnly = readOnly; this.maxHeight = maxHeight; switch (dimension) { case DIM_NORMAL: dimensionDir = worldDir; break; case DIM_NETHER: dimensionDir = new File(worldDir, "DIM-1"); break; case DIM_END: dimensionDir = new File(worldDir, "DIM1"); break; default: throw new IllegalArgumentException("Dimension " + dimension + " not supported"); } if (! worldDir.isDirectory()) { throw new IllegalArgumentException(worldDir + " does not exist or is not a directory"); } regionDir = new File(dimensionDir, "region"); this.cacheSize = cacheSize; cache = new HashMap<>(cacheSize); lruList = new HashList<>(cacheSize); dirtyChunks = new HashSet<>(cacheSize); this.dimension = null; honourReadOnlyChunks = false; } public MinecraftWorldImpl(File worldDir, Dimension dimension, int version) { this(worldDir, dimension, version, false, false, -1); } public MinecraftWorldImpl(File worldDir, Dimension dimension, int version, boolean readOnly, boolean honourReadOnlyChunks, int cacheSize) { this.version = version; this.dimension = dimension; this.readOnly = readOnly; this.honourReadOnlyChunks = honourReadOnlyChunks; if ((dimension.getWorld().getTilesToExport() != null) && dimension.getWorld().getDimensionsToExport().contains(dimension.getDim())) { lowestX = Integer.MAX_VALUE; highestX = Integer.MIN_VALUE; lowestZ = Integer.MAX_VALUE; highestZ = Integer.MIN_VALUE; for (Point tile: dimension.getWorld().getTilesToExport()) { int chunkX = tile.x << 3; int chunkZ = tile.y << 3; if (chunkX < lowestX) { lowestX = chunkX; } if (chunkX + 7 > highestX) { highestX = chunkX + 7; } if (chunkZ < lowestZ) { lowestZ = chunkZ; } if (chunkX + 7 > highestZ) { highestZ = chunkZ + 7; } } } else { Point northEastChunk = new Point((dimension.getHighestX() + 1) * TILE_SIZE - 1, dimension.getLowestY() * TILE_SIZE); Point southWestChunk = new Point(dimension.getLowestX() * TILE_SIZE, (dimension.getHighestY() + 1) * TILE_SIZE - 1); lowestX = southWestChunk.x >> 4; highestX = northEastChunk.x >> 4; lowestZ = northEastChunk.y >> 4; highestZ = southWestChunk.y >> 4; } maxHeight = dimension.getMaxHeight(); switch (dimension.getDim()) { case DIM_NORMAL: dimensionDir = worldDir; break; case DIM_NETHER: dimensionDir = new File(worldDir, "DIM-1"); break; case DIM_END: dimensionDir = new File(worldDir, "DIM1"); break; default: throw new IllegalArgumentException("Dimension " + dimension.getDim() + " not supported"); } if (! worldDir.isDirectory()) { throw new IllegalArgumentException(worldDir + " does not exist or is not a directory"); } regionDir = new File(dimensionDir, "region"); this.cacheSize = (cacheSize >= 0) ? cacheSize : ((highestX - lowestX + 1 + 2 * dimension.getBorderSize()) * 2 + 50); cache = new HashMap<>(cacheSize); lruList = new HashList<>(cacheSize); dirtyChunks = new HashSet<>(cacheSize); } public int getHighestX() { return highestX; } public int getHighestZ() { return highestZ; } public int getLowestX() { return lowestX; } public int getLowestZ() { return lowestZ; } @Override public int getMaxHeight() { return maxHeight; } @Override public int getBlockTypeAt(int x, int y, int height) { Chunk chunk = getChunk(x >> 4, y >> 4); if (chunk != null) { return chunk.getBlockType(x & 0xf, height, y & 0xf); } else { return BLK_AIR; } } @Override public void setBlockTypeAt(int x, int y, int height, int blockType) { Chunk chunk = getChunkForEditing(x >> 4, y >> 4); if (chunk != null) { chunk.setBlockType(x & 0xf, height, y & 0xf, blockType); } } @Override public int getDataAt(int x, int y, int height) { Chunk chunk = getChunk(x >> 4, y >> 4); if (chunk != null) { return chunk.getDataValue(x & 0xf, height, y & 0xf); } else { return 0; } } @Override public void setDataAt(int x, int y, int height, int data) { Chunk chunk = getChunkForEditing(x >> 4, y >> 4); if (chunk != null) { chunk.setDataValue(x & 0xf, height, y & 0xf, data); } } @Override public Material getMaterialAt(int x, int y, int height) { Chunk chunk = getChunk(x >> 4, y >> 4); if (chunk != null) { return chunk.getMaterial(x & 0xf, height, y & 0xf); } else { return Material.AIR; } } @Override public void setMaterialAt(int x, int y, int height, Material material) { Chunk chunk = getChunkForEditing(x >> 4, y >> 4); if (chunk != null) { chunk.setMaterial(x & 0xf, height, y & 0xf, material); } } @Override public int getBlockLightLevel(int x, int y, int height) { Chunk chunk = getChunk(x >> 4, y >> 4); if (chunk != null) { return chunk.getBlockLightLevel(x & 0xf, height, y & 0xf); } else { return 0; } } @Override public void setBlockLightLevel(int x, int y, int height, int blockLightLevel) { Chunk chunk = getChunkForEditing(x >> 4, y >> 4); if (chunk != null) { chunk.setBlockLightLevel(x & 0xf, height, y & 0xf, blockLightLevel); } } @Override public int getSkyLightLevel(int x, int y, int height) { Chunk chunk = getChunk(x >> 4, y >> 4); if (chunk != null) { return chunk.getSkyLightLevel(x & 0xf, height, y & 0xf); } else { return 15; } } @Override public void setSkyLightLevel(int x, int y, int height, int skyLightLevel) { Chunk chunk = getChunkForEditing(x >> 4, y >> 4); if (chunk != null) { chunk.setSkyLightLevel(x & 0xf, height, y & 0xf, skyLightLevel); } } @Override public boolean isChunkPresent(int x, int z) { if ((x == cachedX) && (z == cachedZ)) { return true; } Chunk chunk = cache.get(new Point(x, z)); if (chunk == null) { try { RegionFile regionFile = getRegionFile(new Point(x >> 5, z >> 5)); return (regionFile != null) && regionFile.containsChunk(x & 31, z & 31); } catch (IOException e) { throw new RuntimeException("I/O error while trying to determine existence of chunk " + x + "," + z, e); } } else { return chunk != NON_EXISTANT_CHUNK; } } @Override public synchronized void addChunk(Chunk chunk) { if (readOnly) { throw new IllegalStateException("Read only"); } if (chunkExists(chunk.getCoords())) { throw new IllegalStateException("Existing chunk at " + chunk.getxPos() + ", " + chunk.getzPos()); } maintainCache(); int chunkX = chunk.getxPos(), chunkZ = chunk.getzPos(); Point coords = new Point(chunkX, chunkZ); cache.put(coords, chunk); dirtyChunks.add(coords); lruList.addToEnd(coords); if ((chunkX == cachedX) && (chunkZ == cachedZ)) { cachedChunk = chunk; cachedForEditing = true; } if (chunkX < lowestX) { lowestX = chunkX; } if (chunkX > highestX) { highestX = chunkX; } if (chunkZ < lowestZ) { lowestZ = chunkZ; } if (chunkZ > highestZ) { highestZ = chunkZ; } } @Override public int getHighestNonAirBlock(int x, int y) { Chunk chunk = getChunk(x >> 4, y >> 4); if (chunk != null) { return chunk.getHighestNonAirBlock(x & 0xf, y & 0xf); } else { return -1; } } public synchronized void replaceChunk(Chunk chunk) { if (readOnly) { throw new IllegalStateException("Read only"); } if (! chunkExists(chunk.getCoords())) { throw new IllegalStateException("No existing chunk at " + chunk.getxPos() + ", " + chunk.getzPos()); } maintainCache(); int chunkX = chunk.getxPos(), chunkZ = chunk.getzPos(); Point coords = new Point(chunkX, chunkZ); cache.put(coords, chunk); dirtyChunks.add(coords); lruList.addToEnd(coords); if ((chunkX == cachedX) && (chunkZ == cachedZ)) { cachedChunk = chunk; cachedForEditing = true; } } public boolean chunkExists(int x, int z) { return chunkExists(new Point(x, z)); } public synchronized boolean chunkExists(Point coords) { if ((coords.x == cachedX) && (coords.y == cachedZ)) { return cachedChunk != null; } Chunk chunkFromCache = cache.get(coords); if (chunkFromCache == NON_EXISTANT_CHUNK) { return false; } else if (chunkFromCache != null) { return true; } try { int regionX = coords.x >> 5; int regionZ = coords.y >> 5; Point regionCoords = new Point(regionX, regionZ); RegionFile regionFile = getRegionFile(regionCoords); if (regionFile != null) { return regionFile.containsChunk(coords.x & 31, coords.y & 31); } else { return false; } } catch (IOException e) { throw new RuntimeException("I/O error while determining existance of chunk " + coords.x + ", " + coords.y, e); } } private RegionFile getRegionFile(Point regionCoords) throws IOException { RegionFile regionFile = regionFiles.get(regionCoords); if (regionFile == null) { regionFile = openRegionFile(regionCoords); if (regionFile != null) { regionFiles.put(regionCoords, regionFile); } } return regionFile; } private RegionFile openRegionFile(Point regionCoords) throws IOException { File file = new File(regionDir, "r." + regionCoords.x + "." + regionCoords.y + ((version == SUPPORTED_VERSION_1) ? ".mcr" : ".mca")); return file.exists() ? new RegionFile(file) : null; } @Override public synchronized Chunk getChunk(int x, int z) { if ((x == cachedX) && (z == cachedZ)) { // fastCacheHits++; return cachedChunk; } // long start = System.currentTimeMillis(), loadingTime = 0; cachedX = x; cachedZ = z; cachedForEditing = false; Point coords = new Point(x, z); cachedChunk = cache.get(coords); if (cachedChunk == null) { // loadingTime = System.currentTimeMillis(); cachedChunk = loadChunk(x, z); // loadingTime -= System.currentTimeMillis(); maintainCache(); if (cachedChunk != null) { cache.put(coords, cachedChunk); } else { cache.put(coords, NON_EXISTANT_CHUNK); } } else { // cacheHits++; if (cachedChunk == NON_EXISTANT_CHUNK) { cachedChunk = null; } } lruList.addToEnd(coords); // timeSpentGetting += (System.currentTimeMillis() - start) - loadingTime; return cachedChunk; } @Override public synchronized Chunk getChunkForEditing(int x, int z) { if (readOnly) { throw new IllegalStateException("Read only"); } if ((x == cachedX) && (z == cachedZ)) { if ((! cachedForEditing) && (cachedChunk != null) && (! cachedChunk.isReadOnly())) { dirtyChunks.add(new Point(x, z)); cachedForEditing = true; } // fastCacheHits++; return cachedChunk; } Point coords = new Point(x, z); cachedChunk = getChunk(x, z); if ((cachedChunk != null) && (! cachedChunk.isReadOnly())) { dirtyChunks.add(coords); } cachedForEditing = true; return cachedChunk; } public void saveDirtyChunks() { for (Point coords: dirtyChunks) { saveChunk(cache.get(coords)); } dirtyChunks.clear(); cachedForEditing = false; } /** * Saves all dirty chunks and closes all files. Ensures that all changes are * saved and no system resources are being used, but the objects can still * be used; any subsequent operations will open files as needed again. */ public synchronized void flush() { saveDirtyChunks(); try { if (logger.isDebugEnabled()) { logger.debug("Closing " + regionFiles.size() + " region files"); } for (RegionFile regionFile: regionFiles.values()) { regionFile.close(); } } catch (IOException e) { throw new RuntimeException("I/O error while closing region files", e); } regionFiles.clear(); // float elapsed = (System.currentTimeMillis() - lastStatisticsTimestamp) / 1000f; // System.out.println("Loading " + chunksLoaded / elapsed + " chunks per second"); // System.out.println("Saving " + chunksSaved / elapsed + " chunks per second"); // System.out.println("Fast cache hits: " + fastCacheHits / elapsed + " per second"); // System.out.println("Cache hits: " + cacheHits / elapsed + " per second"); } @Override public void addEntity(int x, int y, int height, Entity entity) { addEntity(x + 0.5, y + 0.5, height + 1.5, entity); } @Override public void addEntity(double x, double y, double height, Entity entity) { if (readOnly) { throw new IllegalStateException("Read only"); } Chunk chunk = getChunkForEditing(((int) x) >> 4, ((int) y) >> 4); if ((chunk != null) && (! chunk.isReadOnly())) { Entity clone = (Entity) entity.clone(); clone.setPos(new double[] {x, height, y}); chunk.getEntities().add(clone); } } @Override public void addTileEntity(int x, int y, int height, TileEntity tileEntity) { if (readOnly) { throw new IllegalStateException("Read only"); } Chunk chunk = getChunkForEditing(x >> 4, y >> 4); if ((chunk != null) && (! chunk.isReadOnly())) { TileEntity clone = (TileEntity) tileEntity.clone(); clone.setX(x); clone.setY(height); clone.setZ(y); chunk.getTileEntities().add(clone); } } public int getCacheSize() { return cache.size(); } private synchronized void saveChunk(Chunk chunk) { // chunksSaved++; // updateStatistics(); // long start = System.currentTimeMillis(); // Do some sanity checks first // Check that all tile entities for which the chunk contains data are // actually there for (Iterator<TileEntity> i = chunk.getTileEntities().iterator(); i.hasNext(); ) { final TileEntity tileEntity = i.next(); final Set<Integer> blockIds = Constants.TILE_ENTITY_MAP.get(tileEntity.getId()); if (blockIds == null) { logger.warn("Unknown tile entity ID \"" + tileEntity.getId() + "\" encountered @ " + tileEntity.getX() + "," + tileEntity.getZ() + "," + tileEntity.getY() + "; can't check whether the corresponding block is there!"); } else { final int existingBlockId = chunk.getBlockType(tileEntity.getX() & 0xf, tileEntity.getY(), tileEntity.getZ() & 0xf); if (! blockIds.contains(existingBlockId)) { // The block at the specified location // is not a tile entity, or a different // tile entity. Remove the data i.remove(); if (logger.isDebugEnabled()) { logger.debug("Removing tile entity " + tileEntity.getId() + " @ " + tileEntity.getX() + "," + tileEntity.getZ() + "," + tileEntity.getY() + " because the block at that location is a " + BLOCK_TYPE_NAMES[existingBlockId]); } } } } // Check that there aren't multiple tile entities (of the same type, // otherwise they would have been removed above) in the same location Set<Point3i> occupiedCoords = new HashSet<>(); for (Iterator<TileEntity> i = chunk.getTileEntities().iterator(); i.hasNext(); ) { TileEntity tileEntity = i.next(); Point3i coords = new Point3i(tileEntity.getX(), tileEntity.getZ(), tileEntity.getY()); if (occupiedCoords.contains(coords)) { // There is already tile data for that location in the chunk; // remove this copy i.remove(); logger.warn("Removing tile entity " + tileEntity.getId() + " @ " + tileEntity.getX() + "," + tileEntity.getZ() + "," + tileEntity.getY() + " because there is already a tile entity of the same type at that location"); } else { occupiedCoords.add(coords); } } try { int x = chunk.getxPos(), z = chunk.getzPos(); RegionFile regionFile = getRegionFile(new Point(x >> 5, z >> 5)); try (NBTOutputStream out = new NBTOutputStream(regionFile.getChunkDataOutputStream(x & 31, z & 31))) { out.writeTag(chunk.toNBT()); } } catch (IOException e) { throw new RuntimeException("I/O error saving chunk", e); } // timeSpentSaving += System.currentTimeMillis() - start; } /** * Load a chunk. Returns <code>null</code> if the chunk is outside the * WorldPainter world boundaries. * * @param x The X coordinate in the Minecraft coordinate system of the chunk * to load. * @param z The Z coordinate in the Minecraft coordinate system of the chunk * to load. * @return The specified chunk, or <code>null</code> if the coordinates are * outside the WorldPainter world boundaries. */ private Chunk loadChunk(int x, int z) { // updateStatistics(); // if ((x < lowestX) || (x > highestX) || (z < lowestZ) || (z > highestZ)) { // return null; // } // long start = System.currentTimeMillis(); try { RegionFile regionFile = getRegionFile(new Point(x >> 5, z >> 5)); if (regionFile == null) { return null; } InputStream chunkIn = regionFile.getChunkDataInputStream(x & 31, z & 31); if (chunkIn != null) { // chunksLoaded++; try (NBTInputStream in = new NBTInputStream(chunkIn)) { CompoundTag tag = (CompoundTag) in.readTag(); // timeSpentLoading += System.currentTimeMillis() - start; boolean readOnly = honourReadOnlyChunks && dimension.getBitLayerValueAt(ReadOnly.INSTANCE, x << 4, z << 4); return (version == SUPPORTED_VERSION_1) ? new ChunkImpl(tag, maxHeight, readOnly) : new ChunkImpl2(tag, maxHeight, readOnly); } } else { // timeSpentLoading += System.currentTimeMillis() - start; return null; } } catch (IOException e) { throw new RuntimeException("I/O error loading chunk", e); } } // private void updateStatistics() { // long now = System.currentTimeMillis(); // if ((now - lastStatisticsTimestamp) > 5000) { // float elapsed = (now - lastStatisticsTimestamp) / 1000f; // System.out.println("Cached chunks: " + cache.size()); // System.out.println("Dirty chunks: " + dirtyChunks.size()); // System.out.println("Loading " + chunksLoaded / elapsed + " chunks per second"); // System.out.println("Saving " + chunksSaved / elapsed + " chunks per second"); // System.out.println("Fast cache hits: " + fastCacheHits / elapsed + " per second"); // System.out.println("Cache hits: " + cacheHits / elapsed + " per second"); // System.out.println("Time spent getting chunks: " + timeSpentGetting + " ms"); // if (chunksLoaded > 0) { // System.out.println("Time spent loading chunks: " + timeSpentLoading + " ms (" + (timeSpentLoading / chunksLoaded) + " ms per chunk"); // } // if (chunksSaved > 0) { // System.out.println("Time spent saving chunks: " + timeSpentSaving + " ms (" + (timeSpentSaving / chunksSaved) + " ms per chunk"); // } // lastStatisticsTimestamp = now; // chunksLoaded = 0; // chunksSaved = 0; // fastCacheHits = 0; // cacheHits = 0; // timeSpentGetting = 0; // timeSpentLoading = 0; // timeSpentSaving = 0; // } // } private void maintainCache() { while (cache.size() >= cacheSize) { Point lruCoords = lruList.remove(0); Chunk lruChunk = cache.remove(lruCoords); if (dirtyChunks.contains(lruCoords)) { saveChunk(lruChunk); dirtyChunks.remove(lruCoords); } } } private final File dimensionDir, regionDir; private final Map<Point, Chunk> cache; private final HashList<Point> lruList; private final Set<Point> dirtyChunks; private final Map<Point, RegionFile> regionFiles = new HashMap<>(); private final int maxHeight, version, cacheSize; private Chunk cachedChunk; private int cachedX = Integer.MIN_VALUE, cachedZ = Integer.MIN_VALUE; private boolean cachedForEditing; private int lowestX, highestX, lowestZ, highestZ; private final boolean readOnly, honourReadOnlyChunks; private final Dimension dimension; // private long lastStatisticsTimestamp; // private int chunksLoaded, chunksSaved, fastCacheHits, cacheHits; // private long timeSpentLoading, timeSpentSaving, timeSpentGetting; private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(MinecraftWorldImpl.class); private static final Chunk NON_EXISTANT_CHUNK = new Chunk() { @Override public int getBlockLightLevel(int x, int y, int z) {return 0;} @Override public int getBlockType(int x, int y, int z) {return 0;} @Override public void setBlockType(int x, int y, int z, int blockType) {} @Override public int getDataValue(int x, int y, int z) {return 0;} @Override public void setDataValue(int x, int y, int z, int dataValue) {} @Override public int getHeight(int x, int z) {return 0;} @Override public int getSkyLightLevel(int x, int y, int z) {return 0;} @Override public int getxPos() {return 0;} @Override public int getzPos() {return 0;} @Override public boolean isTerrainPopulated() {return false;} @Override public Tag toNBT() {return null;} @Override public void setBlockLightLevel(int x, int y, int z, int blockLightLevel) {} @Override public void setHeight(int x, int z, int height) {} @Override public Point getCoords() {return null;} @Override public void setSkyLightLevel(int x, int y, int z, int skyLightLevel) {} @Override public Material getMaterial(int x, int y, int z) {return null;} @Override public void setMaterial(int x, int y, int z, Material material) {} @Override public List<Entity> getEntities() {return null;} @Override public List<TileEntity> getTileEntities() {return null;} @Override public int getMaxHeight() {return 0;} @Override public void setTerrainPopulated(boolean terrainPopulated) {} @Override public boolean isBiomesAvailable() {return false;} @Override public int getBiome(int x, int z) {return 0;} @Override public void setBiome(int x, int z, int biome) {} @Override public boolean isReadOnly() {return false;} @Override public boolean isLightPopulated() {return false;} @Override public void setLightPopulated(boolean lightPopulated) {} @Override public long getInhabitedTime() {return 0;} @Override public void setInhabitedTime(long inhabitedTime) {} @Override public int getHighestNonAirBlock(int x, int z) {return 0;} @Override public int getHighestNonAirBlock() {return 0;} }; }