/* * To change this template, choose Tools | Templates * and open the template in the editor. */ package org.pepsoft.worldpainter.exporting; import org.jetbrains.annotations.NotNull; import org.pepsoft.minecraft.*; import org.pepsoft.util.*; import org.pepsoft.util.ProgressReceiver.OperationCancelled; import org.pepsoft.worldpainter.*; import org.pepsoft.worldpainter.Dimension; import org.pepsoft.worldpainter.gardenofeden.GardenExporter; import org.pepsoft.worldpainter.gardenofeden.Seed; import org.pepsoft.worldpainter.history.HistoryEntry; import org.pepsoft.worldpainter.layers.CombinedLayer; import org.pepsoft.worldpainter.layers.CustomLayer; import org.pepsoft.worldpainter.layers.GardenCategory; import org.pepsoft.worldpainter.layers.Layer; import org.pepsoft.worldpainter.util.FileInUseException; import org.pepsoft.worldpainter.vo.AttributeKeyVO; import org.pepsoft.worldpainter.vo.EventVO; import java.awt.*; import java.io.*; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.*; import java.util.List; import java.util.concurrent.*; import static org.pepsoft.minecraft.Block.BLOCKS; import static org.pepsoft.minecraft.Constants.*; import static org.pepsoft.worldpainter.Constants.*; /** * * @author pepijn */ public class WorldExporter { public WorldExporter(World2 world) { if (world == null) { throw new NullPointerException(); } this.world = world; this.selectedTiles = world.getTilesToExport(); this.selectedDimensions = world.getDimensionsToExport(); if ((selectedTiles != null) && (selectedDimensions.size() != 1)) { throw new IllegalArgumentException("When a tile selection is present exactly one dimension must be selected"); } } public World2 getWorld() { return world; } public File selectBackupDir(File worldDir) throws IOException { File baseDir = worldDir.getParentFile(); File minecraftDir = baseDir.getParentFile(); File backupsDir = new File(minecraftDir, "backups"); if ((! backupsDir.isDirectory()) && (! backupsDir.mkdirs())) { backupsDir = new File(System.getProperty("user.home"), "WorldPainter Backups"); if ((! backupsDir.isDirectory()) && (! backupsDir.mkdirs())) { throw new IOException("Could not create " + backupsDir); } } return new File(backupsDir, worldDir.getName() + "." + DATE_FORMAT.format(new Date())); } public Map<Integer, ChunkFactory.Stats> export(File baseDir, String name, File backupDir, ProgressReceiver progressReceiver) throws IOException, ProgressReceiver.OperationCancelled { // Sanity checks if ((world.getVersion() != SUPPORTED_VERSION_1) && (world.getVersion() != SUPPORTED_VERSION_2)) { throw new IllegalArgumentException("Not a supported version: 0x" + Integer.toHexString(world.getVersion())); } if ((selectedTiles == null) && (selectedDimensions != null)) { throw new IllegalArgumentException("Exporting a subset of dimensions not supported"); } // Backup existing level File worldDir = new File(baseDir, FileUtils.sanitiseName(name)); logger.info("Exporting world " + world.getName() + " to map at " + worldDir); if (worldDir.isDirectory()) { logger.info("Directory already exists; backing up to " + backupDir); if (! worldDir.renameTo(backupDir)) { throw new FileInUseException("Could not move " + worldDir + " to " + backupDir); } } // Record start of export long start = System.currentTimeMillis(); // Export dimensions Dimension dim0 = world.getDimension(0); Level level = new Level(world.getMaxHeight(), world.getVersion()); level.setSeed(dim0.getMinecraftSeed()); level.setName(name); Point spawnPoint = world.getSpawnPoint(); level.setSpawnX(spawnPoint.x); level.setSpawnY(Math.max(dim0.getIntHeightAt(spawnPoint), dim0.getWaterLevelAt(spawnPoint))); level.setSpawnZ(spawnPoint.y); if (world.getGameType() <= GAME_TYPE_ADVENTURE) { level.setGameType(world.getGameType()); level.setHardcore(false); level.setDifficulty(world.getDifficulty()); level.setAllowCommands(world.isAllowCheats()); } else if (world.getGameType() == World2.GAME_TYPE_HARDCORE) { level.setGameType(GAME_TYPE_SURVIVAL); level.setHardcore(true); level.setDifficulty(DIFFICULTY_HARD); level.setDifficultyLocked(true); level.setAllowCommands(false); } else { throw new InternalError("Don't know how to encode game type " + world.getGameType()); } Dimension.Border dim0Border = dim0.getBorder(); boolean endlessBorder = (dim0Border != null) && dim0Border.isEndless(); if (endlessBorder) { StringBuilder generatorOptions = new StringBuilder("3;"); switch (dim0Border) { case ENDLESS_LAVA: case ENDLESS_WATER: boolean bottomless = dim0.isBottomless(); int borderLevel = dim0.getBorderLevel(); int oceanDepth = Math.min(borderLevel / 2, 20); int dirtDepth = borderLevel - oceanDepth - (bottomless ? 1 : 0); if (! bottomless) { generatorOptions.append("1*minecraft:bedrock,"); } generatorOptions.append(dirtDepth); generatorOptions.append("*minecraft:dirt,"); generatorOptions.append(oceanDepth); generatorOptions.append((dim0Border == Dimension.Border.ENDLESS_WATER) ? "*minecraft:water;0;" : "*minecraft:lava;1;"); break; case ENDLESS_VOID: generatorOptions.append("1*minecraft:air;1;"); break; } generatorOptions.append(DEFAULT_GENERATOR_OPTIONS); level.setMapFeatures(false); level.setGenerator(Generator.FLAT); level.setGeneratorOptions(generatorOptions.toString()); } else { level.setMapFeatures(world.isMapFeatures()); level.setGenerator(world.getGenerator()); } if (world.getVersion() == SUPPORTED_VERSION_2) { if ((! endlessBorder) && (world.getGenerator() == Generator.FLAT) && (world.getGeneratorOptions() != null)) { level.setGeneratorOptions(world.getGeneratorOptions()); } World2.BorderSettings borderSettings = world.getBorderSettings(); level.setBorderCenterX(borderSettings.getCentreX()); level.setBorderCenterZ(borderSettings.getCentreY()); level.setBorderSize(borderSettings.getSize()); level.setBorderSafeZone(borderSettings.getSafeZone()); level.setBorderWarningBlocks(borderSettings.getWarningBlocks()); level.setBorderWarningTime(borderSettings.getWarningTime()); level.setBorderSizeLerpTarget(borderSettings.getSizeLerpTarget()); level.setBorderSizeLerpTime(borderSettings.getSizeLerpTime()); level.setBorderDamagePerBlock(borderSettings.getDamagePerBlock()); } // Save the level.dat file. This will also create a session.lock file, hopefully kicking out any Minecraft // instances which may have the map open: level.save(worldDir); Map<Integer, ChunkFactory.Stats> stats = new HashMap<>(); int selectedDimension; if (selectedTiles == null) { selectedDimension = -1; boolean first = true; for (Dimension dimension: world.getDimensions()) { if (dimension.getDim() < 0) { // This dimension will be exported as part of another // dimension, so skip it continue; } if (first) { first = false; } else if (progressReceiver != null) { progressReceiver.reset(); } stats.put(dimension.getDim(), exportDimension(worldDir, dimension, world.getVersion(), progressReceiver)); } } else { selectedDimension = selectedDimensions.iterator().next(); stats.put(selectedDimension, exportDimension(worldDir, world.getDimension(selectedDimension), world.getVersion(), progressReceiver)); } // Update the session.lock file, hopefully kicking out any Minecraft instances which may have tried to open the // map in the mean time: File sessionLockFile = new File(worldDir, "session.lock"); try (DataOutputStream sessionOut = new DataOutputStream(new FileOutputStream(sessionLockFile))) { sessionOut.writeLong(System.currentTimeMillis()); } // Record the export in the world history if (selectedTiles == null) { world.addHistoryEntry(HistoryEntry.WORLD_EXPORTED_FULL, name, worldDir); } else { world.addHistoryEntry(HistoryEntry.WORLD_EXPORTED_PARTIAL, name, worldDir, world.getDimension(selectedDimension).getName()); } // Log an event Configuration config = Configuration.getInstance(); if (config != null) { EventVO event = new EventVO(EVENT_KEY_ACTION_EXPORT_WORLD).duration(System.currentTimeMillis() - start); event.setAttribute(EventVO.ATTRIBUTE_TIMESTAMP, new Date(start)); event.setAttribute(ATTRIBUTE_KEY_MAX_HEIGHT, world.getMaxHeight()); event.setAttribute(ATTRIBUTE_KEY_VERSION, world.getVersion()); event.setAttribute(ATTRIBUTE_KEY_MAP_FEATURES, world.isMapFeatures()); event.setAttribute(ATTRIBUTE_KEY_GAME_TYPE, world.getGameType()); event.setAttribute(ATTRIBUTE_KEY_ALLOW_CHEATS, world.isAllowCheats()); event.setAttribute(ATTRIBUTE_KEY_GENERATOR, world.getGenerator().name()); if ((world.getVersion() == SUPPORTED_VERSION_2) && (world.getGenerator() == Generator.FLAT)) { event.setAttribute(ATTRIBUTE_KEY_GENERATOR_OPTIONS, world.getGeneratorOptions()); } Dimension dimension = world.getDimension(0); event.setAttribute(ATTRIBUTE_KEY_TILES, dimension.getTiles().size()); logLayers(dimension, event, ""); dimension = world.getDimension(1); if (dimension != null) { event.setAttribute(ATTRIBUTE_KEY_NETHER_TILES, dimension.getTiles().size()); logLayers(dimension, event, "nether."); } dimension = world.getDimension(2); if (dimension != null) { event.setAttribute(ATTRIBUTE_KEY_END_TILES, dimension.getTiles().size()); logLayers(dimension, event, "end."); } if (selectedDimension != -1) { event.setAttribute(ATTRIBUTE_KEY_EXPORTED_DIMENSION, selectedDimension); event.setAttribute(ATTRIBUTE_KEY_EXPORTED_DIMENSION_TILES, selectedTiles.size()); } if (world.getImportedFrom() != null) { event.setAttribute(ATTRIBUTE_KEY_IMPORTED_WORLD, true); } config.logEvent(event); } return stats; } protected ExportResults firstPass(MinecraftWorld minecraftWorld, Dimension dimension, Point regionCoords, Map<Point, Tile> tiles, boolean tileSelection, Map<Layer, LayerExporter> exporters, ChunkFactory chunkFactory, boolean ceiling, ProgressReceiver progressReceiver) throws ProgressReceiver.OperationCancelled, IOException { if (logger.isDebugEnabled()) { logger.debug("Start of first pass for region {},{}", regionCoords.x, regionCoords.y); } if (progressReceiver != null) { if (ceiling) { progressReceiver.setMessage("Generating ceiling"); } else { progressReceiver.setMessage("Generating landscape"); } } int lowestChunkX = (regionCoords.x << 5) - 1; int highestChunkX = (regionCoords.x << 5) + 32; int lowestChunkY = (regionCoords.y << 5) - 1; int highestChunkY = (regionCoords.y << 5) + 32; int lowestRegionChunkX = lowestChunkX + 1; int highestRegionChunkX = highestChunkX - 1; int lowestRegionChunkY = lowestChunkY + 1; int highestRegionChunkY = highestChunkY - 1; ExportResults exportResults = new ExportResults(); int chunkNo = 0; int ceilingDelta = dimension.getMaxHeight() - dimension.getCeilingHeight(); for (int chunkX = lowestChunkX; chunkX <= highestChunkX; chunkX++) { for (int chunkY = lowestChunkY; chunkY <= highestChunkY; chunkY++) { ChunkFactory.ChunkCreationResult chunkCreationResult = createChunk(dimension, chunkFactory, tiles, chunkX, chunkY, tileSelection, exporters, ceiling); if (chunkCreationResult != null) { if ((chunkX >= lowestRegionChunkX) && (chunkX <= highestRegionChunkX) && (chunkY >= lowestRegionChunkY) && (chunkY <= highestRegionChunkY)) { exportResults.chunksGenerated = true; exportResults.stats.landArea += chunkCreationResult.stats.landArea; exportResults.stats.surfaceArea += chunkCreationResult.stats.surfaceArea; exportResults.stats.waterArea += chunkCreationResult.stats.waterArea; } if (ceiling) { Chunk invertedChunk = new InvertedChunk(chunkCreationResult.chunk, ceilingDelta); Chunk existingChunk = minecraftWorld.getChunkForEditing(chunkX, chunkY); if (existingChunk == null) { existingChunk = (world.getVersion() == SUPPORTED_VERSION_1) ? new ChunkImpl(chunkX, chunkY, world.getMaxHeight()) : new ChunkImpl2(chunkX, chunkY, world.getMaxHeight()); minecraftWorld.addChunk(existingChunk); } mergeChunks(invertedChunk, existingChunk); } else { minecraftWorld.addChunk(chunkCreationResult.chunk); } } chunkNo++; if (progressReceiver != null) { progressReceiver.setProgress((float) chunkNo / 1156); } } } if (logger.isDebugEnabled()) { logger.debug("End of first pass for region {},{}", regionCoords.x, regionCoords.y); } return exportResults; } /** * Merge the non-air blocks from the source chunk into the destination chunk. * * @param source The source chunk. * @param destination The destination chunk. */ private void mergeChunks(Chunk source, Chunk destination) { final int maxHeight = source.getMaxHeight(); if (maxHeight != destination.getMaxHeight()) { throw new IllegalArgumentException("Different maxHeights"); } for (int y = 0; y < maxHeight; y++) { for (int x = 0; x < 16; x++) { for (int z = 0; z < 16; z++) { int destinationBlock = destination.getBlockType(x, y, z); if (! BLOCKS[destinationBlock].solid) { // Insubstantial blocks in the destination are only // replaced by solid ones; air is replaced by anything // that's not air int sourceBlock = source.getBlockType(x, y, z); if ((destinationBlock == BLK_AIR) ? (sourceBlock != BLK_AIR) : BLOCKS[sourceBlock].solid) { destination.setMaterial(x, y, z, source.getMaterial(x, y, z)); destination.setBlockLightLevel(x, y, z, source.getBlockLightLevel(x, y, z)); destination.setSkyLightLevel(x, y, z, source.getSkyLightLevel(x, y, z)); } } } } } } protected List<Fixup> secondPass(List<Layer> secondaryPassLayers, Dimension dimension, MinecraftWorld minecraftWorld, Map<Layer, LayerExporter> exporters, Collection<Tile> tiles, Point regionCoords, ProgressReceiver progressReceiver) throws ProgressReceiver.OperationCancelled { // Apply other secondary pass layers if (logger.isDebugEnabled()) { logger.debug("Start of second pass for region {},{}", regionCoords.x, regionCoords.y); } int layerCount = secondaryPassLayers.size(), counter = 0; Rectangle area = new Rectangle((regionCoords.x << 9) - 16, (regionCoords.y << 9) - 16, 544, 544); Rectangle exportedArea = new Rectangle((regionCoords.x << 9), (regionCoords.y << 9), 512, 512); List<Fixup> fixups = new ArrayList<>(); // boolean frost = false; for (Layer layer: secondaryPassLayers) { // if (layer instanceof Frost) { // frost = true; // continue; // } @SuppressWarnings("unchecked") SecondPassLayerExporter exporter = (SecondPassLayerExporter) exporters.get(layer); if (logger.isDebugEnabled()) { logger.debug("Exporting layer {} for region {},{}", layer, regionCoords.x, regionCoords.y); } if (progressReceiver != null) { if (minecraftWorld instanceof InvertedWorld) { progressReceiver.setMessage("Exporting layer " + layer + " for ceiling"); } else { progressReceiver.setMessage("Exporting layer " + layer); } } List<Fixup> layerFixups = exporter.render(dimension, area, exportedArea, minecraftWorld); if (layerFixups != null) { fixups.addAll(layerFixups); } if (progressReceiver != null) { counter++; progressReceiver.setProgress((float) counter / layerCount); } } // Garden / seeds first and second pass GardenExporter gardenExporter = new GardenExporter(); Set<Seed> firstPassProcessedSeeds = new HashSet<>(); Set<Seed> secondPassProcessedSeeds = new HashSet<>(); tiles.stream().filter(tile -> tile.getLayers().contains(GardenCategory.INSTANCE)).forEach(tile -> { gardenExporter.firstPass(dimension, tile, minecraftWorld, firstPassProcessedSeeds); gardenExporter.secondPass(dimension, tile, minecraftWorld, secondPassProcessedSeeds); }); // Apply frost layer // TODO: why did we used to do this in a separate step? There must have been a reason... // if (frost) { // @SuppressWarnings("unchecked") // SecondPassLayerExporter<Layer> exporter = (SecondPassLayerExporter<Layer>) exporters.get(Frost.INSTANCE); // exporter.render(dimension, area, exportedArea, minecraftWorld); // if (progressReceiver != null) { // counter++; // progressReceiver.setProgress((float) counter / layerCount); // } // } // TODO: trying to do this for every region should work but is not very // elegant if ((dimension.getDim() == 0) && world.isCreateGoodiesChest()) { Point goodiesPoint = (Point) world.getSpawnPoint().clone(); goodiesPoint.translate(3, 3); int height = Math.min(dimension.getIntHeightAt(goodiesPoint) + 1, dimension.getMaxHeight() - 1); minecraftWorld.setMaterialAt(goodiesPoint.x, goodiesPoint.y, height, Material.CHEST_NORTH); Chunk chunk = minecraftWorld.getChunk(goodiesPoint.x >> 4, goodiesPoint.y >> 4); if (chunk != null) { Chest goodiesChest = createGoodiesChest(); goodiesChest.setX(goodiesPoint.x); goodiesChest.setY(height); goodiesChest.setZ(goodiesPoint.y); chunk.getTileEntities().add(goodiesChest); } } if (logger.isDebugEnabled()) { logger.debug("End of second pass for region {},{}", regionCoords.x, regionCoords.y); } return fixups; } protected void lightingPass(MinecraftWorld minecraftWorld, Point regionCoords, ProgressReceiver progressReceiver) throws ProgressReceiver.OperationCancelled { if (progressReceiver != null) { progressReceiver.setMessage("Calculating primary light"); } LightingCalculator lightingVolume = new LightingCalculator(minecraftWorld); // Calculate primary light int lightingLowMark = Integer.MAX_VALUE, lightingHighMark = Integer.MIN_VALUE; int lowestChunkX = (regionCoords.x << 5) - 1; int highestChunkX = (regionCoords.x << 5) + 32; int lowestChunkY = (regionCoords.y << 5) - 1; int highestChunkY = (regionCoords.y << 5) + 32; int total = highestChunkX - lowestChunkX + 1, count = 0; for (int chunkX = lowestChunkX; chunkX <= highestChunkX; chunkX++) { for (int chunkY = lowestChunkY; chunkY <= highestChunkY; chunkY++) { Chunk chunk = minecraftWorld.getChunk(chunkX, chunkY); if (chunk != null) { int[] levels = lightingVolume.calculatePrimaryLight(chunk); if (levels[0] < lightingLowMark) { lightingLowMark = levels[0]; } if (levels[1] > lightingHighMark) { lightingHighMark = levels[1]; } } } if (progressReceiver != null) { progressReceiver.setProgress(0.2f * ++count / total); } } if (lightingLowMark != Integer.MAX_VALUE) { if (progressReceiver != null) { progressReceiver.setMessage("Propagating light"); } // Calculate secondary light Box originalDirtyArea = new Box((regionCoords.x << 9) - 16, ((regionCoords.x + 1) << 9) + 15, lightingLowMark, lightingHighMark, (regionCoords.y << 9) - 16, ((regionCoords.y + 1) << 9) + 15); int originalVolume = originalDirtyArea.getVolume(); Box dirtyArea = originalDirtyArea.clone(); lightingVolume.setDirtyArea(dirtyArea); while (lightingVolume.calculateSecondaryLight()) { if (progressReceiver != null) { progressReceiver.setProgress(0.2f + 0.8f * (originalVolume - dirtyArea.getVolume()) / originalVolume); } } } if (progressReceiver != null) { progressReceiver.setProgress(1.0f); } } protected final ExportResults exportRegion(MinecraftWorld minecraftWorld, Dimension dimension, Dimension ceiling, Point regionCoords, boolean tileSelection, Map<Layer, LayerExporter> exporters, Map<Layer, LayerExporter> ceilingExporters, ChunkFactory chunkFactory, ChunkFactory ceilingChunkFactory, ProgressReceiver progressReceiver) throws ProgressReceiver.OperationCancelled, IOException { if (progressReceiver != null) { progressReceiver.setMessage("Exporting region " + regionCoords.x + "," + regionCoords.y + " of " + dimension.getName()); } int lowestTileX = (regionCoords.x << 2) - 1; int highestTileX = lowestTileX + 5; int lowestTileY = (regionCoords.y << 2) - 1; int highestTileY = lowestTileY + 5; Map<Point, Tile> tiles = new HashMap<>(), ceilingTiles = new HashMap<>(); for (int tileX = lowestTileX; tileX <= highestTileX; tileX++) { for (int tileY = lowestTileY; tileY <= highestTileY; tileY++) { Point tileCoords = new Point(tileX, tileY); Tile tile = dimension.getTile(tileCoords); if ((tile != null) && ((! tileSelection) || dimension.getWorld().getTilesToExport().contains(tileCoords))) { tiles.put(tileCoords, tile); } if (ceiling != null) { tile = ceiling.getTile(tileCoords); if ((tile != null) && ((! tileSelection) || dimension.getWorld().getTilesToExport().contains(tileCoords))) { ceilingTiles.put(tileCoords, tile); } } } } Set<Layer> allLayers = new HashSet<>(), allCeilingLayers = new HashSet<>(); for (Tile tile: tiles.values()) { allLayers.addAll(tile.getLayers()); } // Add layers that have been configured to be applied everywhere Set<Layer> minimumLayers = dimension.getMinimumLayers(), ceilingMinimumLayers = (ceiling != null) ? ceiling.getMinimumLayers() : null; allLayers.addAll(minimumLayers); // Remove layers which have been excluded for export allLayers.removeIf(layer -> (layer instanceof CustomLayer) && (!((CustomLayer) layer).isExport())); List<Layer> secondaryPassLayers = new ArrayList<>(), ceilingSecondaryPassLayers = new ArrayList<>(); for (Layer layer: allLayers) { LayerExporter exporter = layer.getExporter(); if (exporter instanceof SecondPassLayerExporter) { secondaryPassLayers.add(layer); } } Collections.sort(secondaryPassLayers); // Set up export of ceiling if (ceiling != null) { for (Tile tile: ceilingTiles.values()) { allCeilingLayers.addAll(tile.getLayers()); } allCeilingLayers.addAll(ceilingMinimumLayers); // Remove layers which have been excluded for export allCeilingLayers.removeIf(layer -> (layer instanceof CustomLayer) && (!((CustomLayer) layer).isExport())); for (Layer layer: allCeilingLayers) { LayerExporter exporter = layer.getExporter(); if (exporter instanceof SecondPassLayerExporter) { ceilingSecondaryPassLayers.add(layer); } } Collections.sort(ceilingSecondaryPassLayers); } long t1 = System.currentTimeMillis(); // First pass. Create terrain and apply layers which don't need access // to neighbouring chunks ExportResults exportResults = firstPass(minecraftWorld, dimension, regionCoords, tiles, tileSelection, exporters, chunkFactory, false, (progressReceiver != null) ? new SubProgressReceiver(progressReceiver, 0.0f, ((ceiling != null) ? 0.225f : 0.45f)) : null); ExportResults ceilingExportResults = null; if (ceiling != null) { // First pass for the ceiling. Create terrain and apply layers which // don't need access to neighbouring chunks ceilingExportResults = firstPass(minecraftWorld, ceiling, regionCoords, ceilingTiles, tileSelection, ceilingExporters, ceilingChunkFactory, true, (progressReceiver != null) ? new SubProgressReceiver(progressReceiver, 0.225f, 0.225f) : null); } if (exportResults.chunksGenerated || ((ceiling != null) && ceilingExportResults.chunksGenerated)) { // Second pass. Apply layers which need information from or apply // changes to neighbouring chunks long t2 = System.currentTimeMillis(); List<Fixup> myFixups = secondPass(secondaryPassLayers, dimension, minecraftWorld, exporters, tiles.values(), regionCoords, (progressReceiver != null) ? new SubProgressReceiver(progressReceiver, 0.45f, (ceiling != null) ? 0.05f : 0.1f) : null); if ((myFixups != null) && (! myFixups.isEmpty())) { exportResults.fixups = myFixups; } if (ceiling != null) { // Second pass for ceiling. Apply layers which need information // from or apply changes to neighbouring chunks. Fixups are not // supported for the ceiling for now. TODO: implement secondPass(ceilingSecondaryPassLayers, ceiling, new InvertedWorld(minecraftWorld, ceiling.getMaxHeight() - ceiling.getCeilingHeight()), ceilingExporters, ceilingTiles.values(), regionCoords, (progressReceiver != null) ? new SubProgressReceiver(progressReceiver, 0.4f, 0.05f) : null); } // Post processing. Fix covered grass blocks, things like that long t3 = System.currentTimeMillis(); PostProcessor.postProcess(minecraftWorld, new Rectangle(regionCoords.x << 9, regionCoords.y << 9, 512, 512), (progressReceiver != null) ? new SubProgressReceiver(progressReceiver, 0.55f, 0.1f) : null); // Third pass. Calculate lighting long t4 = System.currentTimeMillis(); lightingPass(minecraftWorld, regionCoords, (progressReceiver != null) ? new SubProgressReceiver(progressReceiver, 0.65f, 0.35f) : null); long t5 = System.currentTimeMillis(); if ("true".equalsIgnoreCase(System.getProperty("org.pepsoft.worldpainter.devMode"))) { String timingMessage = (t2 - t1) + ", " + (t3 - t2) + ", " + (t4 - t3) + ", " + (t5 - t4) + ", " + (t5 - t1); // System.out.println("Export timing: " + timingMessage); synchronized (TIMING_FILE_LOCK) { try (PrintWriter out = new PrintWriter(new FileOutputStream("exporttimings.csv", true))) { out.println(timingMessage); } } } } return exportResults; } protected final void logLayers(Dimension dimension, EventVO event, String prefix) { StringBuilder sb = new StringBuilder(); for (Layer layer: dimension.getAllLayers(false)) { if (sb.length() > 0) { sb.append(','); } sb.append(layer.getName()); } if (sb.length() > 0) { event.setAttribute(new AttributeKeyVO<>(prefix + "layers"), sb.toString()); } } private ChunkFactory.ChunkCreationResult createChunk(Dimension dimension, ChunkFactory chunkFactory, Map<Point, Tile> tiles, int chunkX, int chunkY, boolean tileSelection, Map<Layer, LayerExporter> exporters, boolean ceiling) { final int tileX = chunkX >> 3; final int tileY = chunkY >> 3; final Point tileCoords = new Point(tileX, tileY); final Dimension.Border borderType = dimension.getBorder(); final boolean endlessBorder = (borderType != null) && borderType.isEndless(); final boolean border = (borderType != null) && (! endlessBorder) && (dimension.getBorderSize() > 0); if (tileSelection) { // Tile selection. Don't export bedrock wall or border tiles if (tiles.containsKey(tileCoords)) { return chunkFactory.createChunk(chunkX, chunkY); } else { return null; } } else { if (dimension.getTile(tileCoords) != null) { return chunkFactory.createChunk(chunkX, chunkY); } else if ((! ceiling) && (! endlessBorder)) { // Might be a border or bedrock wall chunk (but not if this is a // ceiling dimension or the border is an endless border) if (border && isBorderChunk(dimension, chunkX, chunkY)) { return BorderChunkFactory.create(chunkX, chunkY, dimension, exporters); } else if (dimension.isBedrockWall() && (border ? (isBorderChunk(dimension, chunkX - 1, chunkY) || isBorderChunk(dimension, chunkX, chunkY - 1) || isBorderChunk(dimension, chunkX + 1, chunkY) || isBorderChunk(dimension, chunkX, chunkY + 1)) : (isWorldChunk(dimension, chunkX - 1, chunkY) || isWorldChunk(dimension, chunkX, chunkY - 1) || isWorldChunk(dimension, chunkX + 1, chunkY) || isWorldChunk(dimension, chunkX, chunkY + 1)))) { // Bedrock wall is turned on and a neighbouring chunk is a // border chunk (if there is a border), or a world chunk (if // there is no border) return BedrockWallChunk.create(chunkX, chunkY, dimension); } else { // Outside known space return null; } } else { // Not a world tile, and we're a ceiling dimension, or the // border is an endless border, so we don't export borders and // bedrock walls return null; } } } private boolean isWorldChunk(Dimension dimension, int x, int y) { return dimension.getTile(x >> 3, y >> 3) != null; } private boolean isBorderChunk(Dimension dimension, int x, int y) { final int tileX = x >> 3, tileY = y >> 3; final int borderSize = dimension.getBorderSize(); if ((dimension.getBorder() == null) || (borderSize == 0)) { // There is no border configured, so definitely no border chunk return false; } else if (dimension.getTile(tileX, tileY) != null) { // There is a tile here, so definitely no border chunk return false; } else { // Check whether there is a tile within a radius of *borderSize*, // in which case we are on a border tile for (int dx = -borderSize; dx <= borderSize; dx++) { for (int dy = -borderSize; dy <= borderSize; dy++) { if (dimension.getTile(tileX + dx, tileY + dy) != null) { // Tile found, we are a border chunk! return true; } } } // No tiles found within a radius of *borderSize*, we are no border // chunk return false; } } private ChunkFactory.Stats exportDimension(final File worldDir, final Dimension dimension, final int version, ProgressReceiver progressReceiver) throws ProgressReceiver.OperationCancelled, IOException { if (progressReceiver != null) { progressReceiver.setMessage("Exporting " + dimension.getName() + " dimension"); } long start = System.currentTimeMillis(); final File dimensionDir; final Dimension ceiling; switch (dimension.getDim()) { case DIM_NORMAL: dimensionDir = worldDir; ceiling = dimension.getWorld().getDimension(DIM_NORMAL_CEILING); break; case DIM_NETHER: dimensionDir = new File(worldDir, "DIM-1"); ceiling = dimension.getWorld().getDimension(DIM_NETHER_CEILING); break; case DIM_END: dimensionDir = new File(worldDir, "DIM1"); ceiling = dimension.getWorld().getDimension(DIM_END_CEILING); break; default: throw new IllegalArgumentException("Dimension " + dimension.getDim() + " not supported"); } File regionDir = new File(dimensionDir, "region"); if (! regionDir.exists()) { if (! regionDir.mkdirs()) { throw new RuntimeException("Could not create directory " + regionDir); } } final ChunkFactory.Stats collectedStats = new ChunkFactory.Stats(); boolean wasDirty = dimension.isDirty(), ceilingWasDirty = (ceiling != null) && ceiling.isDirty(); dimension.rememberChanges(); if (ceiling != null) { ceiling.rememberChanges(); } try { final Map<Layer, LayerExporter> exporters = setupDimensionForExport(dimension); final Map<Layer, LayerExporter> ceilingExporters = (ceiling != null) ? setupDimensionForExport(ceiling) : null; // Determine regions to export int lowestRegionX = Integer.MAX_VALUE, highestRegionX = Integer.MIN_VALUE, lowestRegionZ = Integer.MAX_VALUE, highestRegionZ = Integer.MIN_VALUE; final Set<Point> regions = new HashSet<>(), exportedRegions = new HashSet<>(); final boolean tileSelection = selectedTiles != null; if (tileSelection) { // Sanity check assert selectedDimensions.size() == 1; assert selectedDimensions.contains(dimension.getDim()); for (Point tile: selectedTiles) { int regionX = tile.x >> 2; int regionZ = tile.y >> 2; regions.add(new Point(regionX, regionZ)); if (regionX < lowestRegionX) { lowestRegionX = regionX; } if (regionX > highestRegionX) { highestRegionX = regionX; } if (regionZ < lowestRegionZ) { lowestRegionZ = regionZ; } if (regionZ > highestRegionZ) { highestRegionZ = regionZ; } } } else { for (Tile tile: dimension.getTiles()) { // Also add regions for any bedrock wall and/or border // tiles, if present int r = (((dimension.getBorder() != null) && (! dimension.getBorder().isEndless())) ? dimension.getBorderSize() : 0) + (((dimension.getBorder() == null) || (! dimension.getBorder().isEndless())) && dimension.isBedrockWall() ? 1 : 0); for (int dx = -r; dx <= r; dx++) { for (int dy = -r; dy <= r; dy++) { int regionX = (tile.getX() + dx) >> 2; int regionZ = (tile.getY() + dy) >> 2; regions.add(new Point(regionX, regionZ)); if (regionX < lowestRegionX) { lowestRegionX = regionX; } if (regionX > highestRegionX) { highestRegionX = regionX; } if (regionZ < lowestRegionZ) { lowestRegionZ = regionZ; } if (regionZ > highestRegionZ) { highestRegionZ = regionZ; } } } } if (ceiling != null) { for (Tile tile: ceiling.getTiles()) { int regionX = tile.getX() >> 2; int regionZ = tile.getY() >> 2; regions.add(new Point(regionX, regionZ)); if (regionX < lowestRegionX) { lowestRegionX = regionX; } if (regionX > highestRegionX) { highestRegionX = regionX; } if (regionZ < lowestRegionZ) { lowestRegionZ = regionZ; } if (regionZ > highestRegionZ) { highestRegionZ = regionZ; } } } } // Sort the regions to export the first two rows together, and then // row by row, to get the optimum tempo of performing fixups List<Point> sortedRegions = new ArrayList<>(regions.size()); if (lowestRegionZ == highestRegionZ) { // No point in sorting it sortedRegions.addAll(regions); } else { for (int x = lowestRegionX; x <= highestRegionX; x++) { for (int z = lowestRegionZ; z <= (lowestRegionZ + 1); z++) { Point regionCoords = new Point(x, z); if (regions.contains(regionCoords)) { sortedRegions.add(regionCoords); } } } for (int z = lowestRegionZ + 2; z <= highestRegionZ; z++) { for (int x = lowestRegionX; x <= highestRegionX; x++) { Point regionCoords = new Point(x, z); if (regions.contains(regionCoords)) { sortedRegions.add(regionCoords); } } } } final WorldPainterChunkFactory chunkFactory = new WorldPainterChunkFactory(dimension, exporters, world.getVersion(), world.getMaxHeight()); final WorldPainterChunkFactory ceilingChunkFactory = (ceiling != null) ? new WorldPainterChunkFactory(ceiling, ceilingExporters, world.getVersion(), world.getMaxHeight()) : null; Runtime runtime = Runtime.getRuntime(); runtime.gc(); long totalMemory = runtime.totalMemory(); long freeMemory = runtime.freeMemory(); long memoryInUse = totalMemory - freeMemory; long maxMemory = runtime.maxMemory(); long maxMemoryAvailable = maxMemory - memoryInUse; int maxThreadsByMem = (int) (maxMemoryAvailable / 250000000L); int threads; if (System.getProperty("org.pepsoft.worldpainter.threads") != null) { threads = Math.max(Math.min(Integer.parseInt(System.getProperty("org.pepsoft.worldpainter.threads")), sortedRegions.size()), 1); } else { threads = Math.max(Math.min(Math.min(maxThreadsByMem, runtime.availableProcessors()), sortedRegions.size()), 1); } logger.info("Using " + threads + " thread(s) for export (cores: " + runtime.availableProcessors() + ", available memory: " + (maxMemoryAvailable / 1048576L) + " MB)"); final Map<Point, List<Fixup>> fixups = new HashMap<>(); ExecutorService executor = Executors.newFixedThreadPool(threads, new ThreadFactory() { @Override public synchronized Thread newThread(Runnable r) { Thread thread = new Thread(threadGroup, r, "Exporter-" + nextID++); thread.setPriority(Thread.MIN_PRIORITY); return thread; } private final ThreadGroup threadGroup = new ThreadGroup("Exporters"); private int nextID = 1; }); final ParallelProgressManager parallelProgressManager = (progressReceiver != null) ? new ParallelProgressManager(progressReceiver, regions.size()) : null; try { // Export each individual region for (Point region: sortedRegions) { final Point regionCoords = region; executor.execute(() -> { ProgressReceiver progressReceiver1 = (parallelProgressManager != null) ? parallelProgressManager.createProgressReceiver() : null; if (progressReceiver1 != null) { try { progressReceiver1.checkForCancellation(); } catch (OperationCancelled e) { return; } } try { WorldRegion minecraftWorld = new WorldRegion(regionCoords.x, regionCoords.y, dimension.getMaxHeight(), version); ExportResults exportResults = null; try { exportResults = exportRegion(minecraftWorld, dimension, ceiling, regionCoords, tileSelection, exporters, ceilingExporters, chunkFactory, ceilingChunkFactory, (progressReceiver1 != null) ? new SubProgressReceiver(progressReceiver1, 0.0f, 0.9f) : null); if (logger.isDebugEnabled()) { logger.debug("Generated region " + regionCoords.x + "," + regionCoords.y); } if (exportResults.chunksGenerated) { synchronized (collectedStats) { collectedStats.landArea += exportResults.stats.landArea; collectedStats.surfaceArea += exportResults.stats.surfaceArea; collectedStats.waterArea += exportResults.stats.waterArea; } } } finally { if ((exportResults != null) && exportResults.chunksGenerated) { minecraftWorld.save(dimensionDir); } } synchronized (fixups) { if ((exportResults.fixups != null) && (! exportResults.fixups.isEmpty())) { fixups.put(new Point(regionCoords.x, regionCoords.y), exportResults.fixups); } exportedRegions.add(regionCoords); } // Apply all fixups which can be applied because // all surrounding regions have been exported // (or are not going to be), but only if another // thread is not already doing it if (performingFixups.tryAcquire()) { try { Map<Point, List<Fixup>> myFixups = new HashMap<>(); synchronized (fixups) { for (Iterator<Map.Entry<Point, List<Fixup>>> i = fixups.entrySet().iterator(); i.hasNext(); ) { Map.Entry<Point, List<Fixup>> entry = i.next(); Point fixupRegionCoords = entry.getKey(); if (isReadyForFixups(regions, exportedRegions, fixupRegionCoords)) { myFixups.put(fixupRegionCoords, entry.getValue()); i.remove(); } } } if (! myFixups.isEmpty()) { performFixups(worldDir, dimension, version, (progressReceiver1 != null) ? new SubProgressReceiver(progressReceiver1, 0.9f, 0.1f) : null, myFixups); } } finally { performingFixups.release(); } } } catch (Throwable t) { if (progressReceiver1 != null) { progressReceiver1.exceptionThrown(t); } else { logger.error("Exception while exporting region", t); } } }); } } finally { executor.shutdown(); try { executor.awaitTermination(366, TimeUnit.DAYS); } catch (InterruptedException e) { throw new RuntimeException("Thread interrupted while waiting for all tasks to finish", e); } } // It's possible for there to be fixups left, if thread A was // performing fixups and thread B added new ones and then quit synchronized (fixups) { if (! fixups.isEmpty()) { if (progressReceiver != null) { progressReceiver.setMessage("Doing remaining fixups for " + dimension.getName()); progressReceiver.reset(); } performFixups(worldDir, dimension, version, progressReceiver, fixups); } } // Calculate total size of dimension for (Point region: regions) { File file = new File(dimensionDir, "region/r." + region.x + "." + region.y + ((version == SUPPORTED_VERSION_2) ? ".mca" : ".mcr")); collectedStats.size += file.length(); } collectedStats.time = System.currentTimeMillis() - start; if (progressReceiver != null) { progressReceiver.setProgress(1.0f); } } finally { // Undo any changes we made (such as applying any combined layers) if (dimension.undoChanges()) { // TODO: some kind of cleverer undo mechanism (undo history // cloning?) so we don't mess up the user's redo history dimension.clearRedo(); dimension.armSavePoint(); } // If the dimension wasn't dirty make sure it still isn't dimension.setDirty(wasDirty); if (ceiling != null) { // Undo any changes we made (such as applying any combined layers) if (ceiling.undoChanges()) { // TODO: some kind of cleverer undo mechanism (undo history // cloning?) so we don't mess up the user's redo history ceiling.clearRedo(); ceiling.armSavePoint(); } // If the dimension wasn't dirty make sure it still isn't ceiling.setDirty(ceilingWasDirty); } } return collectedStats; } @NotNull private Map<Layer, LayerExporter> setupDimensionForExport(Dimension dimension) { // Gather all layers used on the map final Map<Layer, LayerExporter> exporters = new HashMap<>(); Set<Layer> allLayers = dimension.getAllLayers(false); allLayers.addAll(dimension.getMinimumLayers()); // If there are combined layers, apply them and gather any newly // added layers, recursively boolean done; do { done = true; for (Layer layer: new HashSet<>(allLayers)) { if ((layer instanceof CombinedLayer) && ((CombinedLayer) layer).isExport()) { // Apply the combined layer Set<Layer> addedLayers = ((CombinedLayer) layer).apply(dimension); // Remove the combined layer from the list allLayers.remove(layer); // Add any layers it might have added allLayers.addAll(addedLayers); // Signal that we have to go around at least once more, // in case any of the newly added layers are themselves // combined layers done = false; } } } while (! done); // Remove layers which have been excluded for export allLayers.removeIf(layer -> (layer instanceof CustomLayer) && (!((CustomLayer) layer).isExport())); // Load all layer settings into the exporters for (Layer layer: allLayers) { @SuppressWarnings("unchecked") LayerExporter exporter = layer.getExporter(); if (exporter != null) { exporter.setSettings(dimension.getLayerSettings(layer)); exporters.put(layer, exporter); } } return exporters; } protected boolean isReadyForFixups(Set<Point> regionsToExport, Set<Point> exportedRegions, Point coords) { for (int dx = -1; dx <= 1; dx++) { for (int dy = -1; dy <= 1; dy++) { if ((dx == 0) && (dy ==0)) { continue; } Point checkCoords = new Point(coords.x + dx, coords.y + dy); if (regionsToExport.contains(checkCoords) && (! exportedRegions.contains(checkCoords))) { // A surrounding region should be exported and hasn't yet // been, so the fixups can't be performed yet return false; } } } return true; } protected void performFixups(final File worldDir, final Dimension dimension, final int version, final ProgressReceiver progressReceiver, final Map<Point, List<Fixup>> fixups) throws OperationCancelled { long start = System.currentTimeMillis(); // Make sure to honour the read-only layer: MinecraftWorldImpl minecraftWorld = new MinecraftWorldImpl(worldDir, dimension, version, false, true, 512); int count = 0, total = 0; for (Map.Entry<Point, List<Fixup>> entry: fixups.entrySet()) { total += entry.getValue().size(); } for (Map.Entry<Point, List<Fixup>> entry: fixups.entrySet()) { if (progressReceiver != null) { progressReceiver.setMessage("Performing fixups for region " + entry.getKey().x + "," + entry.getKey().y); } List<Fixup> regionFixups = entry.getValue(); if (logger.isDebugEnabled()) { logger.debug("Performing " + regionFixups.size() + " fixups for region " + entry.getKey().x + "," + entry.getKey().y); } for (Fixup fixup: regionFixups) { fixup.fixup(minecraftWorld, dimension); if (progressReceiver != null) { progressReceiver.setProgress((float) ++count / total); } } if (logger.isDebugEnabled()) { logger.debug("Flushing region files (chunks in cache: " + minecraftWorld.getCacheSize() + ")"); } minecraftWorld.flush(); // Might affect performance of other threads also performing fixups, but should not cause errors } if (logger.isTraceEnabled()) { logger.trace("Fixups for " + fixups.size() + " regions took " + (System.currentTimeMillis() - start) + " ms"); } } private Chest createGoodiesChest() { List<InventoryItem> list = new ArrayList<>(); list.add(new InventoryItem(ITM_DIAMOND_SWORD, 0, 1, 0)); list.add(new InventoryItem(ITM_DIAMOND_SHOVEL, 0, 1, 1)); list.add(new InventoryItem(ITM_DIAMOND_PICKAXE, 0, 1, 2)); list.add(new InventoryItem(ITM_DIAMOND_AXE, 0, 1, 3)); list.add(new InventoryItem(BLK_SAPLING, 0, 64, 4)); list.add(new InventoryItem(BLK_SAPLING, 1, 64, 5)); list.add(new InventoryItem(BLK_SAPLING, 2, 64, 6)); list.add(new InventoryItem(BLK_BROWN_MUSHROOM, 0, 64, 7)); list.add(new InventoryItem(BLK_RED_MUSHROOM, 0, 64, 8)); list.add(new InventoryItem(ITM_BONE, 0, 64, 9)); list.add(new InventoryItem(ITM_WATER_BUCKET, 0, 1, 10)); list.add(new InventoryItem(ITM_WATER_BUCKET, 0, 1, 11)); list.add(new InventoryItem(ITM_COAL, 0, 64, 12)); list.add(new InventoryItem(ITM_IRON_INGOT, 0, 64, 13)); list.add(new InventoryItem(BLK_CACTUS, 0, 64, 14)); list.add(new InventoryItem(ITM_SUGAR_CANE, 0, 64, 15)); list.add(new InventoryItem(BLK_TORCH, 0, 64, 16)); list.add(new InventoryItem(ITM_BED, 0, 1, 17)); list.add(new InventoryItem(BLK_OBSIDIAN, 0, 64, 18)); list.add(new InventoryItem(ITM_FLINT_AND_STEEL, 0, 1, 19)); list.add(new InventoryItem(BLK_WOOD, 0, 64, 20)); list.add(new InventoryItem(BLK_CRAFTING_TABLE, 0, 1, 21)); list.add(new InventoryItem(BLK_END_PORTAL_FRAME, 0, 12, 22)); list.add(new InventoryItem(ITM_EYE_OF_ENDER, 0, 12, 23)); Chest chest = new Chest(); chest.setItems(list); return chest; } protected final World2 world; protected final Set<Integer> selectedDimensions; protected final Set<Point> selectedTiles; protected final Semaphore performingFixups = new Semaphore(1); private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyyMMddHHmmss"); private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(WorldExporter.class); private static final Object TIMING_FILE_LOCK = new Object(); private static final String DEFAULT_GENERATOR_OPTIONS = "village,mineshaft(chance=0.01),stronghold(distance=32 count=3 spread=3),biome_1(distance=32),dungeon,decoration,lake,lava_lake,oceanmonument(spacing=32 separation=5)"; public static class ExportResults { /** * Whether any chunks were actually generated for this region. */ public boolean chunksGenerated; /** * Statistics for the generated chunks, if any */ public final ChunkFactory.Stats stats = new ChunkFactory.Stats(); /** * Fixups which have to be performed synchronously after all regions * have been generated */ public List<Fixup> fixups; } }